Javaモジュールを分離するための設計戦略

1. 概要

link:/java-9-modularity[Java Platform Module System](JPMS)は、カプセル化を強化し、信頼性を高め、懸念事項をより適切に分離します。
しかし、これらの便利な機能はすべて高価です。 モジュール化されたアプリケーションは、適切に機能するために他のモジュールに依存するモジュールのネットワーク上に構築されるため、*多くの場合、モジュールは互いに密結合されています*。
これにより、モジュール性と疎結合は、同じシステムでは共存できない機能であると考えるようになります。 しかし、実際には可能です!
このチュートリアルでは、Javaモジュールを簡単に分離するために使用できる2つの有名なデザインパターンを詳しく見ていきます。

2. 親モジュール

Javaモジュールの分離に使用するデザインパターンを紹介するために、デモ用のマルチモジュールMavenプロジェクトを構築します。
コードを単純にするために、プロジェクトには最初に2つのMavenモジュールとlink:/maven-multi-module-project-java-jpms [各MavenモジュールがJavaモジュールにラップされます]が含まれます。
最初のモジュールには、2つの実装(サービスプロバイダー)に加えて、サービスインターフェイスが含まれます。 2番目のモジュールは、_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の定義には、強調する価値のある詳細がいくつかあります。
最初に、*このファイルには、前述の2つの子モジュール、つまり_servicemodule_と_consumermodule_が含まれます(詳細については後で説明します)。
次に、Java 11を使用しているため、* system __、__には少なくともMaven 3.5.0が必要です。Mavenはそのバージョン以降のJava 9以降をサポートしています*。
最後に、少なくともlink:/maven-compiler-plugin[Maven compiler plugin]のバージョン3.8.0も必要です。 したがって、最新のものであることを確認するために、https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.apache.maven.plugins%22%20AND%を確認します。 20a%3A%22maven-compiler-plugin%22 [Maven Central] Mavenコンパイラプラグインの最新バージョン用。

3. サービスモジュール

デモの目的で、_servicemodule_モジュールを実装するために迅速で汚れたアプローチを使用して、この設計で発生する欠陥を明確に見つけることができます。
*サービスインターフェースとサービスプロバイダーを同じパッケージに入れて、それらをすべてエクスポートして、*を公開しましょう。 これはかなり良い設計の選択肢のように思えますが、すぐにわかるように、*プロジェクトのモジュール間のカップリングのレベルを大幅に高めます*。
プロジェクトのルートディレクトリの下に、_servicemodule / src / main / java_ディレクトリを作成します。 次に、パッケージ_com.baeldung.servicemodule_を定義して、次の_TextService_インターフェイスを配置する必要があります。
public interface TextService {

    String processText(String text);

}
_TextService_インターフェースは本当にシンプルなので、サービスプロバイダーを定義しましょう。
同じパッケージで、_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. 消費者モジュール

次に、前に作成したサービスプロバイダーの1つを使用するコンシューマモジュールを作成する必要があります。
次の_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!
確かにこれは機能しますが、注目に値する重要な注意事項が1つあります。
*プロバイダーを外部から見えるようにしているため、コンシューマーモジュールはそれらを認識しています。*
さらに、これはソフトウェアコンポーネントを抽象化に依存させることとの戦いです。

5. サービスプロバイダーファクトリー

サービスインターフェイスのみをエクスポートすることで、モジュール間の結合を簡単に削除できます*。 対照的に、サービスプロバイダーはエクスポートされないため、コンシューマモジュールから隠されたままになります。 コンシューマモジュールには、サービスインターフェイスタイプのみが表示されます。
これを実現するには、次のことが必要です。
  1. エクスポートされる別のパッケージにサービスインターフェイスを配置します。
    外の世界へ

  2. サービスプロバイダーを別のパッケージに配置します。
    輸出された

  3. エクスポートされるファクトリクラスを作成します。 消費者モジュールは使用します
    サービスプロバイダーを検索するファクトリクラス

    上記の手順を設計パターンの形式で概念化できます:*パブリックサービスインターフェイス、プライベートサービスプロバイダー、およびパブリックサービスプロバイダーファクトリ*。

* 5.1。 公共サービスインターフェース*

このパターンがどのように機能するかを明確に確認するために、サービスインターフェイスとサービスプロバイダーを異なるパッケージに配置してみましょう。 インターフェイスはエクスポートされますが、プロバイダーの実装はエクスポートされません。
_TextService_を新しいパッケージに移動して、_com.baeldung.servicemodule.external_を呼び出します。

* 5.2。 民間サービスプロバイダー*

次に、_LowercaseTextService_と_UppercaseTextService_を_com.baeldung.servicemodule.internal._に同様に移動します。

* 5.3。 公共サービスプロバイダー工場*

サービスプロバイダークラスはプライベートになり、他のモジュールからアクセスできないため、*パブリックファクトリクラスを使用して、コンシューマモジュールがサービスプロバイダーのインスタンスを取得するために使用できる単純なメカニズムを提供します*。
_com.baeldung.servicemodule.external_ packageで、次の_TextServiceFactory_クラスを定義しましょう。
public class TextServiceFactory {

    private TextServiceFactory() {}

    public static TextService getTextService(String name) {
        return name.equalsIgnoreCase("lowercase") ? new LowercaseTextService(): new UppercaseTextService();
    }

}
もちろん、ファクトリクラスをもう少し複雑にすることもできます。 ただし、物事を単純にするために、サービスプロバイダーは、_getTextService()_メソッドに渡される_String_値に基づいて作成されます。
次に、module-info.javaファイルを置き換えて、externalパッケージのみをエクスポートします。
module com.baeldung.servicemodule {
    exports com.baeldung.servicemodule.external;
}
*サービスインターフェイスとファクトリクラスのみをエクスポートしていることに注意してください*。 実装はプライベートであるため、他のモジュールからは見えません。

* 5.4。 アプリケーションクラス*

次に、_Application_クラスをリファクタリングして、サービスプロバイダーファクトリクラスを使用できるようにします。
public static void main(String args[]) {
    TextService textService = TextServiceFactory.getTextService("lowercase");
    System.out.println(textService.processText("Hello from Baeldung!"));
}
予想どおり、アプリケーションを実行すると、コンソールに同じテキストが出力されるはずです。
hello from baeldung!
*サービスインターフェイスをパブリックにし、サービスプロバイダーをプライベートにすることにより、単純なファクトリクラスを介してサービスモジュールとコンシューマモジュールを効果的に分離できました。*
もちろん、特効薬は特効薬ではありません。 いつものように、まず最初にユースケースを分析して適合させる必要があります。

6. サービスおよびコンシューマモジュール

JPMSは、_provides…with_および_uses_ディレクティブを介して、すぐに使用可能なサービスおよびコンシューマーモジュールのサポートを提供します。
したがって、モジュールを分離するためにこの機能を使用できます。*追加のファクトリクラスを作成する必要はありません*。
サービスモジュールとコンシューマモジュールを連携させるには、次のことを行う必要があります。
  1. サービスインターフェイスをモジュールに配置し、インターフェイスをエクスポートします

  2. サービスプロバイダーを別のモジュールに配置する-プロバイダーは
    輸出された

  3. 提供したいプロバイダーのモジュール記述子で指定します
    _provides…with_ディレクティブを使用した_TextService_実装

  4. _Application_クラスを独自のモジュール(コンシューマモジュール)に配置します

  5. コンシューマモジュールのモジュール記述子で、モジュールが
    _uses_ディレクティブを持つコンシューマモジュール

  6. Service Loader APIを使用して、
    サービスプロバイダーを検索するコンシューマモジュール

    このアプローチは、サービスおよびコンシューマモジュールがテーブルにもたらすすべての機能を活用するため、非常に強力です。 しかし、それもややトリッキーです。
    一方では、コンシューマモジュールがサービスプロバイダーではなく、サービスインターフェイスのみに依存するようにします。 一方、*サービスプロバイダーをまったく定義することさえできず、アプリケーションは引き続きコンパイルされます*。

* 6.1。 親モジュール*

このパターンを実装するには、親POMと既存のモジュールもリファクタリングする必要があります。
サービスインターフェース、サービスプロバイダー、およびコンシューマは異なるモジュールに存在するため、最初に親POMの_ <modules> _セクションを変更して、この新しい構造を反映させる必要があります。
<modules>
    <module>servicemodule</module>
    <module>providermodule</module>
    <module>consumermodule</module>
</modules>

* 6.2。 サービスモジュール*

_TextService_インターフェイスは_com.baeldung.servicemodule._に戻ります
そして、それに応じてモジュール記述子を変更します。
module com.baeldung.servicemodule {
    exports com.baeldung.servicemodule;
}

* 6.3。 プロバイダーモジュール*

前述のとおり、プロバイダーモジュールは実装用であるため、代わりにここに_LowerCaseTextService_とU__ppercaseTextService__を配置しましょう。 それらを呼び出すパッケージに入れます_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。 消費者モジュール*

それでは、コンシューマモジュールをリファクタリングしましょう。 最初に、_Application_を_com.baeldung.consumermodule_パッケージに戻します。
次に、_Application_クラスの_main()_メソッドをリファクタリングして、https://docs.oracle.com/javase/9​​/docs/api/java/util/ServiceLoader.html [_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!
ご覧のとおり、このパターンの実装は、ファクトリクラスを使用するパターンよりも若干複雑です。 それでも、追加の労力は、より柔軟で疎結合の設計で高く評価されます。
*コンシューマーモジュールは抽象化に依存しており、実行時にさまざまなサービスプロバイダーを簡単にドロップできます*。

7. 結論

このチュートリアルでは、Javaモジュールを分離するための2つのパターンを実装する方法を学びました。
*どちらのアプローチでも、コンシューマモジュールは抽象化に依存します。これは、ソフトウェアコンポーネントの設計で常に望ましい機能です。*
もちろん、それぞれに長所と短所があります。 最初のものでは、素晴らしい分離が得られますが、追加のファクトリクラスを作成する必要があります。
2つ目のモジュールでは、モジュールを分離するには、追加の抽象化モジュールを作成し、Service Loader APIで新しいhttps://en.wikipedia.org/wiki/Indirection [間接化のレベル]を追加する必要があります。
いつものように、このチュートリアルに示されているすべての例はGitHubで利用できます。 https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-jpms/decouple-pattern1[https:/]の両方のサンプルコードを必ず確認してください。 /github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-jpms/decouple-pattern2 [プロバイダーモジュール]パターン。