java-dependency-inversion-principle
Javaの依存関係反転の原理
-
link:/category/architecture/ [アーキテクチャ]
-
link:/category/programming/ [プログラミング]
1. 概要
依存性反転の原理(DIP)は、一般的にlink:/solid-principles[SOLID]として知られるオブジェクト指向プログラミングの原理のコレクションの一部を形成します。
基本的に、DIPはシンプルでありながら強力なプログラミングパラダイムであり、これを使用して、適切に構造化され、高度に分離され、再利用可能なソフトウェアコンポーネントを実装できます。
このチュートリアルでは、https://www.baeldung.com/java-9-modularity [JPMS]を使用して、DIPを実装するためのさまざまなアプローチ(Java 8で1つ、Java 11で1つ)について説明します( Javaプラットフォームモジュールシステム)。
2. 依存性注入と制御の反転はDIP実装ではありません
何よりもまず、基本を正しく理解するために基本的な区別を行いましょう。制御の反転(IoC)] *。 それでも、それらはすべて一緒にうまく機能します。
簡単に言えば、DIとは、ソフトウェアコンポーネントを作成して、APIを使用して依存関係や共同作業者を明示的に宣言することであり、それらを単独で取得することではありません。
DIがなければ、ソフトウェアコンポーネントは互いに密接に結合されます。 そのため、再利用、交換、モック、テストが困難であり、その結果、設計が厳格になります。
-
DIでは、コンポーネントの依存関係を提供し、オブジェクトグラフを配線する責任は、コンポーネントから基礎となる注入フレームワークに移されます。*その観点から、DIはIoCを達成するための単なる方法です。
一方、* IoCは、アプリケーションのフローの制御が逆になるパターンです*。 従来のプログラミング方法では、カスタムコードがアプリケーションのフローを制御します。 逆に、* IoCでは、制御は外部フレームワークまたはコンテナに転送されます*。
*フレームワークは拡張可能なコードベースであり、独自のコードをプラグインするためのフックポイントを定義します*。
次に、フレームワークは、1つ以上の特殊なサブクラスを介して、インターフェイスの実装を使用して、注釈を介してコードをコールバックします。 link:/spring-tutorial[Spring framework]は、この最後のアプローチの良い例です。
*3. DIP *の基礎
DIPの背後にある動機を理解するために、Robert Cによって与えられた正式な定義から始めましょう。 Martinの著書https://www.pearson.com/us/higher-education/program/Martin-Agile-Software-Development-Principles-Patterns-and-Practices/PGM272869.html[_Agile Software Development:Principles、Patterns 、およびPractices_]:
-
上位モジュールは下位モジュールに依存してはいけません。 両方とも
抽象化に依存します。 -
抽象化は詳細に依存してはいけません。 詳細は
抽象化です。したがって、コアでは、* DIPは、高レベルコンポーネントと低レベルコンポーネント間の相互作用を抽象化することにより、それらの間の古典的な依存関係を反転させることについて明確です。
従来のソフトウェア開発では、高レベルのコンポーネントは低レベルのコンポーネントに依存しています。 したがって、高レベルのコンポーネントを再利用することは困難です。
* 3.1。 設計の選択肢とDIP *
_StringReader_コンポーネントを使用して_String_値を取得し、_StringWriter_コンポーネントを使用して別の場所に書き込む単純な_StringProcessor_クラスを考えてみましょう。
public class StringProcessor {
private final StringReader stringReader;
private final StringWriter stringWriter;
public StringProcessor(StringReader stringReader, StringWriter stringWriter) {
this.stringReader = stringReader;
this.stringWriter = stringWriter;
}
public void printString() {
stringWriter.write(stringReader.getValue());
}
}
_StringProcessor_クラスの実装は基本的なものですが、ここでいくつかの設計上の選択を行うことができます。
各デザインの選択を個別の項目に分けて、それぞれがデザイン全体にどのように影響するかを明確に理解しましょう。
-
* StringReader_および_StringWriter(低レベルコンポーネント)は
同じパッケージに配置された具象クラス。* StringProcessor、高レベルコンポーネントは別のパッケージに配置されます。 _StringProcessor_は、_StringReader_および_StringWriter_に依存しています。 依存関係の反転はないため、_StringProcessor_は別のコンテキストで再利用できません。 -
* _StringReader_と_StringWriter_は同じ場所に配置されたインターフェイスです
パッケージと実装*。 _StringProcessor_は抽象化に依存するようになりましたが、低レベルのコンポーネントは依存しません。 依存関係の反転はまだ達成されていません。 -
* StringReader_と_StringWriter_は同じ場所に配置されたインターフェイスです
_StringProcessor *と一緒にパッケージ化します。 現在、StringProcessor_は抽象化の明示的な所有権を持っています。 _StringProcessor、StringReader、、および_StringWriter_はすべて抽象化に依存しています。 components . _StringProcessor_間の相互作用を抽象化することにより、上から下への依存関係の反転を実現しました。これは、異なるコンテキストで再利用可能です。 -
* StringReader_および_StringWriter_は、別々に配置されたインターフェイスです
_StringProcessor *からのパッケージ。 依存関係の反転を達成しました。また、_StringReader_と_StringWriter_の実装を置き換えるのも簡単です。 _StringProcessor_は、異なるコンテキストでも再利用できます。上記のすべてのシナリオのうち、項目3と4のみがDIPの有効な実装です。
* 3.2。 抽象化の所有権の定義*
アイテム3は直接DIP実装です。*高レベルコンポーネントと抽象化は同じパッケージに配置されます。*したがって、*高レベルコンポーネントは抽象化を所有します*。 この実装では、高レベルのコンポーネントは、低レベルのコンポーネントと対話する抽象プロトコルを定義する責任があります。
同様に、項目4はより分離されたDIP実装です。 このパターンの変形では、*高レベルコンポーネントも低レベルコンポーネントも抽象化の所有権を持ちません*。
抽象化は、低レベルのコンポーネントの切り替えを容易にする別のレイヤーに配置されます。 同時に、すべてのコンポーネントが互いに分離されているため、カプセル化が強化されます。
* 3.3。 適切な抽象化レベルの選択*
ほとんどの場合、高レベルのコンポーネントが使用する抽象化の選択はかなり簡単なはずですが、注目すべき1つの注意事項があります。抽象化のレベルです。
上記の例では、DIを使用して、_StringReader_型を_StringProcessor_クラスに挿入しました。 これは、_StringReader_の抽象化レベルが_StringProcessor_ *のドメインに近い限り、効果的です。
対照的に、_StringReader_が、たとえば、_String_を読み取るlink:/java-how-to-create-a-file[_File_]オブジェクトである場合、DIPの本質的な利点が失われます。ファイルからの値。 その場合、_StringReader_の抽象化レベルは、_StringProcessor_のドメインのレベルよりもはるかに低くなります。
簡単に言えば、*高レベルのコンポーネントが低レベルのコンポーネントと相互運用するために使用する抽象化のレベルは、常に前者のドメインに近いはずです*。
4. Java 8の実装
すでにDIPの主要な概念を詳細に検討したので、Java 8でのパターンの実用的な実装をいくつか検討します。
* 4.1。 直接DIP実装*
パーシステンスレイヤーから一部の顧客を取得し、追加の方法で処理するデモアプリケーションを作成しましょう。
レイヤーの基礎となるストレージは通常データベースですが、コードをシンプルにするために、ここではプレーン_Map_を使用します。
*高レベルのコンポーネントを定義する*ことから始めましょう:
public class CustomerService {
private final CustomerDao customerDao;
// standard constructor / getter
public Optional<Customer> findById(int id) {
return customerDao.findById(id);
}
public List<Customer> findAll() {
return customerDao.findAll();
}
}
ご覧のとおり、_CustomerService_クラスは_findById()_および_findAll()_メソッドを実装します。これらのメソッドは、単純なlink:/java-dao-pattern[DAO]実装を使用して、永続レイヤーから顧客を取得します。 もちろん、クラス内により多くの機能をカプセル化することもできますが、簡単にするためにこのように保ちましょう。
この場合、* CustomerDao_型は、_CustomerService_が低レベルコンポーネントを使用するために使用する抽象化*です。
これは直接的なDIP実装なので、_CustomerService_の同じパッケージ内のインターフェースとして抽象化を定義しましょう。
public interface CustomerDao {
Optional<Customer> findById(int id);
List<Customer> findAll();
}
高レベルコンポーネントの同じパッケージに抽象化を配置することにより、抽象化の所有を担当するコンポーネントを作成しています。 この実装の詳細は、*高レベルコンポーネントと低レベルコンポーネントの依存関係を実際に反転させるものです*。
さらに、* CustomerDao_の抽象化レベルは、* _ * CustomerService *、_のレベルに近いため、DIPの適切な実装にも必要です。
それでは、別のパッケージに低レベルのコンポーネントを作成しましょう。 この場合、それは基本的な_CustomerDao_実装です。
public class SimpleCustomerDao implements CustomerDao {
// standard constructor / getter
@Override
public Optional<Customer> findById(int id) {
return Optional.ofNullable(customers.get(id));
}
@Override
public List<Customer> findAll() {
return new ArrayList<>(customers.values());
}
}
最後に、_CustomerService_クラスの機能を確認する単体テストを作成しましょう。
@Before
public void setUpCustomerServiceInstance() {
var customers = new HashMap<Integer, Customer>();
customers.put(1, new Customer("John"));
customers.put(2, new Customer("Susan"));
customerService = new CustomerService(new SimpleCustomerDao(customers));
}
@Test
public void givenCustomerServiceInstance_whenCalledFindById_thenCorrect() {
assertThat(customerService.findById(1)).isInstanceOf(Optional.class);
}
@Test
public void givenCustomerServiceInstance_whenCalledFindAll_thenCorrect() {
assertThat(customerService.findAll()).isInstanceOf(List.class);
}
@Test
public void givenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect() {
var customers = new HashMap<Integer, Customer>();
customers.put(1, null);
customerService = new CustomerService(new SimpleCustomerDao(customers));
Customer customer = customerService.findById(1).orElseGet(() -> new Customer("Non-existing customer"));
assertThat(customer.getName()).isEqualTo("Non-existing customer");
}
単体テストは、_CustomerService_ APIを実行します。 また、抽象化を高レベルコンポーネントに手動で挿入する方法も示します。 ほとんどの場合、これを実現するために何らかのDIコンテナーまたはフレームワークを使用します。
さらに、次の図は、デモアプリケーションの構造を、高レベルから低レベルのパッケージの観点まで示しています。
link:/uploads/direct-dip-100x96.png%20100w []
* 4.2。 代替DIP実装*
前に説明したように、別のDIP実装を使用することもできます。この場合、高レベルのコンポーネント、抽象化、および低レベルのコンポーネントを異なるパッケージに配置します。
明らかな理由により、このバリアントは柔軟性が高く、コンポーネントのカプセル化が向上し、低レベルのコンポーネントの交換が容易になります。
もちろん、このパターンのバリアントを実装すると、_CustomerService _、_ MapCustomerDao、_、および_CustomerDao_を個別のパッケージに配置することになります。
したがって、この実装で各コンポーネントがどのようにレイアウトされるかを示すには図で十分です。
link:/uploads/alternative-dip-100x139.png%20100w []
5. Java 11モジュラー実装
デモアプリケーションをモジュール化することは非常に簡単です。
これは、JPMSがDIPによる強力なカプセル化、抽象化、コンポーネントの再利用など、プログラミングのベストプラクティスをどのように実施するかを示すための非常に優れた方法です。
サンプルコンポーネントを最初から再実装する必要はありません。 したがって、*サンプルアプリケーションのモジュール化は、各コンポーネントファイルを、対応するモジュール記述子とともに個別のモジュールに配置するだけです。
モジュールプロジェクトの構造は次のようになります。
project base directory (could be anything, like dipmodular)
|- com.baeldung.dip.services
module-info.java
|- com
|- baeldung
|- dip
|- services
CustomerService.java
|- com.baeldung.dip.daos
module-info.java
|- com
|- baeldung
|- dip
|- daos
CustomerDao.java
|- com.baeldung.dip.daoimplementations
module-info.java
|- com
|- baeldung
|- dip
|- daoimplementations
SimpleCustomerDao.java
|- com.baeldung.dip.entities
module-info.java
|- com
|- baeldung
|- dip
|- entities
Customer.java
|- com.baeldung.dip.mainapp
module-info.java
|- com
|- baeldung
|- dip
|- mainapp
MainApplication.java
5.1. 高レベルコンポーネントモジュール
_CustomerService_クラスを独自のモジュールに配置することから始めましょう。
ルートディレクトリ_com.baeldung.dip.services、_にこのモジュールを作成し、モジュール記述子_module-info.java_を追加します。
module com.baeldung.dip.services {
requires com.baeldung.dip.entities;
requires com.baeldung.dip.daos;
uses com.baeldung.dip.daos.CustomerDao;
exports com.baeldung.dip.services;
}
明らかな理由から、JPMSの仕組みについては詳しく説明しません。 それでも、_requires_ディレクティブを見るだけでモジュールの依存関係を確認できます。
ここで注目に値する最も重要な詳細は、_uses_ディレクティブです。 モジュールは、_CustomerDao_インターフェースの実装を消費する*クライアントモジュールであると述べています。
もちろん、このモジュールに高レベルのコンポーネント_CustomerService_クラスを配置する必要があります。 そのため、ルートディレクトリ_com.baeldung.dip.services_内で、次のパッケージのようなディレクトリ構造を作成しましょう:_com / baeldung / dip / services._
最後に、_CustomerService.java_ファイルをそのディレクトリに配置しましょう。
* 5.2。 抽象化モジュール*
同様に、_CustomerDao_インターフェイスを独自のモジュールに配置する必要があります。 したがって、ルートディレクトリ_com.baeldung.dip.daos_にモジュールを作成し、モジュール記述子を追加しましょう。
module com.baeldung.dip.daos {
requires com.baeldung.dip.entities;
exports com.baeldung.dip.daos;
}
ここで、_com.baeldung.dip.daos_ディレクトリに移動して、次のディレクトリ構造を作成します:_com / baeldung / dip / daos_。 _CustomerDao.java_ファイルをそのディレクトリに配置しましょう。
* 5.3。 低レベルコンポーネントモジュール*
論理的には、低レベルのコンポーネント_SimpleCustomerDao_も別のモジュールに配置する必要があります。 予想どおり、このプロセスは、他のモジュールで行ったことと非常によく似ています。
ルートディレクトリ_com.baeldung.dip.daoimplementations_に新しいモジュールを作成し、モジュール記述子を含めましょう。
module com.baeldung.dip.daoimplementations {
requires com.baeldung.dip.entities;
requires com.baeldung.dip.daos;
provides com.baeldung.dip.daos.CustomerDao with com.baeldung.dip.daoimplementations.SimpleCustomerDao;
exports com.baeldung.dip.daoimplementations;
}
JPMSコンテキストでは、これは_provides_および_with_ディレクティブを宣言するため、*これはサービスプロバイダーモジュール*です。
この場合、モジュールは、_SimpleCustomerDao_実装を介して、1つ以上のコンシューマモジュールで_CustomerDao_サービスを利用できるようにします。
コンシューマモジュール_com.baeldung.dip.services_は、_uses_ディレクティブを介してこのサービスを消費することに注意してください。
これは、*コンシューマー、サービスプロバイダー、および抽象化を異なるモジュールで定義するだけで、JPMSで直接DIPを実装することがいかに簡単かを明確に示しています*。
同様に、_SimpleCustomerDao.java_ファイルをこの新しいモジュールに配置する必要があります。 _com.baeldung.dip.daoimplementations_ディレクトリに移動して、_com / baeldung / dip / daoimplementations_という名前の新しいパッケージのようなディレクトリ構造を作成しましょう。
最後に、_SimpleCustomerDao.java_ファイルをディレクトリに配置します。
* 5.4。 エンティティモジュール*
さらに、_Customer.java_クラスを配置できる別のモジュールを作成する必要があります。 前にやったように、ルートディレクトリ_com.baeldung.dip.entities_を作成し、モジュール記述子を含めましょう。
module com.baeldung.dip.entities {
exports com.baeldung.dip.entities;
}
パッケージのルートディレクトリで、_com / baeldung / dip / entities_ディレクトリを作成し、次の_Customer.java_ファイルを追加します。
public class Customer {
private final String name;
// standard constructor / getter / toString
}
* 5.5。 メインアプリケーションモジュール*
次に、デモアプリケーションのエントリポイントを定義できる追加モジュールを作成する必要があります。 したがって、別のルートディレクトリ_com.baeldung.dip.mainapp_を作成し、その中にモジュール記述子を配置しましょう。
module com.baeldung.dip.mainapp {
requires com.baeldung.dip.entities;
requires com.baeldung.dip.daos;
requires com.baeldung.dip.daoimplementations;
requires com.baeldung.dip.services;
exports com.baeldung.dip.mainapp;
}
次に、モジュールのルートディレクトリに移動して、次のディレクトリ構造を作成します。_com / baeldung / dip / mainapp._そのディレクトリに、_Main()_メソッドを実装する_MainApplication.java_ファイルを追加します。
public class MainApplication {
public static void main(String args[]) {
var customers = new HashMap<Integer, Customer>();
customers.put(1, new Customer("John"));
customers.put(2, new Customer("Susan"));
CustomerService customerService = new CustomerService(new SimpleCustomerDao(customers));
customerService.findAll().forEach(System.out::println);
}
}
最後に、IDE内またはコマンドコンソールからデモアプリケーションをコンパイルして実行します。
予想どおり、アプリケーションの起動時に、_Customer_オブジェクトのリストがコンソールに出力されます:
Customer{name=John}
Customer{name=Susan}
さらに、次の図は、アプリケーションの各モジュールの依存関係を示しています。
link:/uploads/module-dependency-1-100x126.png%20100w []
6. 結論
このチュートリアルでは、* DIPの主要な概念を深く掘り下げ、JPMSを使用するJava 8とJava 11 *のパターンの異なる実装も示しました。
https://github.com/eugenp/tutorials/tree/master/patterns/dip[Java 8 DIP実装]およびhttps://github.com/eugenp/tutorials/tree/master/patterns/のすべての例dipmodular [Java 11実装]はGitHubで利用できます。