Javaの依存性逆転の原則
1.概要
依存性逆転の原則(DIP)は、SOLIDとして一般に知られているオブジェクト指向プログラミングの原則のコレクションの一部を形成します。
必要最低限の点で、DIPはシンプルでありながら強力なプログラミングパラダイムであり、を使用して、適切に構造化され、高度に分離され、再利用可能なソフトウェアコンポーネントを実装できます。
このチュートリアルでは、DIPを実装するためのさまざまなアプローチを検討します。1つはJava8で、もう1つは JPMS (Javaプラットフォームモジュールを使用)Java11です。システム)。
2. 依存性の注入と制御の反転はDIPの実装ではありません
まず第一に、基本を正しく理解するために基本的な区別をしてみましょう。 DIPは依存性注入(DI)でも制御の反転(IoC)でもありません。 それでも、それらはすべて一緒にうまく機能します。
簡単に言うと、DIとは、ソフトウェアコンポーネントを作成して、APIを介して依存関係や共同作業者を明示的に宣言することであり、それらを単独で取得することではありません。
DIがない場合、ソフトウェアコンポーネントは互いに緊密に結合されます。 したがって、再利用、交換、モック、テストが難しく、結果として堅固な設計になります。
DIを使用すると、コンポーネントの依存関係と配線オブジェクトグラフを提供する責任が、コンポーネントから基盤となるインジェクションフレームワークに移されます。その観点から、DIはIoCを実現するための単なる方法です。
一方、 IoCは、アプリケーションのフローの制御が逆になるパターンです。 従来のプログラミング方法では、カスタムコードでアプリケーションのフローを制御できます。 逆に、 IoCを使用すると、制御は外部フレームワークまたはコンテナーに転送されます。
フレームワークは拡張可能なコードベースであり、独自のコードをプラグインするためのフックポイントを定義します。
次に、フレームワークは、インターフェイスの実装を使用して、1つ以上の特殊なサブクラスを介して、およびアノテーションを介してコードをコールバックします。 Springフレームワークは、この最後のアプローチの良い例です。
3. DIPの基礎
DIPの背後にある動機を理解するために、RobertCによって与えられた正式な定義から始めましょう。 マーティンの著書、アジャイルソフトウェア開発:原則、パターン、および実践:
- 高レベルのモジュールは、低レベルのモジュールに依存するべきではありません。 どちらも抽象化に依存する必要があります。
- 抽象化は詳細に依存するべきではありません。 詳細は抽象化に依存する必要があります。
したがって、コアである 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はすべて抽象化に依存しています。 コンポーネント間の相互作用を抽象化することにより、依存関係を上から下に反転させることができました。 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値を読み取るFileオブジェクトである場合、DIPの本質的な利点が失われます。 その場合、 StringReader の抽象化のレベルは、StringProcessorのドメインのレベルよりもはるかに低くなります。
簡単に言うと、高レベルのコンポーネントが低レベルのコンポーネントと相互運用するために使用する抽象化のレベルは、常に前者のドメインに近い必要があります。
4. Java8の実装
DIPの主要な概念についてはすでに詳しく説明したので、次にJava8のパターンのいくつかの実用的な実装について説明します。
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()メソッドを実装し、単純なを使用して永続層から顧客をフェッチします。 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");
}
単体テストは、 CustomerServiceAPIを実行します。 また、抽象化を高レベルのコンポーネントに手動で挿入する方法も示しています。 ほとんどの場合、これを実現するために、ある種のDIコンテナまたはフレームワークを使用します。
さらに、次の図は、高レベルから低レベルのパッケージの観点から、デモアプリケーションの構造を示しています。
4.2. 代替のDIP実装
前に説明したように、代替のDIP実装を使用することができます。この実装では、高レベルのコンポーネント、抽象化、および低レベルのコンポーネントを異なるパッケージに配置します。
明らかな理由から、このバリアントはより柔軟性があり、コンポーネントのカプセル化が向上し、低レベルのコンポーネントの交換が容易になります。
もちろん、このパターンのバリアントを実装することは、 CustomerService 、 MapCustomerDao、、およびCustomerDaoを別々のパッケージに配置することです。
したがって、この実装で各コンポーネントがどのように配置されるかを示すには、図で十分です。
5. Java11モジュラー実装
デモアプリケーションをモジュラーアプリケーションにリファクタリングするのはかなり簡単です。
これは、強力なカプセル化、抽象化、DIPによるコンポーネントの再利用など、JPMSがプログラミングのベストプラクティスをどのように実施するかを示すための非常に優れた方法です。
サンプルコンポーネントを最初から再実装する必要はありません。 したがって、サンプルアプリケーションのモジュール化は、各コンポーネントファイルを対応するモジュール記述子とともに個別のモジュールに配置するだけです。
モジュラープロジェクト構造は次のようになります。
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がどのように機能するかについては詳しく説明しません。 それでも、requireディレクティブを見るだけでモジュールの依存関係を確認できることは明らかです。
ここで注目に値する最も関連性のある詳細は、usedディレクティブです。 モジュールは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のコンテキストでは、これはサービスプロバイダーモジュールです。これは、がおよびにディレクティブを提供することを宣言しているためです。
この場合、モジュールは、 SimpleCustomerDao 実装を通じて、CustomerDaoサービスを1つ以上のコンシューマーモジュールで利用できるようにします。
コンシューマーモジュールcom.baeldung.dip.servicesは、usedディレクティブを介してこのサービスを使用することに注意してください。
これは、さまざまなモジュールでコンシューマー、サービスプロバイダー、および抽象化を定義するだけで、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 / entity を作成し、次の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。そのディレクトリに、MainApplication。javaファイルを追加します。 、 main()メソッドを実装するだけです。
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}
さらに、次の図は、アプリケーションの各モジュールの依存関係を示しています。
6. 結論
このチュートリアルでは、 DIPの主要な概念を深く掘り下げ、Java8とJava11 のパターンのさまざまな実装を示し、後者はJPMSを使用しました。
Java8DIP実装およびJava11実装のすべての例は、GitHubで入手できます。