なぜ依存性注入を使用するのですか?


536

依存性注入(DI)を理解しようとしていますが、もう一度失敗しました。ばかげているように見えるだけです。私のコードは決して混乱しない。私はほとんど仮想関数とインターフェイスを作成しません(ただし、ブルームーンで1回行います)。すべての構成は、json.netを使用して(時にはXMLシリアライザーを使用して)魔法のようにクラスにシリアル化されます。

それがどのような問題を解決するのかよくわかりません。「こんにちは。この関数を実行すると、このタイプのオブジェクトを返し、これらのパラメーター/データを使用します。」
しかし...なぜそれを使うのでしょうか?私も使用する必要がなかったことに注意してくださいobjectが、それが何のためにあるのか理解しています。

DIを使用するWebサイトまたはデスクトップアプリケーションを構築する場合の実際の状況は何ですか?ゲームでインターフェイス/仮想関数を使用する理由を簡単に考え出すことができますが、ゲーム以外のコードでそれを使用することは非常にまれです(単一のインスタンスを思い出せないほどまれです)。


3
これも役立つ情報です:martinfowler.com/articles/injection.html
ta.speot.is




5
DIの別の非常に簡単な説明:codearsenal.net/2015/03/...
ybonda

回答:


840

最初に、私がこの回答に対して行う前提について説明します。それは常に正しいとは限りませんが、かなり頻繁に:

インターフェイスは形容詞です。クラスは名詞です。

(実際には、名詞でもあるインターフェースがありますが、ここで一般化したいと思います。)

だから、インタフェースを例えばすることなど何もありIDisposableIEnumerableまたはIPrintable。クラスは、これらのインターフェースの1つ以上の実際の実装です。ListまたはMap、両方がの実装である場合もありますIEnumerable

要点を言うと、多くの場合、クラスは互いに依存しています。たとえばDatabase、データベースにアクセスするクラスを作成することもできますが(ハァッ、サプライズ!;-))、このクラスにデータベースへのアクセスに関するロギングを実行させることもできます。あなたが別のクラスがあるとしLogger、その後、Databaseへの依存関係を持っていますLogger

ここまでは順調ですね。

Database次の行を使用して、クラス内でこの依存関係をモデル化できます。

var logger = new Logger();

そして、すべてが大丈夫です。大量のロガーが必要であることがわかった日まで問題ありません。コンソール、ファイルシステム、TCP / IPやリモートロギングサーバーなどを使用してログを記録したい場合もあります。

そしてもちろん、あなたはあなたのすべてのコードを変更したくない(その間あなたはそれの何十億も持っている)そしてすべての行を置き換えたくない

var logger = new Logger();

沿って:

var logger = new TcpLogger();

まず、これは面白くない。次に、これはエラーが発生しやすくなります。第三に、これは訓練された猿のための愚かで反復的な作業です。それで、あなたは何をしますか?

明らかにICanLog、すべてのさまざまなロガーによって実装されるインターフェース(または同様のもの)を導入することは非常に良いアイデアです。したがって、コードのステップ1は次のようにすることです。

ICanLog logger = new Logger();

型推論によって型が変更されることはなくなりました。開発する単一のインターフェイスが常に1つあります。次のステップは、何度も何度も持ちたくないということですnew Logger()。したがって、信頼性を高めて、単一の中央ファクトリクラスに新しいインスタンスを作成し、次のようなコードを取得します。

ICanLog logger = LoggerFactory.Create();

ファクトリー自体がどのようなロガーを作成するかを決定します。コードは不要になり、使用するロガーのタイプを変更したい場合は、一度変更します:ファクトリー内。

もちろん、このファクトリを一般化して、どのタイプでも機能させることができます。

ICanLog logger = TypeFactory.Create<ICanLog>();

このTypeFactoryのどこかに、特定のインターフェースタイプが要求されたときに実際のクラスがインスタンス化する構成データが必要なので、マッピングが必要です。もちろん、コード内でこのマッピングを行うことはできますが、型の変更は再コンパイルを意味します。ただし、このマッピングをXMLファイル内に配置することもできます。これにより、コンパイル時間(!)の後でさえ、実際に使用されるクラスを変更できます。つまり、再コンパイルせずに動的に変更できます。

このための便利な例を挙げましょう。通常はログに記録しないソフトウェアを考えてください。ただし、顧客が問題を抱えているために電話をかけて助けを求めた場合、顧客に送信するのは更新されたXML構成ファイルだけであり、ロギングが有効になり、サポートはログファイルを使用して顧客を支援できます。

そして今、名前を少し置き換えると、Service Locatorの単純な実装になります。これは、Inversion of Controlの 2つのパターンの1つです(インスタンス化する正確なクラスをだれが決定するかについて制御を反転するため)。

これにより、コード内の依存関係が減少しますが、すべてのコードが中央の単一のサービスロケータに依存します。

依存性注入がこの行の次のステップになりました:サービスロケーターへのこの単一の依存関係を取り除くだけです:さまざまなクラスが特定のインターフェイスの実装をサービスロケーターに要求する代わりに、もう一度-誰が何をインスタンス化するかの制御を元に戻します。

依存性注入により、Databaseクラスに型のパラメーターを必要とするコンストラクターが追加されましたICanLog

public Database(ICanLog logger) { ... }

これで、データベースには常に使用するロガーがありますが、このロガーがどこから来たかはわかりません。

これがDIフレームワークの出番です。もう一度マッピングを構成してから、DIフレームワークにアプリケーションのインスタンスを作成するように依頼します。Applicationクラスに必要なICanPersistData実装のインスタンスがDatabase注入される-しかし、そのためには、最初のために構成されているロガーの種類のインスタンスを作成する必要がありますICanLog。等々 ...

したがって、長い話を簡単に言うと、依存関係の注入は、コード内の依存関係を削除する2つの方法のうちの1つです。これは、コンパイル後の構成変更に非常に役立ち、単体テストに最適です(スタブやモックの挿入が非常に簡単になるため)。

実際には、サービスロケーターなしでは実行できないことがいくつかあります(たとえば、特定のインターフェイスに必要なインスタンスの数が事前にわからない場合):DIフレームワークは常にパラメーターごとに1つのインスタンスのみを注入しますが、もちろん、ループ内のサービスロケーター)、したがって、ほとんどの場合、各DIフレームワークもサービスロケーターを提供します。

しかし、基本的にはそれだけです。

PS:ここで説明したのは、コンストラクターインジェクションと呼ばれる手法です。コンストラクターパラメーターではなくプロパティインジェクションもありますが、プロパティは依存関係の定義と解決に使用されます。プロパティインジェクションはオプションの依存関係、コンストラクタインジェクションは必須の依存関係と考えてください。しかし、これに関する議論はこの質問の範囲を超えています。


7
もちろん、これもこの方法で実行できますが、実装の交換可能性をサポートするすべての単一クラスにこのロジックを実装する必要があります。これは、重複した冗長なコードがたくさんあることを意味します。また、必要になった場合は、既存のクラスを変更して部分的に書き直す必要があることも意味します。DIを使用すると、任意のクラスでこれを使用できます。特別な方法でそれらを記述する必要はありません(コンストラクターでパラメーターとして依存関係を定義する場合を除く)。
Golo Roden、2013年

137
ここに私がDIについて決して得ないものがあります:それはアーキテクチャを非常に複雑にします。それでも、私が見ているように、使用はかなり制限されています。例は確かに常に同じです:交換可能なロガー、交換可能なモデル/データアクセス。時々交換可能なビュー。しかし、それだけです。これらのいくつかのケースは、非常に複雑なソフトウェアアーキテクチャを本当に正当化しますか?–完全な開示:私はすでに大きな効果を得るためにDIを使用しましたが、これは一般化できない非常に特殊なプラグインアーキテクチャのためのものでした。
Konrad Rudolph

17
@GoloRoden、なぜILoggerではなくインターフェイスICanLogを呼び出すのですか?私はこれを頻繁に行う別のプログラマーと一緒に作業しましたが、慣例を理解できませんでしたか?私にとっては、IEnumerable ICanEnumerateを呼び出すようなものですか?
DermFrench 2013年

28
私はそれをICanLogと呼びました。なぜなら、私たちは何も意味しない単語(名詞)を頻繁に処理するからです。たとえば、ブローカーとは何ですか?マネージャー?リポジトリでさえ、独自の方法で定義されていません。そして、これらすべてを名詞として持つことは、オブジェクト指向言語の典型的な病気です(steve-yegge.blogspot.de/2006/03/…を参照)。私が表現したいのは、私のためにロギングを実行できるコンポーネントがあるということです-では、なぜそれをそのように呼び出さないのですか?もちろん、これは最初の人としてIを使用しているため、ICanLog(ForYou)でもあります。
Golo Roden 2013年

18
@Davidユニットテストは問題なく動作します。結局のところ、ユニットは他のものから独立しています(そうでなければ、ユニットではありません)。何をしない DIコンテナなしで動作することはモックテストです。十分に公平ですが、モックの利点は、すべてのケースでDIコンテナーを追加することで追加される複雑さを上回っているとは思いません。厳密な単体テストを行っています。あざけることはめったにありません。
Konrad Rudolph

499

依存関係の注入と依存関係の注入のフレームワーク(またはよく呼ばれるコンテナー)の違いについて、人々が混乱することがよくあります。

依存性注入は非常に単純な概念です。このコードの代わりに:

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

あなたはこのようなコードを書きます:

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

以上です。真剣に。これはあなたに多くの利点を与えます。2つの重要なものは、Main()プログラム全体に機能を分散させる代わりに、中央の場所(関数)から機能を制御する機能と、各クラスを分離してより簡単にテストする機能(モックまたは他の偽のオブジェクトをコンストラクタに渡すことができるため)実際の価値の)。

もちろん、欠点は、プログラムで使用されるすべてのクラスを認識するメガ関数が1つあることです。これが、DIフレームワークが役立つことです。しかし、このアプローチがなぜ価値があるのか​​理解に問題がある場合は、最初に手動の依存性注入から始めることをお勧めします。そうすれば、さまざまなフレームワークが何をできるかをよりよく理解できるでしょう。


7
なぜ最初のコードよりも2番目のコードを選ぶのですか?最初のものは新しいキーワードしかありませんが、これはどのように役立ちますか?
user962206 2013年

17
@ user962206は、Bから独立してAをテストする方法について考えます
jk。

66
@ user962206も、Bがそのコンストラクターでいくつかのパラメーターを必要とする場合にどうなるかを考えます。インスタンス化するために、Aはそれらのパラメーターについて知る必要があります。これは、Aとはまったく無関係である可能性があります(Bに依存したいだけです) 、Bが依存するものではありません)。そのAのコンストラクタ解きにすでに構築さB(またはそのことについてはBの任意のサブクラスまたはモック)を渡すと:) AはBのみに依存します
epidemian

17
@ acidzombie24:多くの設計パターンと同様に、コードベースが単純なアプローチで問題になるほど大きくない限り、DIは実際には役に立ちません。私の直感は、アプリケーションのコードが約20,000行を超えるか、他のライブラリやフレームワークへの依存関係が20以上になるまで、DIは実際には改善されないということです。アプリケーションがそれよりも小さい場合でも、DIスタイルでプログラミングすることをお勧めしますが、その差はそれほど劇的ではありません。
Daniel Pryden 2013年

2
@DanielPrydenコードサイズは、コードの動的さほど重要ではないと思います。同じインターフェイスに適合する新しいモジュールを定期的に追加する場合、依存するコードを頻繁に変更する必要はありません。
FistOfFury

35

他の回答が述べたように、依存性注入はそれを使用するクラスの外に依存関係を作成する方法です。それらを外部から注入し、クラスの内部から離れてそれらの作成を制御します。これは、依存性注入が制御反転(IoC)原理の実現である理由でもあります。

IoCは原則であり、DIはパターンです。私の経験では、「複数のロガーが必要」になるかもしれないという理由は、実際には決して満たされませんが、実際の理由は、何かをテストするときはいつでも本当に必要なためです。例:

私の特徴:

オファーを見たときに、それを自動的に見たことをマークして、忘れないようにします。

あなたはこのようにこれをテストするかもしれません:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

したがって、のどこかに、次のOfferWeaselようなオファーオブジェクトが作成されます。

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

ここでの問題は、設定されている日付がアサートされている日付とは異なるため、このテストは常に失敗する可能性が高いことです。DateTime.Nowテストコードを入力しただけでも、数ミリ秒ずれる可能性があり、したがって常に失敗します。より良い解決策は、このためのインターフェースを作成することです。これにより、設定する時間を制御できます。

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

インターフェイスは抽象化です。1つは実際のもので、もう1つは必要な場所で時間を偽ることができます。テストは次のように変更できます。

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

このように、依存関係を注入する(現在の時刻を取得する)ことにより、「制御の反転」の原則を適用しました。これを行う主な理由は、分離された単体テストを簡単にするためです。これを行う方法は他にもあります。たとえば、C#では関数を変数として渡すことができるため、ここでのインターフェイスとクラスは不要です。したがって、インターフェイスの代わりにを使用しFunc<DateTime>て同じことを実現できます 。または、動的なアプローチをとる場合は、同等のメソッド(ダックタイピング)を持つオブジェクトを渡すだけで、インターフェイスはまったく必要ありません。

複数のロガーが必要になることはほとんどありません。それでも、JavaやC#などの静的に型付けされたコードには依存関係の注入が不可欠です。

また 、オブジェクトの依存関係がすべて利用可能である場合、オブジェクトは実行時にのみその目的を適切に満たすことができるため、プロパティインジェクションを設定することはあまり役に立ちません。私の意見では、コンストラクターが呼び出されているときにすべての依存関係が満たされている必要があるため、コンストラクター注入が適切です。

お役に立てば幸いです。


4
それは実際には恐ろしい解決策のように見えます。私は間違いなくDaniel Prydenが提案するようなコードを記述しますが、その特定の単体テストでは、関数の前後にDateTime.Nowを実行して、時間が間にあるかどうかを確認しますか?インターフェイスを追加したり、コードをさらに多く追加したりすることは、私にはよくない考えのようです。

3
私は一般的なA(B)の例が好きではありません。ロガーが100の実装を持つ必要があるとは感じていません。これは私が最近遭遇した例であり、PostSharpを使用して実際に含めた、それを解決する5つの方法の1つです。これは、古典的なクラスベースのctorインジェクションアプローチを示しています。DIの適切な使用が発生した場所のより良い実例を提供できますか?
2013年

2
DIの良い使い方は見たことがありません。それが私が質問を書いた理由です。

2
役に立たなかった。私のコードはいつでも簡単にテストできます。DIは、不良コードのある大きなコードベースに適しているようです。

1
小さな関数型プログラムであっても、f(x)、g(f)がある場合は、すでに依存性注入を使用しているため、JSの各継続が依存性注入としてカウントされることに注意してください。私の推測では、あなたはすでにそれを使用していると思います;)
2013年

15

古典的な答えは、実行時にどの実装が使用されるかを知らない、より分離されたアプリケーションを作成することだと思います。

たとえば、私たちは中心的な支払いプロバイダーであり、世界中の多くの支払いプロバイダーと協力しています。ただし、リクエストが行われたとき、どの支払い処理業者に電話をかけるかわかりません。1つのクラスを次のような大量のスイッチケースでプログラムできます。

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

正しく分離されていないため、このすべてのコードを1つのクラスで維持する必要があると想像してください。サポートするすべての新しいプロセッサについて、新しい// if caseを作成する必要があると想像できます。どの方法でも、これはより複雑になるだけですが、依存性注入(または制御の反転-時々呼び出されるため、プログラムの実行を制御する人は実行時にのみ知られていて、複雑ではないことを意味します)を使用すると、何かを達成できます非常にきちんとしていて、保守可能です。

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

**コードはコンパイルされません、私は知っています:)


+1は、仮想メソッド/インターフェースを使用する場合に必要だと言っているように聞こえるためです。しかし、それはまだまれです。私はまだそれを渡したいnew ThatProcessor()というし、フレームワークを使用する

@ItaiSクラスファクトリーデザインパターンで無数のスイッチを回避できます。リフレクションSystem.Reflection.Assembly.GetExecutingAssembly()。CreateInstance()を使用します
domenicr

@domenicrもちろん!簡単な例で説明したかったのですが
板井サギ

ファクトリクラスの必要性を除いて、上記の説明に同意します。ファクトリクラスを実装する瞬間は、単純な選択です。ブルース・エルケルによる多相性と仮想関数の章で見つけた上記の最も良い説明。真のDIは選択から解放されている必要があり、オブジェクトタイプは実行時にインターフェイスを介して自動的に決定される必要があります。それも真の多態性の振る舞いです。
Arvind Krmar 2016年

たとえば、(c ++ごとに)基本クラスへの参照だけを受け取り、選択なしで派生クラスの動作を実装する共通のインターフェイスがあります。void tune(Instrument&i){i.play(middleC); } int main(){風笛; tune(フルート); インストゥルメントは基本クラスであり、風はそれから派生しています。C ++のように、仮想関数はこれを可能にして、共通インターフェースを介して派生クラスの動作を実装します。
Arvind Krmar 2016年

6

DIを使用する主な理由は、知識が存在する実装の知識に責任を持たせたいということです。DIの考え方は、カプセル化とインターフェイスによる設計に非常によく似ています。フロントエンドがバックエンドからデータを要求する場合、バックエンドがその質問をどのように解決するかはフロントエンドにとって重要ではありません。それはrequesthandler次第です。

これは、長い間OOPですでに一般的です。多くの場合、次のようなコードを作成します。

I_Dosomething x = new Impl_Dosomething();

欠点は、実装クラスがまだハードコーディングされているため、フロントエンドでどの実装が使用されているかがわかっていることです。DIは、インターフェースごとの設計をさらに一歩進め、フロントエンドが知る必要があるのはインターフェースの知識だけであるということです。DYIとDIの間にサービスロケーターのパターンがあります。フロントエンドがキーを提供する必要があるためです(サービスロケーターのレジストリに存在)、要求を解決できるようにします。サービスロケーターの例:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

DIの例:

I_Dosomething x = DIContainer.returnThat();

DIの要件の1つは、どのクラスがどのインターフェースの実装であるかをコンテナーが検出できる必要があることです。したがって、DIコンテナには、厳密に型指定された設計と、各インターフェイスに同時に1つの実装のみが必要です。同時にインターフェイスの実装がさらに必要な場合(計算機など)は、サービスロケーターまたはファクトリーデザインパターンが必要です。

D(b)I:インターフェースによる依存性注入と設計。ただし、この制限は実際にはそれほど大きな問題ではありません。D(b)Iを使用する利点は、クライアントとプロバイダー間の通信に役立つことです。インターフェースは、オブジェクトまたは一連の動作に関するパースペクティブです。ここでは後者が重要です。

コーディングでは、D(b)Iと共にサービス契約の管理を好みます。彼らは一緒に行くべきです。DIはカプセル化の追加のレイヤーにすぎないため、サービス契約を組織的に管理せずにD(b)Iを技術的ソリューションとして使用することは、私の見解ではあまり有益ではありません。しかし、それを組織管理と一緒に使用できる場合、D(b)Iが提供する組織原則を実際に利用できます。長期的には、テスト、バージョン管理、代替案の開発などのトピックでクライアントや他の技術部門とのコミュニケーションを構築するのに役立ちます。ハードコードされたクラスのように暗黙的なインターフェースがある場合、D(b)Iを使用して明示的にする場合よりも、時間の経過とともに通信がはるかに少なくなります。すべては、メンテナンスではなく、時間の経過によるものであり、一度にではありません。:-)


1
「欠点は、実装クラスがまだハードコードされていることです」<-ほとんどの場合、実装は1つしかなく、私が言ったように、まだ組み込まれていないインターフェイス(.NETを必要とするゲーム以外のコードは考えられません) )。

@ acidzombie24かもしれませんが...インターフェースが必要な場合は、最初からDIを使用してソリューションを実装する取り組みと、非DIソリューションを後で変更する取り組みを比較してください。ほとんどの場合、最初のオプションを選択します。明日100.000ドルを支払う代わりに、100ドルを支払う方が良いでしょう。
Golo Roden

1
@GoloRoden確かに、メンテナンスはD(b)Iのような手法を使用する重要な問題です。これは、アプリケーションのコストの80%です。最初からインターフェイスを使用して必要な動作を明示的にする設計により、組織は後で多くの時間と費用を節約できます。
Loek Bergman 2013

今のところ$ 0を支払い、今のところ$ 0を支払うだけでよいので、支払う必要があるまでは本当に理解できません。しかし、私はすべての行または関数をきれいに保つために$ 0.05を支払います。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.