依存関係逆転の原則とは何ですか? なぜ重要なのですか?
16 に答える
このドキュメントをチェックしてください: The Dependency Inversion Principle。
それは基本的に言います:
- 高レベル モジュールは低レベル モジュールに依存すべきではありません。どちらも抽象化に依存する必要があります。
- 抽象化は決して詳細に依存すべきではありません。詳細は抽象化に依存する必要があります。
なぜ重要なのかというと、簡単に言うと、変更にはリスクが伴います。実装ではなく概念に依存することで、コール サイトでの変更の必要性を減らすことができます。
事実上、DIP は異なるコード間の結合を減らします。たとえばロギング機能を実装する方法はたくさんありますが、それを使用する方法は時間的に比較的安定している必要があります。ロギングの概念を表すインターフェースを抽出できれば、このインターフェースは実装よりもはるかに安定しているはずです。また、ロギング メカニズムを維持または拡張している間に行った変更による呼び出しサイトへの影響ははるかに少なくなるはずです。
また、実装をインターフェイスに依存させることで、特定の環境により適した実装を実行時に選択できるようになります。場合によっては、これも面白いかもしれません。
ソフトウェア アプリケーションを設計する場合、低レベル クラスを基本操作と主要操作 (ディスク アクセス、ネットワーク プロトコルなど) を実装するクラスと見なし、高レベル クラスを複雑なロジック (ビジネス フローなど) をカプセル化するクラスと見なすことができます。
最後のものは、低レベルのクラスに依存しています。このような構造を実装する自然な方法は、低レベルのクラスを作成し、それらを取得したら、複雑な高レベルのクラスを作成することです。高レベルのクラスは他のクラスで定義されているため、これは論理的な方法のようです。しかし、これは柔軟な設計ではありません。低レベルのクラスを置き換える必要がある場合はどうなりますか?
依存性逆転の原則は、次のように述べています。
- 高レベル モジュールは低レベル モジュールに依存すべきではありません。どちらも抽象化に依存する必要があります。
- 抽象化は詳細に依存すべきではありません。詳細は抽象化に依存する必要があります。
この原則は、ソフトウェアの高レベル モジュールが低レベル モジュールに依存する必要があるという従来の概念を「逆転」させようとします。ここで、高レベルのモジュールは、低レベルのモジュールによって実装される抽象化 (たとえば、インターフェースのメソッドの決定) を所有します。したがって、下位レベルのモジュールを上位レベルのモジュールに依存させます。
依存関係の反転を適切に適用すると、アプリケーションのアーキテクチャ全体のレベルで柔軟性と安定性が得られます。これにより、アプリケーションをより安全かつ安定して進化させることができます。
従来のレイヤード アーキテクチャ
従来、レイヤード アーキテクチャの UI はビジネス レイヤーに依存し、これはデータ アクセス レイヤーに依存していました。
レイヤー、パッケージ、またはライブラリを理解する必要があります。コードがどのようになるか見てみましょう。
データアクセスレイヤー用のライブラリまたはパッケージがあります。
// DataAccessLayer.dll
public class ProductDAO {
}
また、データ アクセス層に依存する別のライブラリまたはパッケージ層のビジネス ロジック。
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private ProductDAO productDAO;
}
依存関係を逆転させたレイヤード アーキテクチャ
依存関係の逆転は、次のことを示しています。
高レベル モジュールは低レベル モジュールに依存すべきではありません。どちらも抽象化に依存する必要があります。
抽象化は詳細に依存すべきではありません。詳細は抽象化に依存する必要があります。
上位モジュールと下位モジュールとは何ですか? ライブラリやパッケージなどのモジュールを考えると、高レベルのモジュールは、伝統的に依存関係があり、それらが依存する低レベルのものです。
つまり、モジュールの高レベルはアクションが呼び出される場所であり、低レベルはアクションが実行される場所です。
この原則から導き出される合理的な結論は、具象間には依存があってはならないということですが、抽象化には依存がなければならないということです。しかし、私たちがとるアプローチによれば、投資に依存する依存関係を誤って適用する可能性がありますが、抽象化です。
コードを次のように調整すると想像してください。
抽象化を定義するデータアクセスレイヤー用のライブラリまたはパッケージがあります。
// DataAccessLayer.dll
public interface IProductDAO
public class ProductDAO : IProductDAO{
}
また、データ アクセス層に依存する別のライブラリまたはパッケージ層のビジネス ロジック。
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private IProductDAO productDAO;
}
ビジネスとデータ アクセスの間の依存関係の抽象化に依存していますが、同じままです。
依存関係の反転を取得するには、下位レベルのモジュールではなく、この高レベルのロジックまたはドメインがあるモジュールまたはパッケージで永続化インターフェイスを定義する必要があります。
最初に、ドメイン層とは何かを定義し、その通信の抽象化を永続化と定義します。
// Domain.dll
public interface IProductRepository;
using DataAccessLayer;
public class ProductBO {
private IProductRepository productRepository;
}
永続化レイヤーがドメインに依存した後、依存関係が定義されている場合は今すぐ反転します。
// Persistence.dll
public class ProductDAO : IProductRepository{
}
(ソース: xurxodev.com )
原理を深める
コンセプトをうまく吸収し、目的やメリットを深めていくことが大切です。機械的にとどまり、典型的なケースリポジトリを学習すると、依存の原則をどこに適用できるかを特定できなくなります。
しかし、なぜ依存関係を逆転させるのでしょうか? 具体的な例を超えた主な目的は何ですか?
これにより、通常、安定性の低いものに依存しない最も安定したものをより頻繁に変更できます。
永続性と通信するように設計されたドメイン ロジックまたはアクションよりも、同じデータベースにアクセスするデータベースまたはテクノロジのいずれかで、永続性タイプを変更する方が簡単です。このため、この変更が発生した場合に永続性を変更する方が簡単であるため、依存関係が逆転します。この方法では、ドメインを変更する必要はありません。ドメイン層は最も安定しているため、何にも依存すべきではありません。
しかし、このリポジトリの例だけではありません。この原則が適用される多くのシナリオがあり、この原則に基づくアーキテクチャがあります。
アーキテクチャ
依存関係の逆転がその定義の鍵となるアーキテクチャがあります。すべてのドメインで最も重要であり、ドメインと残りのパッケージまたはライブラリとの間の通信プロトコルが定義されていることを示すのは抽象化です。
クリーンなアーキテクチャ
クリーンなアーキテクチャでは、ドメインが中心に位置し、依存関係を示す矢印の方向に目を向けると、最も重要で安定したレイヤーが明確になります。外側のレイヤーは不安定なツールと見なされるため、それらに依存することは避けてください。
(ソース: 8thlight.com )
六角形のアーキテクチャ
六角形アーキテクチャでも同じことが起こります。ドメインも中央部分に配置され、ポートはドミノから外部への通信の抽象化です。ここでも、ドメインが最も安定しており、従来の依存関係が逆転していることは明らかです。
(ソース: pragprog.com )
私にとって、依存関係逆転の原則は、公式記事で説明されているように、本質的に再利用性が低いモジュールの再利用性を高めるための見当違いの試みであり、C++ 言語の問題を回避する方法でもあります。
C++ の問題は、通常、ヘッダー ファイルにプライベート フィールドとメソッドの宣言が含まれていることです。したがって、高レベル C++ モジュールに低レベル モジュールのヘッダー ファイルが含まれている場合、それはそのモジュールの実際の実装の詳細に依存します。そして、それは明らかに良いことではありません。しかし、これは、今日一般的に使用されているより近代的な言語では問題になりません。
高レベル モジュールは、低レベル モジュールよりも本質的に再利用性が低くなります。たとえば、UI 画面を実装するコンポーネントは最上位であり、アプリケーションに非常に (完全に?) 固有です。このようなコンポーネントを別のアプリケーションで再利用しようとすることは非生産的であり、オーバーエンジニアリングにつながるだけです。
したがって、コンポーネント A の同じレベルでコンポーネント B に依存する (コンポーネント A に依存しない) 別の抽象化の作成は、コンポーネント A が異なるアプリケーションまたはコンテキストでの再利用に本当に役立つ場合にのみ実行できます。そうでない場合、DIP を適用するのは悪い設計です。
基本的にそれは言います:
クラスは、特定の詳細 (実装) ではなく、抽象化 (インターフェイス、抽象クラスなど) に依存する必要があります。
依存性逆転の原則をより明確に説明するには、次のようにします。
複雑なビジネス ロジックをカプセル化するモジュールは、ビジネス ロジックをカプセル化する他のモジュールに直接依存するべきではありません。代わりに、単純なデータへのインターフェイスのみに依存する必要があります。
Logic
つまり、通常のようにクラスを実装する代わりに、次のようにします。
class Dependency { ... }
class Logic {
private Dependency dep;
int doSomething() {
// Business logic using dep here
}
}
次のようにする必要があります。
class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
private Dependency dep;
...
}
class Logic {
int doSomething(Data data) {
// compute something with data
}
}
Data
とではなくDataFromDependency
、と同じモジュールに存在する必要があります。Logic
Dependency
なぜこれを行うのですか?
- 2 つのビジネス ロジック モジュールが分離されました。変更する場合
Dependency
、変更する必要はありませんLogic
。 - 何をするのかを理解する
Logic
のは、はるかに単純なタスクです。それは、ADT のように見えるものに対してのみ動作します。 Logic
より簡単にテストできるようになりました。偽のデータを直接インスタンス化Data
して渡すことができるようになりました。モックや複雑なテストの足場は必要ありません。
良い答えと良い例は、ここで他の人によってすでに与えられています。
DIPが重要な理由は、オブジェクト指向の原則である「疎結合設計」を保証するためです。
ソフトウェア内のオブジェクトは、一部のオブジェクトが最上位のオブジェクトであり、下位レベルのオブジェクトに依存している階層に入ってはなりません。低レベルのオブジェクトの変更は、トップレベルのオブジェクトに波及し、ソフトウェアを変更に対して非常に脆弱にします。
「トップレベル」オブジェクトを非常に安定させ、変更に対して脆弱にしないようにするため、依存関係を反転する必要があります。
制御の反転(IoC) は、フレームワークに依存関係を要求するのではなく、オブジェクトが外部フレームワークによって依存関係を渡される設計パターンです。
従来のルックアップを使用した疑似コードの例:
class Service {
Database database;
init() {
database = FrameworkSingleton.getService("database");
}
}
IoC を使用した同様のコード:
class Service {
Database database;
init(database) {
this.database = database;
}
}
IoC の利点は次のとおりです。
- 中央のフレームワークに依存していないため、必要に応じて変更できます。
- オブジェクトは、できればインターフェイスを使用してインジェクションによって作成されるため、依存関係をモック バージョンに置き換える単体テストを簡単に作成できます。
- コードのデカップリング。
依存関係逆転のポイントは、再利用可能なソフトウェアを作成することです。
2 つのコードが相互に依存するのではなく、抽象化されたインターフェイスに依存するという考え方です。その後、どちらか一方を再利用できます。
これを実現する最も一般的な方法は、Java の Spring のような制御の反転 (IoC) コンテナーを使用することです。このモデルでは、オブジェクトが外に出て依存関係を見つけるのではなく、オブジェクトのプロパティが XML 構成によって設定されます。
この疑似コードを想像してみてください...
public class MyClass
{
public Service myService = ServiceLocator.service;
}
MyClass は Service クラスと ServiceLocator クラスの両方に直接依存します。別のアプリケーションで使用する場合は、両方が必要です。これを想像してみてください...
public class MyClass
{
public IService myService;
}
現在、MyClass は IService インターフェイスという単一のインターフェイスに依存しています。IoC コンテナーに実際にその変数の値を設定させます。
そのため、MyClass は、他の 2 つのクラスの依存関係を持ち込むことなく、他のプロジェクトで簡単に再利用できます。
さらに良いことに、MyService の依存関係、およびそれらの依存関係の依存関係をドラッグする必要はありません。
一般的に良い回答が相次ぐことに加えて、私自身の小さなサンプルを追加して、良い方法と悪い方法を示したいと思います。そして、はい、私は石を投げる人ではありません!
たとえば、コンソール I/O を介して文字列を base64 形式に変換する小さなプログラムが必要だとします。単純なアプローチは次のとおりです。
class Program
{
static void Main(string[] args)
{
/*
* BadEncoder: High-level class *contains* low-level I/O functionality.
* Hence, you'll have to fiddle with BadEncoder whenever you want to change
* the I/O mode or details. Not good. A good encoder should be I/O-agnostic --
* problems with I/O shouldn't break the encoder!
*/
BadEncoder.Run();
}
}
public static class BadEncoder
{
public static void Run()
{
Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(Console.ReadLine())));
}
}
DIP は基本的に、高レベルのコンポーネントは低レベルの実装に依存すべきではないと述べています。「レベル」は、Robert C. Martin (「クリーン アーキテクチャ」) によると、I/O からの距離です。しかし、どうやってこの苦境から抜け出すのですか?インターフェースの実装方法を気にせずに、中央のエンコーダーをインターフェースのみに依存させるだけです。
class Program
{
static void Main(string[] args)
{
/* Demo of the Dependency Inversion Principle (= "High-level functionality
* should not depend upon low-level implementations"):
* You can easily implement new I/O methods like
* ConsoleReader, ConsoleWriter without ever touching the high-level
* Encoder class!!!
*/
GoodEncoder.Run(new ConsoleReader(), new ConsoleWriter()); }
}
public static class GoodEncoder
{
public static void Run(IReadable input, IWriteable output)
{
output.WriteOutput(Convert.ToBase64String(Encoding.ASCII.GetBytes(input.ReadInput())));
}
}
public interface IReadable
{
string ReadInput();
}
public interface IWriteable
{
void WriteOutput(string txt);
}
public class ConsoleReader : IReadable
{
public string ReadInput()
{
return Console.ReadLine();
}
}
public class ConsoleWriter : IWriteable
{
public void WriteOutput(string txt)
{
Console.WriteLine(txt);
}
}
I/O モードを変更するために触る必要はないことに注意してくださいGoodEncoder
— そのクラスは、それが知っている I/O インターフェースに満足しています。低レベルの実装でIReadable
あり、IWriteable
気にすることはありません。