1. 程式人生 > 實用技巧 >解耦Java模組的設計策略

解耦Java模組的設計策略

解耦Java模組的設計策略

點選左上角藍字,關注“鍋外的大佬”
專注分享國外最新技術內容

1. 概述

Java 平臺模組系統 (Java Platform Module System,JPMS)提供了更強的封裝、更可靠且更好的關注點分離。

但所有的這些方便的功能都需要付出代價。由於模組化的應用程式建立在依賴其他正常工作的模組的模組網上,因此在許多情況下,模組彼此緊密耦合。

這可能會導致我們認為模組化和鬆耦合是在同一系統中不能共存的特性。但事實上可以!

在本教程中,我們將深入探討兩種眾所周知的設計模式,我們可以用它們輕鬆的解耦 Java 模組。

2. 父模組

為了展示用於解耦 Java 模組的設計模式,我們將構建一個多模組 Maven 專案的 demo。

為了保持程式碼簡單,專案最初將包含兩個 Maven 模組,每個 Maven 模組將被包裝為 Java 模組。

第一個模組將包含一個服務介面,以及兩個實現——服務provider。第二個模組將使用該provider解析 String 的值。

讓我們從建立名為 demoproject 的專案根目錄開始,定義專案的父 POM:

<packaging>
pom
</packaging>
<modules>

<module>
servicemodule
</module>

<module>
consumermodule
</module>
</modules>
<build>

<pluginManagement>

<plugins>

<plugin>

<groupId>
org.apache.maven.plugins
</groupId>

<artifactId>
maven-compiler-plugin
</artifactId>

<version>
3.8.1
</version>

<configuration>

<source>
11
</source>

<target>
11
</target>

</configuration>

</plugin>

</plugins>

</pluginManagement>
</build>

在該父 POM 的定義中有一些值得強調的細節。

首先,該檔案包含我們上面提到的兩個子模組,即 servicemodule 和 comsumermodule(我們稍後詳細討論它們)。

然後,由於我們使用 Java 11,因此我們的系統至少需要 Maven 3.5.0,因為 Maven 從該版本開始支援 Java 9 及更高版本。

最後,我們需要最低 3.8.0 版本的 Maven 編譯外掛。因此,為了保證我們是最新的,檢查 Maven Central 以獲取最新版本的 Maven 編譯外掛。

3. Service 模組

出於演示目的,我們使用一種快速上手的方式實現 servicemodule 模組,這樣我們可以清楚的發現這種設計帶來的缺陷。

讓我們將 service 介面和 service provider公開,將它們放置在同一個包中並匯出所有這些介面。這似乎是一個相當不錯的設計選擇,但我們稍後將看到,它大大的提高了專案的模組之間的耦合程度。

在專案的根目錄下,我們建立 servicemodule/src/main/java 目錄。然後,在定義包 com.baeldung.servicemodule,並在其中放置以下 TextService 介面:

public

interface

TextService

{

String
 processText
(
String
 text
);
}

TextService 介面非常簡單,現在讓我們定義服務provider。在同樣的包下,新增一個 Lowercase 實現:

public

class

LowercaseTextService

implements

TextService

{

@Override

public

String
 processText
(
String
 text
)

{

return
 text
.
toLowerCase
();

}
}

現在,讓我們新增一個 Uppercase 實現:

public

class

UppercaseTextService

implements

TextService

{

@Override

public

String
 processText
(
String
 text
)

{

return
 text
.
toUpperCase
();

}
}

最後,在 servicemodule/src/main/java 目錄下,讓我們引入模組描述,module-info.java:

module com
.
baeldung
.
servicemodule 
{
    exports com
.
baeldung
.
servicemodule
;
}

美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學

4. Consumer 模組

現在我們需要建立一個使用之前建立的服務provider之一的 consumer 模組。

讓我們新增以下 com.baeldung.consumermodule.Application 類:

public

class

Application

{

public

static

void
 main
(
String
 args
[])

{

TextService
 textService 
=

new

LowercaseTextService
();

System
.
out
.
println
(
textService
.
processText
(
"Hello from Baeldung!"
));

}
}

現在,在原始碼根目錄引入模組描述,module-info.java,應該在 consumermodule/src/main/java:

module com
.
baeldung
.
consumermodule 
{
    requires com
.
baeldung
.
servicemodule
;
}
美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學
美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學
美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學美國大學

最後,從 IDE 或命令控制檯中編譯原始檔並執行應用程式。

和我們預期的一樣,我們應該看到以下輸出:

hello 
from
 baeldung
!

這可以執行,但有一個值得注意的重要警告:我們不必將 service provider和 consumer 模組耦合起來。

由於我們讓provider對外部世界可見,consumer 模組會知道它們。

此外,這與軟體元件依賴於抽象相沖突。

5. Service provider工廠

我們可以輕鬆的移除模組間的耦合,通過只暴露 service 介面。相比之下,service provider不會被匯出,因此對 consumer 模組保持隱藏。consumer 模組只能看到 service 介面型別。

要實現這一點,我們需要:

1放置 service 介面到單獨的包中,該包將匯出到外部世界
2放置 service provider到不匯出的其他包中,該包不匯出
3建立匯出的工廠類。consumer 模組使用工廠類查詢 service provider
我們可以以設計模式的形式概念化以上步驟:公共的 service 介面、私有的 service provider以及公共的 service provider工廠。

香港理工大學 香港PolyU香港科技大學 香港HKUST香港教育學院 香港EdUHK香港嶺南大學 香港LU澳門科技大學 香港MUST澳門理工學院 澳門IPM香港浸會大學 香港HKBU澳門城市大學 澳門CUM香港城市大學 香港CityU澳門旅遊學院 澳門IFT共和理工學院 新加坡RP南洋藝術學院 新加坡NAFA義安理工學院 新加坡NP淡馬錫理工學院 新加坡TP科廷大學 新加坡Curtin 南洋理工學院 新加坡ntu 英迪大學 馬來INTI 世紀大學 馬來SEGi 亞太科技大學 馬來APU 精英大學 馬來HELP 漢陽大學 韓國Hanyang 慶熙大學 韓國Kyung 東國大學 韓國Dongguk 高麗大學 韓國Korea 建國大學 韓國Konkuk 中央大學 韓國Chung 延世大學 韓國Yonsei 成均館大學 韓國SKKU 弘益大學 韓國Hongik 梨花女子大學 韓國EWU早稻田大學 日本Waseda筑波大學 日本Tsukuba立命館大學 日本Ritsumeikan東京藝術大學 日本Tokyo名古屋大學 日本Nagoya九州大學 日本Kyushu美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學 美國大學

5.1. 公共的 Service 介面

要清楚的知道該模式如何運作,讓我們將 service 介面和 service provider放到不同的包中。介面將被匯出,但provider實現不會被匯出。

因此,將 TextService 移到叫做 com.baeldung.servicemodule.external 的新包。

5.2. 私有的 Service provider

然後,類似的將 LowercaseTextService 和 UppercaseTextService 移動到 com.baeldung.servicemodule.internal。

5.3. 公共的 Service Provider工廠

由於 service provider類現在是私有的且無法從其他模組訪問,我們將使用公共工廠類來提供消費者模組可用於獲取 service provider例項的簡單機制。

在 com.baeldung.servicemodule.external 包中,定義以下 TextServiceFactory 類:

public

class

TextServiceFactory

{

private

TextServiceFactory
()

{}

public

static

TextService
 getTextService
(
String
 name
)

{

return
 name
.
equalsIgnoreCase
(
"lowercase"
)

?

new

LowercaseTextService
():

new

UppercaseTextService
();

}
}

當然,我們可以讓工廠類稍微複雜一點。為了簡單起見,根據傳遞給 getTextService() 方法的 String 值簡單的建立 service provider。

現在,放置 module-info.java 檔案只以匯出 external 包:

module com
.
baeldung
.
servicemodule 
{
    exports com
.
baeldung
.
servicemodule
.
external
;

}
注意,我們只匯出了 service 介面和工廠類。實現是私有的,因此它們對其他模組不可見。

5.4. Application 類

現在,讓我們重構 Application 類,以便它可以使用 service provider工廠類:


public
static

void
 main
(
String
 args
[])

{

TextService
 textService 
=

TextServiceFactory
.
getTextService
(
"lowercase"
);

System
.
out
.
println
(
textService
.
processText
(
"Hello from Baeldung!"
));
}

和預期一樣,如果我們執行應用程式,可以導線相同的文字被列印到控制檯:

hello 
from
 baeldung
!

通過是 service 介面公開以及 service provider私有,有效的允許我們通過簡單的工廠類來解耦 service 和 consumer 模組。

當然,沒有一種模式是銀彈。和往常一樣,我們應該首先分析我們適合的情景。

6. Service 和 Consumer 模組

JPMS 通過 provides…with 和 uses 指令為 service 和 consumer 模組提供開箱即用的支援。

因此,我們可以使用該功能解耦模組,無需建立額外的工廠類。

要使 service 和 consumer 模組協同工作,我們需要執行以下操作:

1將 service 介面放到匯出介面的模組中
2在另一個模組中放置 service provider——provider被匯出
3在provider的模組描述中使用 provides…with 指令指定我們我們要使用的 TextService 實現
4將 Application 類放置到它自己的模組——consumer 模組
5在 consumer 模組描述中使用 uses 指令指定該模組是 consumer 模組
6在 consumer 模組中使用 Service Loader API 查詢 service provider
該方法非常強大,因為它利用了 service 和 consumer 模組帶來的所有功能。但這有一點棘手。





一方面,我們使 consumer 模組只依賴於 service 介面,不依賴 service provider。另一方面,我們甚至根本無法定義 service 應用者,但應用程式仍然可以編譯。

6.1. 父模組

要實現這種模式,我們需要重構父 POM 和現有模組。

由於 service 介面、service provider以及 consumer 將存在於不同的模組,我們首先修改父 POM 的 部分,以反映新結構:

<modules>

<module>
servicemodule
</module>

<module>
providermodule
</module>

<module>
consumermodule
</module>
</modules>

愛丁堡大學 英國Edinburgh伯明翰大學 英國UoB杜倫大學 英國Durham拉夫堡大學 英國LU華威大學 英國Warwick巴斯大學 英國Bath白金漢大學 英國UCB牛津大學 英國Oxon帝國理工學院 英國IC伯明翰大學 英國UoB格拉斯哥大學 英國Glasgow南安普頓大學 英國Soton諾丁漢大學 英國UNUK薩里大學 英國Surrey中央蘭開夏大學 英國UCLAN牛津布魯克斯大學 英國UNUK伯明翰城市大學 英國BCU西英格蘭大學 英國UWE阿伯丁大學 英國Aberdeen薩塞克斯大學 英國Sussex倫敦政治經濟學院 英國LSE約克大學 英國York倫敦大學學院 英國London謝菲爾德大學 英國Sheffield埃克塞特大學 英國Exon倫敦藝術大學 英國UAL金斯頓大學 英國Kingston倫敦瑪麗女王大學 英國QMUL布魯內爾大學 英國Brunel倫敦城市大學 英國CITY伯恩茅斯大學 英國BU樸次茅斯大學 英國UOP卡迪夫城市大學 英國UWIC赫特福德大學 英國Hertford愛丁堡藝術學院 英國ECA澳門大學 澳門UM香港中文大學 香港CUHK

6.2. Service 模組

TextService 介面將回到 com.baeldung.servicemodule 中。

我們將相應的更改模組描述:

module com
.
baeldung
.
servicemodule 
{
    exports com
.
baeldung
.
servicemodule
;
}

6.3. Provider模組

如上所述,provider模組是我們的實現,所以現在讓我們在這裡放置 LowerCaseTextService 和 UppercaseTextService。將它們放置到我們稱為 com.baeldung.providermodule 的包中。

最後,新增 module-info.java 檔案:

module com
.
baeldung
.
providermodule 
{
    requires com
.
baeldung
.
servicemodule
;
    provides com
.
baeldung
.
servicemodule
.
TextService
 with com
.
baeldung
.
providermodule
.
LowercaseTextService
;
}

6.4. Consumer 模組

現在,重構 consumer 模組。首先,將 Application 放回 com.baeldung.consumermodule 包。

接下來,重構 Application 類的 main() 方法,這樣它可以使用 ServiceLoader 類發現合適的實現:

public

static

void
 main
(
String
[]
 args
)

{

ServiceLoader
<
TextService
>
 services 
=

ServiceLoader
.
load
(
TextService
.
class
);

for

(
final

TextService
 service
:
 services
)

{

System
.
out
.
println
(
"The service "

+
 service
.
getClass
().
getSimpleName
()

+

" says: "

+
 service
.
parseText
(
"Hello from Baeldung!"
));

}
}

最後,重構 module-info.java 檔案:

module com
.
baeldung
.
consumermodule 
{
    requires com
.
baeldung
.
servicemodule
;
    uses com
.
baeldung
.
servicemodule
.
TextService
;
}

現在,讓我們執行應用程式。和期望的一樣,我們應該看到以下文字列印到控制檯:

The
 service 
LowercaseTextService
 says
:
 hello 
from
 baeldung
!

可以看到,實現這種模式比使用工廠類的稍微複雜一些。即便如此,額外的努力會獲得更靈活、鬆耦合的設計。

consumer 模組依賴於抽象,並且在執行時也可以輕鬆的在不同的 service provider中切換。

7. 總結

在本教程中,我們學習瞭如何解耦 Java 模組的兩種模式。

這兩種方法都使得 consumer 模組依賴於抽象,這在軟體元件設計中始終是期待的特性。

當然,每種都有其優點和缺點。對於第一種,我們獲得了很好的解耦,但我們不得不建立額外的工廠類。

對於第二種,為了解耦模組,我們不得不建立額外的抽象模組並新增使用 Service Loader API 的新的中間層 。

和往常一樣,本教程中的展示的所有示例都可以在 GitHub 上找到。務必檢視 Service Factory和 Provider Module 模式的示例程式碼。

-多倫多大學 Toronto滑鐵盧大學 Waterloo英屬哥倫比亞大學 UBC阿爾伯塔大學 UA西安大略大學 UWO-不列顛哥倫比亞大學 UBC-曼尼託巴大學 UM-西蒙弗雷澤大學 Clan麥吉爾大學 McGill維多利亞大學 UVic西蒙菲莎大學 SFU布魯克大學 Brock薩省大學 Saskatchewan紐芬蘭紀念大學 Newfoundland渥太華大學 Ottawa溫莎大學 Windsor溫莎大學 Windsor卡爾頓大學 Carleton卡普頓大學 CBU卡爾加里大學 UofC圭爾夫大學 Guelph肯考迪亞大學 Concordia溫哥華島大學 VIU百年理工學院 CENTENNIAL戴爾豪斯大學 Dalhousie昆特蘭理工大學 KPU西三一大學 TWU卡普蘭諾大學 Capilano亞崗昆學院 Algonquin勞裡埃大學 WLU喬治布朗學院 George Brown道格拉斯學院 Douglas哥倫比亞國際學院 CIC昆士蘭大學 澳洲UQ莫納什大學 澳洲Monash新南威爾士大學 澳洲UNSW澳洲國立大學 澳洲ANU麥考瑞大學 澳洲Macquarie阿德雷德大學 澳洲Adelaide墨爾本大學 澳洲Unimelb皇家墨爾本理工大學 澳洲RMIT悉尼科技大學 澳洲UTS臥龍崗大學 澳洲UOW南澳大學 澳洲UniSA迪肯大學 澳洲Deakin塔斯馬尼亞大學 澳洲UTAS悉尼大學 澳洲USYD澳洲紐卡斯爾大學 澳洲UON格里菲斯大學 澳洲GU昆士蘭科技大學 澳洲QUT中央昆士蘭大學 澳洲CQU維多利亞大學 澳洲UVic詹姆斯庫克大學 澳洲JCU堪培拉大學 澳洲UC南十字星大學 澳洲Southern Cross西悉尼大學 澳洲WSU斯威本科技大學 澳洲SUT南昆士蘭大學 澳洲USQ新英格蘭大學 澳洲England莫道克大學 澳洲MU拉籌伯大學 澳洲LTU西澳大學 澳洲UWA邦德大學 澳洲Bond陽光海岸大學 澳洲USC曼徹斯特大學 英國UoM雷丁大學 英國UoR亞伯大學 英國Aber卡迪夫大學 英國Cardiff利茲大學 英國Leeds利物浦大學 英國Liverpool考文垂大學 英國Coventry