Java 9 模塊解耦的設計策略


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](search.maven.org/classic/#se… AND a%3A"maven-compiler-plugin") 以獲取最新版本的 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工廠。

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> 

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 模式的示例代碼。

原文鏈接:www.baeldung.com/java-module…

作者:Alejandro Ugarte

譯者:Darren Luo

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM