依存関係の逆転の原理とは何ですか?なぜそれが重要なのですか?
依存関係の逆転の原理とは何ですか?なぜそれが重要なのですか?
回答:
このドキュメントをチェックしてください:依存関係の逆転の原則。
それは基本的に言う:
なぜそれが重要なのか、つまり、変更は危険であり、実装ではなく概念に依存することで、呼び出しサイトでの変更の必要性を減らします。
効果的に、DIPは異なるコード部分間の結合を減らします。たとえば、ロギングファシリティを実装する方法はたくさんありますが、使用方法は比較的安定しているはずです。ロギングの概念を表すインターフェースを抽出できれば、このインターフェースはその実装よりも時間的にはるかに安定しているはずであり、そのロギングメカニズムを維持または拡張している間に加えた変更によるコールサイトへの影響ははるかに少ないはずです。
また、実装をインターフェイスに依存させることで、特定の環境により適した実装を実行時に選択できるようになります。場合によっては、これも興味深いかもしれません。
『アジャイルソフトウェア開発、原則、パターン、および実践』およびC#のアジャイル原則、パターン、および実践に関する書籍は、依存関係逆転の原則の背後にある元の目標と動機を完全に理解するための最良のリソースです。記事「依存関係の逆転の原則」も優れたリソースですが、それが最終的に前述の本に取り入れられたドラフトの要約版であることから、それは概念のいくつかの重要な議論を省いていますこの原則を、「Design Patterns(Gamma、et al。)」の本にある「実装ではなくインターフェイスへのプログラム」へのより一般的なアドバイスから区別するための鍵となるパッケージとインターフェイスの所有権。
要約すると、依存関係の逆転の原則は、主に依存関係の従来の方向を「上位レベル」コンポーネントから「下位レベル」コンポーネントに逆転させ、「下位レベル」コンポーネントが「上位レベル」コンポーネントが所有するインターフェースに依存するようにすることです。 。(注:ここでの「より高いレベル」のコンポーネントは、外部の依存関係/サービスを必要とするコンポーネントを指します。必ずしもレイヤードアーキテクチャ内での概念的な位置ではありません。)その際、理論的にはコンポーネントからシフトしているため、カップリングはそれほど減少していません。理論的にはより価値のあるコンポーネントには価値がありません。
これは、コンポーネントのコンシューマーが実装を提供する必要のあるインターフェースに関して外部依存関係が表現されるコンポーネントを設計することによって実現されます。言い換えると、定義されたインターフェースは、コンポーネントの使用方法ではなく、コンポーネントに必要なものを表します(たとえば、「IDoSomething」ではなく「INeedSomething」)。
依存関係の逆転の原則が言及していないのは、インターフェイスを使用して依存関係を抽象化する簡単な方法です(たとえば、MyService→[ILogger⇐Logger])。これにより、依存関係の特定の実装の詳細からコンポーネントが分離されますが、コンシューマと依存関係の関係は反転しません(例:[MyService→IMyServiceLogger]⇐Logger。
依存関係の逆転の原則の重要性は、機能の一部(ロギング、検証など)の外部依存関係に依存するソフトウェアコンポーネントを再利用できるという単一の目標にまで蒸留できます。
再利用というこの一般的な目標の範囲内で、2つのサブタイプの再利用を描くことができます。
サブ依存関係の実装を持つ複数のアプリケーション内でソフトウェアコンポーネントを使用する(例:DIコンテナーを開発していて、ロギングを提供したいが、コンテナーを使用するすべてのユーザーが必要になるように、コンテナーを特定のロガーに結合したくない場合選択したロギングライブラリを使用します)。
進化するコンテキスト内でソフトウェアコンポーネントを使用する(たとえば、実装の詳細が進化するアプリケーションの複数のバージョン間で同じままであるビジネスロジックコンポーネントを開発した)。
インフラストラクチャライブラリなど、複数のアプリケーション間でコンポーネントを再利用する最初のケースでは、そのような依存関係への依存関係を取得する必要があるため、コンシューマを自分のライブラリのサブ依存関係に結合することなく、コアインフラストラクチャのニーズをコンシューマに提供することが目標です。消費者も同じ依存関係を必要とします。これは、ライブラリのコンシューマーが同じインフラストラクチャのニーズ(NLogとlog4netなど)に異なるライブラリを使用することを選択した場合、またはバージョンとの下位互換性がない必要なライブラリの新しいバージョンを使用することを選択した場合に問題になる可能性があります。ライブラリで必要です。
ビジネスロジックコンポーネント(つまり「上位レベルのコンポーネント」)を再利用する2番目のケースの目的は、アプリケーションのコアドメイン実装を、実装の詳細の変化するニーズ(つまり、永続ライブラリ、メッセージングライブラリの変更/アップグレード)から分離することです。 、暗号化戦略など)。理想的には、アプリケーションの実装の詳細を変更しても、アプリケーションのビジネスロジックをカプセル化しているコンポーネントが壊れないようにする必要があります。
注:一部の人は、この2番目のケースを実際の再利用として説明することに反対する場合があります。単一の進化するアプリケーション内で使用されるビジネスロジックコンポーネントなどのコンポーネントは、単一の使用のみを表すためです。ただし、ここでの考え方は、アプリケーションの実装の詳細を変更するたびに新しいコンテキストが提供されるため、ユースケースが異なるということですが、最終的な目標は、分離か移植性かで区別できます。
この2番目のケースで依存関係の逆転の原則に従うといくつかの利点が得られますが、JavaやC#などの現代の言語に適用される場合、その値は大幅に低下し、おそらく無関係になります。前に説明したように、DIPには、実装の詳細を個別のパッケージに完全に分離することが含まれます。ただし、進化するアプリケーションの場合、ビジネスドメインで定義されたインターフェースを単に利用することで、実装の詳細が最終的に同じパッケージ内にある場合でも、実装の詳細コンポーネントのニーズの変化により、上位レベルのコンポーネントを変更する必要がなくなります。 。原則のこの部分は、原則が成文化されたときに見た言語に関連していた側面(つまり、C ++)を反映していますが、新しい言語には関係ありません。それは言った、
インターフェースの単純な使用、依存性注入、および分離されたインターフェースパターンに関連するため、この原則についての詳細な説明は、ここで見つけることができます。さらに、JavaScriptなどの動的型付け言語に原則がどのように関連するかについての議論は、ここにあります。
ソフトウェアアプリケーションを設計するとき、低レベルのクラスを基本および基本操作(ディスクアクセス、ネットワークプロトコルなど)を実装するクラス、高レベルのクラスを複雑なロジック(ビジネスフローなど)をカプセル化するクラスと見なすことができます。
最後のものは低レベルのクラスに依存しています。このような構造を実装する自然な方法は、低レベルのクラスを記述し、それらを用意したら、複雑な高レベルのクラスを記述します。高レベルのクラスは他のクラスで定義されているため、これは論理的な方法のようです。しかし、これは柔軟な設計ではありません。低レベルのクラスを置き換える必要がある場合はどうなりますか?
依存関係逆転の原則は次のように述べています:
この原則は、ソフトウェアの高レベルのモジュールが低レベルのモジュールに依存する必要があるという従来の概念を「逆転」しようとしています。ここで、高レベルのモジュールは、低レベルのモジュールによって実装される抽象化(たとえば、インターフェースのメソッドの決定)を所有します。したがって、下位レベルのモジュールを上位レベルのモジュールに依存させます。
適切に適用された依存関係の反転は、アプリケーションのアーキテクチャ全体のレベルで柔軟性と安定性を提供します。これにより、アプリケーションをより安全かつ安定して進化させることができます。
従来、レイヤードアーキテクチャの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)
これは、六角形のアーキテクチャでも同じように発生します。ドメインも中央部分にあり、ポートはドミノから外部への通信の抽象化です。ここでも、ドメインが最も安定しており、従来の依存関係が逆転していることが明らかです。
私にとって、Dependency Inversion Principleは、公式記事で説明されているように、本質的に再利用可能性が低いモジュールの再利用性を高めるための見当違いの試みであり、C ++言語の問題を回避する方法でもあります。
C ++の問題は、ヘッダーファイルには通常、プライベートフィールドとメソッドの宣言が含まれていることです。したがって、高レベルC ++モジュールに低レベルモジュールのヘッダーファイルが含まれている場合、そのモジュールの実際の実装の詳細に依存します。そして、それは明らかに、良いことではありません。しかし、これは今日一般的に使用されているより現代的な言語では問題ではありません。
前者は通常後者よりもアプリケーション/コンテキスト固有であるため、高レベルモジュールは本質的に低レベルモジュールよりも再利用性が低くなります。たとえば、UI画面を実装するコンポーネントは最高レベルであり、アプリケーションに非常に(完全に)固有です。このようなコンポーネントを別のアプリケーションで再利用しようとすると、生産性が低下し、過剰なエンジニアリングにつながるだけです。
したがって、コンポーネントBに依存する(コンポーネントAに依存しない)コンポーネントAの同じレベルでの別個の抽象化の作成は、コンポーネントAが異なるアプリケーションまたはコンテキストでの再利用に本当に役立つ場合にのみ実行できます。そうでない場合、DIPを適用するのは悪い設計です。
基本的にそれは言う:
クラスは、具体的な詳細(実装)ではなく、抽象化(インターフェース、抽象クラスなど)に依存する必要があります。
良い答えと良い例は、すでにここで他の人によって与えられています。
その理由のDIPは重要であり、それは「緩やかにデザインを結合された」OO-原則を保証するからです。
ソフトウェア内のオブジェクトは、低レベルのオブジェクトに依存して、一部のオブジェクトがトップレベルのオブジェクトである階層に入らないようにしてください。低レベルのオブジェクトの変更は、トップレベルのオブジェクトに波及し、ソフトウェアの変更が非常に脆弱になります。
「トップレベル」のオブジェクトが非常に安定していて、変更に対して脆弱ではないようにしたいので、依存関係を逆にする必要があります。
依存関係の逆転の原則を明確にする方法は次のとおりです。
複雑なビジネスロジックをカプセル化するモジュールは、ビジネスロジックをカプセル化する他のモジュールに直接依存すべきではありません。代わりに、単純なデータへのインターフェースのみに依存する必要があります。
つまり、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
偽のデータで直接インスタンス化して渡すことができるようになりました。モックや複雑なテストの足場は必要ありません。DataFromDependency
これを直接参照する、Dependency
として、同じモジュールでありLogic
、その後、Logic
モジュールはまだ直接に依存してDependency
、コンパイル時にモジュール。パー原則のおじさんボブの説明、それを回避することはDIPの全体のポイントです。むしろ、DIPをフォローするにData
は、と同じモジュールLogic
にあるDataFromDependency
必要がありますが、と同じモジュールにある必要がありますDependency
。
制御の反転(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の依存関係とそれらの依存関係の依存関係をドラッグする必要はありません。
企業の「高レベル」の従業員が彼らの計画の実行に対して支払われ、これらの計画が多くの「低レベル」の従業員の計画の総計の実行によって提供されると仮定すると、次のように言うことができます。高レベルの従業員の計画の説明が何らかの方法で低レベルの従業員の特定の計画に結び付けられている場合、それは一般的にひどい計画です。
上級幹部が「配達時間を改善する」計画を持っており、配送ラインの従業員が毎朝コーヒーを飲んでストレッチをしなければならないことを示している場合、その計画は結合度が高く、凝集度が低くなります。しかし、計画が特定の従業員について言及しておらず、実際に単に「作業を実行できるエンティティーが機能するように準備されている」ことを要求する場合、計画は疎結合でより凝集性があります。計画は重複せず、簡単に置き換えることができます。請負業者、またはロボットは、従業員を簡単に置き換えることができ、高レベルの計画は変更されません。
依存関係逆転原理の「高レベル」は「より重要」を意味します。
上記の回答で良い説明がなされているのが分かります。ただし、簡単な例を使用して簡単な説明を提供したいと思います。
依存関係の逆転の原則により、プログラマーはハードコーディングされた依存関係を削除して、アプリケーションを疎結合にして拡張可能にすることができます。
これを達成する方法:抽象化を通じて
依存関係の逆転なし:
class Student {
private Address address;
public Student() {
this.address = new Address();
}
}
class Address{
private String perminentAddress;
private String currentAdrress;
public Address() {
}
}
上記のコードスニペットでは、アドレスオブジェクトはハードコーディングされています。代わりに、依存関係の逆転を使用し、コンストラクターまたはセッターメソッドを介してアドレスオブジェクトを挿入できる場合。どれどれ。
依存関係の反転あり:
class Student{
private Address address;
public Student(Address address) {
this.address = address;
}
//or
public void setAddress(Address address) {
this.address = address;
}
}
依存関係の逆転:具体化ではなく、抽象化に依存します。
制御の反転:メインvs抽象化、およびメインがシステムの接着剤である方法。
これらはこれについて話すいくつかの良い記事です:
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/
2つのクラスがあるEngineer
としProgrammer
ます:と:
クラスエンジニアは、以下のようにプログラマクラスに依存しています。
class Engineer () {
fun startWork(programmer: Programmer){
programmer.work()
}
}
class Programmer {
fun work(){
//TODO Do some work here!
}
}
この例でEngineer
は、クラスはクラスに依存していますProgrammer
。変更する必要がある場合はどうなりProgrammer
ますか?
明らかに私Engineer
も変更する必要があります。(うわー、この時点でOCP
も違反されています)
次に、この混乱を解消するために何が必要なのでしょうか。答えは実際には抽象化です。抽象化により、これら2つのクラス間の依存関係を削除できます。たとえばInterface
、Programmerクラスのを作成できます。これからProgrammer
は、を使用Interface
するすべてのクラスでを使用する必要があります。次に、Programmerクラスを変更することにより、それを使用したクラスを変更する必要はありません。中古。
注:DependencyInjection
を行うのDIP
にSRP
も役立ちます。
一般的に良い答えの急増に加えて、自分自身の小さなサンプルを追加して、良い実践と悪い実践を実証したいと思います。そして、はい、私は石を投げる人ではありません!
たとえば、小さなプログラムでコンソール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);
}
}
GoodEncoder
I / Oモードを変更するために触れる必要がないことに注意してください。そのクラスは、知っているI / Oインターフェースに満足しています。低レベルの実装でIReadable
、IWriteable
それを気にすることはありません。
GoodEncoder
2番目の例では、リーダーとライターは依存しません。DIPの例を作成するには、ここで抽出したインターフェイスを「所有」する概念を導入する必要があります。特に、実装を外部に残しながら、それらをGoodEncoderと同じパッケージに配置する必要があります。
依存関係逆転原理(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();
}
}
注:クラスは、特定の詳細(インターフェースの実装)ではなく、インターフェースまたは抽象クラスのような抽象化に依存する必要があります。