依存関係の逆転の原則とは何ですか、なぜ重要なのですか?
このドキュメントを確認してください: The Dependency Inversion Principle 。
それは基本的に言う:
なぜ重要なのか、要するに、変更にはリスクが伴い、実装ではなく概念に依存することで、コールサイトでの変更の必要性を減らすことができます。
効果的に、DIPはコードの異なる部分間の結合を減らします。考えは、たとえばロギング機能を実装する多くの方法がありますが、それを使用する方法は時間内に比較的安定していなければならないということです。ロギングの概念を表すインターフェースを抽出できる場合、このインターフェースはその実装よりもはるかに安定している必要があり、呼び出しサイトはそのロギングメカニズムの維持または拡張中に行うことができる変更による影響がはるかに少ないはずです。
また、実装をインターフェイスに依存させることにより、実行時に特定の環境により適した実装を選択できるようになります。場合によっては、これも興味深い場合があります。
C#の「アジャイルソフトウェア開発、原則、パターン、および実践」および「アジャイルの原則、パターン、および実践」という本は、依存関係逆転の原理の元の目標と動機を完全に理解するための最良のリソースです。 「The Dependency Inversion Principle」という記事も優れたリソースですが、最終的に前述の本に反映されたドラフトの要約版であるという事実により、aの概念に関する重要な議論を省略しています。このデザインを、Design Patterns(Gamma、et。al。)にある「実装ではなくインターフェイスにプログラムする」というより一般的なアドバイスと区別するための重要なパッケージとインターフェイスの所有権。
要約を提供するために、依存関係反転の原則は、「高レベル」コンポーネントから「低レベル」コンポーネントへの従来の依存関係の方向を主に逆転することです。 「下位レベル」コンポーネントは、「上位レベル」コンポーネントが所有するインターフェースに依存します。 (注:ここでの「より高いレベル」のコンポーネントとは、外部の依存関係/サービスを必要とするコンポーネントを指し、必ずしも階層化アーキテクチャ内の概念的な位置とは限りません。)そうすることで、カップリングはreduced理論的に価値の低いコンポーネントから理論的に価値の高いコンポーネントへシフトするほど.
これは、コンポーネントのコンシューマーが実装を提供しなければならないインターフェイスの観点から外部依存関係が表現されているコンポーネントを設計することによって実現されます。つまり、定義されたインターフェイスは、コンポーネントの使用方法ではなく、コンポーネントに必要なものを表します(例:「IDoSomething」ではなく「INeedSomething」)。
依存関係反転の原則が言及していないのは、インターフェイスを使用して依存関係を抽象化する簡単な方法です(例:MyService→[ILogger⇐Logger])。これにより、コンポーネントは依存関係の特定の実装詳細から切り離されますが、コンシューマと依存関係の関係は逆転しません(例:[MyService→IMyServiceLogger]⇐Logger。
依存性反転の原理の重要性は、機能の一部(ロギング、検証など)の外部依存性に依存するソフトウェアコンポーネントを再利用できるという単一の目標にまで絞り込むことができます。
再利用というこの一般的な目標の中で、次の2つのサブタイプの再利用を説明できます。
サブ依存の実装を持つ複数のアプリケーション内でソフトウェアコンポーネントを使用する(たとえば、DIコンテナーを開発してロギングを提供したいが、コンテナーを使用する全員が特定のロガーに連結しないようにしたい場合)選択したロギングライブラリを使用します)。
進化するコンテキスト内でソフトウェアコンポーネントを使用する(たとえば、実装の詳細が進化しているアプリケーションの複数のバージョンで同じままのビジネスロジックコンポーネントを開発した場合)。
インフラストラクチャライブラリなど、複数のアプリケーションでコンポーネントを再利用する最初のケースでは、消費者を独自のライブラリの下位依存に結合することなく、コアインフラストラクチャのニーズを消費者に提供することが目標です。消費者も同じ依存関係を要求します。これは、ライブラリの消費者が同じインフラストラクチャのニーズに応じて異なるライブラリを使用することを選択した場合(NLogとlog4netなど)、またはバージョンと後方互換性のない必要なライブラリの新しいバージョンを使用する場合に問題になる可能性がありますライブラリに必要です。
ビジネスロジックコンポーネント(つまり「高レベルコンポーネント」)を再利用する2番目のケースでは、目標は、アプリケーションのコアドメイン実装を、実装の詳細の変化するニーズ(永続ライブラリ、メッセージングライブラリの変更/アップグレード)から分離することです。 、暗号化戦略など)。理想的には、アプリケーションの実装の詳細を変更しても、アプリケーションのビジネスロジックをカプセル化するコンポーネントが壊れてはいけません。
注:一部の人は、この2番目のケースを実際の再利用として説明することに反対する場合があります。ただし、ここでの考え方は、アプリケーションの実装の詳細を変更するたびに、新しいコンテキストと異なるユースケースが表示されるというものですが、最終的な目標は分離と移植性で区別できます。
この2番目のケースで依存性反転の原則に従うことはいくつかの利点を提供しますが、JavaおよびC#のような現代の言語に適用される値は、おそらく前述のように、DIPでは実装の詳細を完全に個別のパッケージに分離する必要がありますが、アプリケーションが進化している場合は、ビジネスドメインの観点から定義されたインターフェイスを使用するだけで、実装の詳細が最終的に同じパッケージ内にある場合でも、実装の詳細コンポーネントのニーズの変化原則のこの部分は、原則が成文化されたときに見た言語に関連した側面(つまり、C++)を反映します。ただし、依存関係反転の原則の重要性は、主に再利用可能なソフトウェアコンポーネント/ライブラリの開発にあります。
インターフェースの単純な使用、依存性注入、および分離インターフェースパターンに関連するこの原則の詳細な説明は、 here にあります。さらに、原則がJavaScriptなどの動的に型付けされた言語にどのように関連するかについての議論は、よく知られています here 。
ソフトウェアアプリケーションを設計するとき、低レベルクラスを基本操作と主要操作(ディスクアクセス、ネットワークプロトコルなど)を実装するクラス、高レベルクラスを複雑なロジック(ビジネスフローなど)をカプセル化するクラスと見なすことができます。
最後のものは低レベルのクラスに依存しています。そのような構造を実装する自然な方法は、低レベルのクラスを記述し、複雑な高レベルのクラスを記述したらそれらを記述することです。高レベルのクラスは他の用語で定義されているため、これが論理的な方法のようです。しかし、これは柔軟な設計ではありません。低レベルのクラスを置き換える必要がある場合はどうなりますか?
依存性反転の原理は次のように述べています:
この原則は、ソフトウェアの高レベルモジュールが低レベルモジュールに依存するという従来の概念を「逆転」させようとしています。ここで、高レベルモジュールは、低レベルモジュールによって実装される抽象化(たとえば、インターフェイスのメソッドの決定)を所有します。したがって、下位モジュールを上位モジュールに依存させます。
私にとって、 公式記事 で説明されている依存性反転の原理は、本質的に再利用性の低いモジュールの再利用性を高めるための見当違いの試みであり、 C++言語。
C++の問題は、通常、ヘッダーファイルにプライベートフィールドとメソッドの宣言が含まれていることです。したがって、高レベルC++モジュールに低レベルモジュールのヘッダーファイルが含まれている場合、実際の 実装 そのモジュールの詳細。そして、それは明らかに良いことではありません。しかし、これは、今日一般的に使用されている最新の言語では問題になりません。
前者は通常、後者よりもアプリケーション/コンテキスト固有であるため、高レベルモジュールは低レベルモジュールよりも本質的に再利用性が低くなります。たとえば、UI画面を実装するコンポーネントは、最高レベルであり、アプリケーションに非常に(完全に?)固有です。そのようなコンポーネントを別のアプリケーションで再利用しようとすると、生産性が低下し、過剰なエンジニアリングにつながる可能性があります。
したがって、コンポーネントBに依存するコンポーネントAの同じレベルでの個別の抽象化の作成は、コンポーネントAが異なるアプリケーションまたはコンテキストでの再利用に本当に役立つ場合にのみ実行できます。そうでない場合は、DIPを適用するのは適切ではありません。
依存関係の反転を適切に適用すると、アプリケーションのアーキテクチャ全体のレベルで柔軟性と安定性が得られます。アプリケーションをより安全かつ安定的に進化させることができます。
従来、レイヤードアーキテクチャUIはビジネスレイヤーに依存しており、これはデータアクセスレイヤーに依存していました。
http://xurxodev.com/content/images/2016/02/Traditional-Layered.png
レイヤー、パッケージ、またはライブラリを理解する必要があります。コードがどのようになるか見てみましょう。
データアクセスレイヤー用のライブラリまたはパッケージがあります。
// 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;
}
ビジネスとデータアクセスの間の抽象化依存に依存していますが、同じままです。
http://xurxodev.com/content/images/2016/02/Traditional-Layered.png
依存関係の反転を取得するには、低レベルモジュールではなく、この高レベルロジックまたはドメインがあるモジュールまたはパッケージで永続性インターフェイスを定義する必要があります。
最初にドメイン層が何であるかを定義し、その通信の抽象化が永続性として定義されます。
// Domain.dll
public interface IProductRepository;
using DataAccessLayer;
public class ProductBO {
private IProductRepository productRepository;
}
永続層がドメインに依存した後、依存関係が定義されている場合はすぐに反転するようになります。
// Persistence.dll
public class ProductDAO : IProductRepository{
}
http://xurxodev.com/content/images/2016/02/Dependency-Inversion-Layers.png
コンセプトをうまく取り入れて、目的と利点を深めることが重要です。機械的に留まり、典型的な事例リポジトリを学習した場合、依存の原則をどこで適用できるかを特定することはできません。
しかし、なぜ依存関係を逆転させるのでしょうか?特定の例を超えた主な目的は何ですか?
このような一般的なにより、安定性の低いものに依存しない最も安定したものをより頻繁に変更できます。
永続性と通信するように設計されたドメインロジックまたはアクションよりも、データベースまたはテクノロジが同じデータベースにアクセスするために、永続性タイプを変更する方が簡単です。このため、この変更が発生した場合に永続性を変更する方が簡単なので、依存関係が逆転します。この方法では、ドメインを変更する必要はありません。ドメイン層は最も安定しているため、何にも依存してはいけません。
しかし、このリポジトリの例だけではありません。この原則が適用される多くのシナリオがあり、この原則に基づいたアーキテクチャがあります。
依存関係の逆転がその定義の鍵となるアーキテクチャがあります。すべてのドメインで最も重要であり、ドメインとパッケージまたはライブラリの残りの部分との間の通信プロトコルが定義されていることを示すのは抽象化です。
クリーンアーキテクチャ では、ドメインは中央にあり、依存関係を示す矢印の方向を見ると、最も重要で安定したレイヤーが明確になっています。外層は不安定なツールと見なされるため、それらに依存しないようにしてください。
六角形のアーキテクチャでも同じことが起こります。ドメインも中央部にあり、ポートはドミノから外部への通信の抽象化です。ここでも、ドメインが最も安定しており、従来の依存関係が反転していることが明らかです。
基本的にそれは言います:
クラスは、具体的な詳細(実装)ではなく、抽象化(インターフェイス、抽象クラスなど)に依存する必要があります。
依存性反転の原理を説明するはるかに明確な方法は次のとおりです。
複雑なビジネスロジックをカプセル化するモジュールは、ビジネスロジックをカプセル化する他のモジュールに直接依存しないでください。代わりに、単純なデータへのインターフェイスのみに依存する必要があります。
つまり、通常のようにクラス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
と同じモジュールに存在する必要があります。
どうしてですか?
Dependency
が変更された場合、Logic
を変更する必要はありません。Logic
が何をするかを理解することは、はるかに簡単なタスクです。ADTのように見えるものでのみ動作します。Logic
をより簡単にテストできるようになりました。偽データでData
を直接インスタンス化して渡すことができるようになりました。モックや複雑なテスト足場は必要ありません。良い答えと良い例はすでに他の人からここに与えられています。
[〜#〜] dip [〜#〜] が重要な理由は、OO原則の「疎結合設計」を保証するためです。
ソフトウェア内のオブジェクトは、一部のオブジェクトが低レベルのオブジェクトに依存するトップレベルのオブジェクトである階層には入らないようにしてください。低レベルのオブジェクトの変更は、トップレベルのオブジェクトに波及し、ソフトウェアを非常に脆弱にします。
「トップレベル」オブジェクトを非常に安定し、変更に対して脆弱ではないようにするため、依存関係を反転する必要があります。
制御の反転 (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の依存関係、およびそれらの依存関係の依存関係をドラッグする必要はありません。
私ははるかに優れた(より直感的な)例があると思います。
したがって、従業員管理モジュールと連絡先管理モジュールの両方について知る必要がある「メイン」エントリポイントがあり、リンクを提供する必要があります。言い換えると、メインモジュールはこれら2つに依存します。つまり、(共有)ナビゲーションでレンダリングする必要のあるコントローラー、ルート、およびリンクを認識します。
Node.jsの例
// main.js
import express from 'express'
// two modules, each having many exports
import { api as contactsApi, navigation as cNav } from './contacts/'
import { api as employeesApi, navigation as eNav } from './employees/'
const api = express()
const navigation = {
...cNav,
...eNav
}
api.use('contacts', contactsApi)
api.use('employees', employeesApi)
// do something with navigation, possibly do some other setup
また、注意してくださいこれがまったく問題ない場合(単純な場合)があります。
そのため、新しいモジュールを追加するのはそれほど簡単ではない時点に達するでしょう。 API、ナビゲーション、おそらくpermissionsを登録することを忘れないでください。このmain.jsはどんどん大きくなります。
そして、それが依存関係の逆転の出番です。メインモジュールが他のすべてのモジュールに依存する代わりに、いくつかの「コア」を導入し、すべてのモジュールを登録します。
したがって、この場合は、多くのサービス(ルート、ナビゲーション、アクセス許可)に自分自身を送信できるApplicationModuleの概念を持ち、メインモジュールは単純なままにすることができます(モジュールをインポートしてインストールします)
つまり、プラグ可能なアーキテクチャを作成することです。これは追加の作業であり、コードの記述/読み取りと保守が必要になるため、前もって行うのではなく、このような匂いがする場合に行う必要があります。
特に興味深いのは、何でもプラグインにすることができることです。永続化レイヤーでさえも可能です。多くの永続化実装をサポートする必要がある場合、これは価値があるかもしれませんが、通常はそうではありません。六角形のアーキテクチャの画像に関する他の回答を参照してください、それは説明に最適です-コアがあり、他のすべては本質的にプラグインです。
依存関係の反転:コンクリートではなく、抽象化に依存します。
制御の反転:メイン対抽象化、およびメインがシステムの接着剤である方法。
これらについて、次のような良い投稿があります。
https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/
https://coderstower.com/2019/04/02/main-and-abstraction-the-decoupled-peers/
https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/
コントロールコンテナの反転と依存性注入パターン Martin Fowlerによる読むのも良い方法です。 Head First Design Patterns は、DIやその他のパターンを初めて学習するための素晴らしい本です。
一般的に良い回答が急増していることに加えて、自分自身の小さなサンプルを追加して、良いプラクティスと悪いプラクティスを示したいと思います。そして、はい、私は石を投げる人ではありません!
たとえば、コンソールI/Oを介して、小さなプログラムで文字列をbase64形式に変換にしたいとします。素朴なアプローチは次のとおりです。
class Program
{
static void Main(string[] args)
{
/*
* BadEncoder: High-level class *contains* low-lever 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(「Clean Architecture」)によると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
の低レベルの実装はそれを気にしません。
他の答えに加えて....
最初に例を示します。
食料品の供給を依頼するホテルがあるとします。ホテルは、食品の名前(鶏肉など)をFood Generatorに渡し、Generatorは要求された食品をホテルに返します。しかし、ホテルは受け取った料理の種類を気にしません。そのため、ジェネレーターはホテルに「食品」というラベルの付いた食品を供給します。
この実装はJavaにあります
Factoryメソッドを使用したFactoryClass。FoodGenerator
public class FoodGenerator {
Food food;
public Food getFood(String name){
if(name.equals("fish")){
food = new Fish();
}else if(name.equals("chicken")){
food = new Chicken();
}else food = null;
return food;
}
}
抽象/インターフェースクラス
public abstract class Food {
//None of the child class will override this method to ensure quality...
public void quality(){
String fresh = "This is a fresh " + getName();
String tasty = "This is a tasty " + getName();
System.out.println(fresh);
System.out.println(tasty);
}
public abstract String getName();
}
チキンは食品を実装します(具体的なクラス)
public class Chicken extends Food {
/*All the food types are required to be fresh and tasty so
* They won't be overriding the super class method "property()"*/
public String getName(){
return "Chicken";
}
}
FishはFood(A Concrete Class)を実装しています
public class Fish extends Food {
/*All the food types are required to be fresh and tasty so
* They won't be overriding the super class method "property()"*/
public String getName(){
return "Fish";
}
}
最後に
ホテル
public class Hotel {
public static void main(String args[]){
//Using a Factory class....
FoodGenerator foodGenerator = new FoodGenerator();
//A factory method to instantiate the foods...
Food food = foodGenerator.getFood("chicken");
food.quality();
}
}
ご覧のとおり、ホテルは鶏オブジェクトか魚オブジェクトかを知りません。食品オブジェクトであることのみを知っています。つまり、ホテルは食品クラスに依存しています。
また、魚と鶏肉のクラスは食品のクラスを実装しており、ホテルとは直接関係がないことに気付くでしょう。つまり、鶏肉と魚も食品の種類に依存します。
これは、高レベルのコンポーネント(ホテル)と低レベルのコンポーネント(魚と鶏肉)の両方が抽象化(食品)に依存していることを意味します。
これは、依存性反転と呼ばれます。
依存性反転原理(DIP)によると
i)高レベルのモジュールは低レベルのモジュールに依存すべきではありません。どちらも抽象化に依存する必要があります。
ii)抽象化は決して詳細に依存してはなりません。詳細は抽象化に依存する必要があります。
例:
public interface ICustomer
{
string GetCustomerNameById(int id);
}
public class Customer : ICustomer
{
//ctor
public Customer(){}
public string GetCustomerNameById(int id)
{
return "Dummy Customer Name";
}
}
public class CustomerFactory
{
public static ICustomer GetCustomerData()
{
return new Customer();
}
}
public class CustomerBLL
{
ICustomer _customer;
public CustomerBLL()
{
_customer = CustomerFactory.GetCustomerData();
}
public string GetCustomerNameById(int id)
{
return _customer.GetCustomerNameById(id);
}
}
public class Program
{
static void Main()
{
CustomerBLL customerBLL = new CustomerBLL();
int customerId = 25;
string customerName = customerBLL.GetCustomerNameById(customerId);
Console.WriteLine(customerName);
Console.ReadKey();
}
}
注:クラスは、特定の詳細(インターフェイスの実装)ではなく、インターフェイスや抽象クラスなどの抽象化に依存する必要があります。