1. 程式人生 > >Java 9 模組解耦的設計策略

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