web-dev-qa-db-ja.com

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

依存性注入 (DI)を理解しようとしていますが、もう一度失敗しました。ばかげているようです。私のコードは決して混乱しません。私は仮想関数やインターフェースを書くことはほとんどありませんが(私は一度でもやっていますが)、私の設定はすべてjson.netを使って魔法のようにクラスにシリアライズされています。

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

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

505
user34537

最初に、私がこの答えに対してする仮定を説明したいと思います。それは必ずしも真実ではありませんが、かなり頻繁に:

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

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

だから、例えば。インターフェースはIDisposableIEnumerableIPrintableのようなものです。クラスは、これらのインタフェースのうちの1つ以上の実際の実装です。ListまたはMapは、両方ともIEnumerableの実装です。

要するに:あなたのクラスはお互いに依存していることが多いです。例えば。あなたはあなたのデータベースにアクセスするDatabaseクラスを持つことができます(ハァッ、サプライズ!;-))が、あなたはまたこのクラスにデータベースへのアクセスについてのログ記録をさせたいです。別のクラスLoggerがあり、DatabaseLoggerに依存しているとします。

ここまでは順調ですね。

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

var logger = new Logger();

そしてすべてが大丈夫です。それはあなたがたくさんのロガーを必要としていることに気づいた日までは罰金です:時にはあなたは時々TCP/IPとリモートロギングサーバを使って、そして時々ファイルシステムへとコンソールにログを取りたいです、...

そしてもちろんあなたはNOTあなたのすべてのコードを変更したい(その間にあなたはそれを見ている)そして全ての行を置き換えたい

var logger = new Logger();

によって:

var logger = new TcpLogger();

まず、これは楽しいことではありません。第二に、これは間違いを起こしやすいです。第三に、これは訓練された猿のための愚かな、反復的な作業です。それで、あなたは何をしますか?

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

ICanLog logger = new Logger();

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

ICanLog logger = LoggerFactory.Create();

工場自体がどの種類のロガーを作成するかを決定します。あなたのコードはもはや気にしません、そしてあなたが使用されているロガーのタイプを変更したいならば、あなたはそれを変更しますonce:ファクトリーの中。

さて、もちろん、あなたはこのファクトリを一般化して、それをどんなタイプにも使えるようにすることができます:

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

このTypeFactoryのどこかで、特定のインターフェースタイプが要求されたときにインスタンス化する実際のクラスの設定データが必要なので、マッピングが必要です。もちろん、あなたはあなたのコードの中でこのマッピングをすることができます、しかしそれからタイプ変更は再コンパイルを意味します。しかし、このマッピングをXMLファイルの中に入れることもできます。例えば、コンパイル時(!)の後でも、実際に使用されているクラスを変更することができます。

このような場合の便利な例を挙げましょう。通常はログに記録されないが、問題があるために顧客が電話をかけて助けを求めるソフトウェアを考えてみましょう。ログ記録が有効になっており、サポート担当者がログファイルを使用して顧客を支援できます。

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

これらすべてをまとめると、コード内の依存関係が軽減されますが、すべてのコードは中央の単一のサービスロケータに依存するようになります。

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

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

public Database(ICanLog logger) { ... }

今すぐあなたのデータベースは常に使用するロガーを持っています、しかしそれはこのロガーがどこから来るのかをもう知りません。

そして、これがDIフレームワークが登場するところです:あなたはもう一度あなたのマッピングを設定し、そしてあなたのためにあなたのアプリケーションをインスタンス化するようにあなたのDIフレームワークに依頼します。 ApplicationクラスはICanPersistDataの実装を必要とするので、Databaseのインスタンスが注入されます - しかしそのためにはまずICanLog用に設定された種類のロガーのインスタンスを作成しなければなりません。等々 ...

だから、長い話を簡単に言えば、依存性注入は、コード内の依存性を除去する方法の2つの方法のうちの1つです。これはコンパイル時の設定変更に非常に役立ち、単体テストには非常に便利です(スタブやモックの挿入が非常に簡単になるため)。

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

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

それが役立つことを願っています。

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

812
Golo Roden

依存性注入 と依存性注入 framework (または container と呼ばれることが多いので)の違いについて、人々は混乱することが多いと思います。

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

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フレームワークが役立つことです。しかし、なぜこのアプローチが価値があるのか​​理解できない場合は、まず手動の依存性注入から始めることをお勧めします。そうすれば、さまざまなフレームワークがあなたに役立つことを理解することができます。

479
Daniel Pryden

他の答えが述べたように、依存性注入はそれを使用するクラスの外にあなたの依存性を作成する方法です。あなたはそれらを外側から注入し、そしてあなたのクラスの内側から離れて彼らの創造について制御を取ります。これが、依存性注入が 制御の反転 (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;
    }
}

インタフェースは抽象化です。一つは本当のことです、そしてもう一つはそれが必要とされるところであなたがしばらく偽物をすることを可能にします。テストは次のように変更できます。

[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#などの静的型付きコードには不可欠です。

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

それが役に立ったと思います。

35
cessor

私は古典的な答えはもっと分離されたアプリケーションを作成することだと思います。そして、それは実行時にどの実装が使われるかについての知識を持っていません。

たとえば、私たちは世界中の多くの支払いプロバイダと協力している、中央の支払いプロバイダです。ただし、要求があったときに、どの支払い処理会社に電話をかけるのかわかりません。以下のような大量のスイッチケースで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;
        }
    }
}

今、あなたは今それが適切に分離されていないので単一のクラスですべてのこれらのコードを維持する必要があると想像することができるでしょう、あなたがサポートするすべての新しいプロセッサのためにどのメソッドでも、これはより複雑になりますが、Dependency Injection(またはInversion of Control - プログラムの実行を制御するユーザーは実行時にのみ認識され、複雑ではないことを意味します)を使用して非常にきちんとしていて保守可能です。

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();
}

**コードがコンパイルされない、私は知っている:)

12
Itai Sagi

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

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

I_Dosomething x = new Impl_Dosomething();

欠点は、実装クラスがまだハードコードされているため、フロントエンドにどの実装が使用されているかという知識があることです。フロントエンドが知る必要があるのはインターフェースの知識だけであるということです。 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と一緒にサービス契約の管理を好む。彼らは一緒に行くべきです。サービス契約を組織的に管理しないで技術的な解決策としてD(b)Iを使用することは、DIがカプセル化の単なる追加層になるため、私の観点からはあまり有益ではありません。しかし、それを組織の管理と一緒に使用できる場合は、私が提供している組織化原則D(b)を実際に利用できます。長期的に見れば、テスト、バージョン管理、代替手段の開発などのトピックで、クライアントや他の技術部門とのコミュニケーションを構造化するのに役立ちます。ハードコードされたクラスのように暗黙のインタフェースを持っているとき、それは時間が経つにつれてそれほど通信できなくなります、それからあなたがそれをD(b)Iを使って明示的にするとき。それはすべてメンテナンスに煮詰まります。 :-)

6
Loek Bergman