Java 9 模塊解耦的設計策略

1. 概述

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

但全部的這些方便的功能都須要付出代價。因爲模塊化的應用程序創建在依賴其餘正常工做的模塊的模塊網上,所以在許多狀況下,模塊彼此緊密耦合git

這可能會致使咱們認爲模塊化和鬆耦合是在同一系統中不能共存的特性。但事實上能夠!github

在本教程中,咱們將深刻探討兩種衆所周知的設計模式,咱們能夠用它們輕鬆的解耦 Java 模塊。apache

2. 父模塊

爲了展現用於解耦 Java 模塊的設計模式,咱們將構建一個多模塊 Maven 項目的 demo。設計模式

爲了保持代碼簡單,項目最初將包含兩個 Maven 模塊,每一個 Maven 模塊將被包裝爲 Java 模塊bash

第一個模塊將包含一個服務接口,以及兩個實現——服務provider。第二個模塊將使用該provider解析 String 的值。maven

讓咱們從建立名爲 demoproject 的項目根目錄開始,定義項目的父 POM:ide

<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 的定義中有一些值得強調的細節。模塊化

首先,該文件包含咱們上面提到的兩個子模塊,即 servicemodulecomsumermodule(咱們稍後詳細討論它們)。學習

而後,因爲咱們使用 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

而後,相似的將 LowercaseTextServiceUppercaseTextService 移動到 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…withuses 指令爲 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 FactoryProvider Module 模式的示例代碼。

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

做者:Alejandro Ugarte

譯者:Darren Luo

相關文章
相關標籤/搜索