Javaモジュールを分離するための設計戦略
1. 概要
Javaプラットフォームモジュールシステム(JPMS)は、より強力なカプセル化、より高い信頼性、および関心の分離を提供します。
しかし、これらすべての便利な機能には代償が伴います。 モジュール化されたアプリケーションは、適切に機能するために他のモジュールに依存するモジュールのネットワーク上に構築されているため、多くの場合、モジュールは互いに緊密に結合されています。
これにより、モジュール性と緩い結合は、同じシステムでは共存できない機能であると考える可能性があります。 しかし実際には、彼らはそうすることができます!
このチュートリアルでは、Javaモジュールを簡単にデカップリングするために使用できる2つのよく知られたデザインパターンを詳しく見ていきます。
2. 親モジュール
Javaモジュールのデカップリングに使用するデザインパターンを紹介するために、デモマルチモジュールMavenプロジェクトをビルドします。
コードを単純にするために、プロジェクトには最初に2つのMavenモジュールが含まれ、各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を使用しているため、システムには少なくともMaven 3.5.0が必要です。これは、MavenがJava9以降のバージョンをサポートしているためです。
最後に、少なくともバージョン3.8.0のMavenコンパイラプラグインも必要です。 したがって、最新であることを確認するために、 MavenCentralでMavenコンパイラプラグインの最新バージョンを確認します。
3. サービスモジュール
デモの目的で、 servicemodule モジュールを実装するための手っ取り早いアプローチを使用して、この設計で発生する欠陥を明確に特定できるようにします。
サービスインターフェイスとサービスプロバイダーをパブリックにして、それらを同じパッケージに入れ、すべてをエクスポートしてみましょう。 これはかなり良い設計上の選択のようですが、すぐにわかるように、プロジェクトのモジュール間の結合レベルが大幅に向上します。
プロジェクトのルートディレクトリの下に、 servicemodule / src / main /javaディレクトリを作成します。 次に、パッケージ com.baeldung.servicemodule を定義し、その中に次のTextServiceインターフェイスを配置する必要があります。
public interface TextService {
String processText(String text);
}
TextService インターフェースは本当にシンプルなので、サービスプロバイダーを定義しましょう。
同じパッケージに、小文字の実装を追加しましょう。
public class LowercaseTextService implements TextService {
@Override
public String processText(String text) {
return text.toLowerCase();
}
}
次に、大文字の実装を追加しましょう。
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. サービスプロバイダーファクトリー
サービスインターフェイスのみをエクスポートすることで、モジュール間の結合を簡単に削除できます。 対照的に、サービスプロバイダーはエクスポートされないため、コンシューマーモジュールからは隠されたままになります。 コンシューマモジュールは、サービスインターフェイスタイプのみを認識します。
これを実現するには、次のことを行う必要があります。
- サービスインターフェイスを別のパッケージに配置し、外部にエクスポートします
- エクスポートされない別のパッケージにサービスプロバイダーを配置します
- エクスポートされるファクトリクラスを作成します。 コンシューマーモジュールは、ファクトリクラスを使用してサービスプロバイダーを検索します
上記の手順は、パブリックサービスインターフェイス、プライベートサービスプロバイダー、およびパブリックサービスプロバイダーファクトリのデザインパターンの形で概念化できます。
5.1. 公共サービスインターフェース
このパターンがどのように機能するかを明確に確認するために、サービスインターフェイスとサービスプロバイダーを異なるパッケージに配置してみましょう。 インターフェイスはエクスポートされますが、プロバイダーの実装はエクスポートされません。
それでは、TextServiceをcom.baeldung.servicemodule.externalという新しいパッケージに移動しましょう。
5.2. プライベートサービスプロバイダー
次に、同様にLowercaseTextServiceとUppercaseTextServiceをcom.baeldung.servicemodule.internal。に移動しましょう。
5.3. 公共サービスプロバイダーファクトリー
サービスプロバイダークラスはプライベートになり、他のモジュールからアクセスできないため、パブリックファクトリクラスを使用して、コンシューマモジュールがサービスプロバイダーのインスタンスを取得するために使用できる単純なメカニズムを提供します。
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値に基づいて作成されます。
次に、module-info。javaファイルを置き換えて、外部パッケージのみをエクスポートします。
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は、がおよび uses ディレクティブを提供することにより、すぐに使用できるサービスおよびコンシューマーモジュールのサポートを提供します。
したがって、追加のファクトリクラスを作成しなくても、この機能をモジュールのデカップリングに使用できます。
サービスモジュールとコンシューマモジュールを連携させるには、次のことを行う必要があります。
- インターフェイスをエクスポートするモジュールにサービスインターフェイスを配置します
- サービスプロバイダーを別のモジュールに配置します–プロバイダーはエクスポートされます
- プロバイダーのモジュール記述子で、TextService実装にprovides…withディレクティブを提供することを指定します
- Application クラスを独自のモジュール(コンシューマーモジュール)に配置します
- モジュールがusesディレクティブを持つコンシューマーモジュールであることをコンシューマーモジュールのモジュール記述子で指定します
- コンシューマーモジュールのServiceLoader API を使用して、サービスプロバイダーを検索します
このアプローチは、サービスモジュールとコンシューマーモジュールが提供するすべての機能を活用するため、非常に強力です。 しかし、それもややトリッキーです。
一方では、コンシューマーモジュールをサービスプロバイダーではなく、サービスインターフェイスのみに依存させるようにします。 一方、サービスプロバイダーを定義することすらできず、アプリケーションはコンパイルされます。
6.1. 親モジュール
このパターンを実装するには、親POMと既存のモジュールもリファクタリングする必要があります。
サービスインターフェイス、サービスプロバイダー、およびコンシューマーは異なるモジュールに存在するため、最初に親POMを変更する必要があります。
<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と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. コンシューマーモジュール
それでは、コンシューマーモジュールをリファクタリングしましょう。 まず、アプリケーションを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!
ご覧のとおり、このパターンの実装は、ファクトリクラスを使用するパターンよりも少し複雑です。 それでも、追加の努力は、より柔軟で緩く結合された設計によって非常に報われます。
コンシューマーモジュールは抽象化に依存しており、実行時にさまざまなサービスプロバイダーに簡単にドロップすることもできます。
7. 結論
このチュートリアルでは、Javaモジュールを分離するための2つのパターンを実装する方法を学びました。
どちらのアプローチでも、コンシューマーモジュールは抽象化に依存します。これは、ソフトウェアコンポーネントの設計において常に望ましい機能です。
もちろん、それぞれに長所と短所があります。 最初のものでは、優れたデカップリングが得られますが、追加のファクトリクラスを作成する必要があります。
2つ目では、モジュールを分離するには、追加の抽象化モジュールを作成し、ServiceLoaderAPIを使用して新しいレベルの間接参照を追加する必要があります。
いつものように、このチュートリアルに示されているすべての例はGitHubで入手できます。 ServiceFactoryパターンとProviderModuleパターンの両方のサンプルコードを確認してください。