583

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

それがどのような問題を解決するのかよくわかりません。「こんにちは。この関数に遭遇したら、このタイプのオブジェクトを返し、これらのパラメーター/データを使用します。」という言い方のように見えます。
しかし... なぜ私はそれを使うのでしょうか? object同様に使用する必要はありませんが、その目的は理解しています。

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

4

6 に答える 6

892

まず、この回答のために私が立てた仮定を説明したいと思います。それは常に正しいとは限りませんが、かなり頻繁に:

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

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

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

ポイントを理解するには: 多くの場合、クラスは互いに依存しています。たとえば、データベースにアクセスするクラスを作成することもできますがDatabase(ハァッ、驚き! ;-))、このクラスでデータベースへのアクセスに関するログも記録する必要があります。別のクラスLoggerDatabaseあり、依存関係があるとし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の単純な実装になります。これは、制御の反転の 2 つのパターンのうちの 1 つです(インスタンス化する正確なクラスを誰が決定するかについて制御を反転するため)。

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

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

依存性注入により、Databaseクラスには type のパラメーターを必要とするコンストラクターが含まれるようになりましたICanLog

public Database(ICanLog logger) { ... }

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

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

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

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

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

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

于 2013-01-13T07:21:33.747 に答える
548

多くの場合、依存性注入と依存性注入フレームワーク(またはコンテナーと呼ばれることが多い)の違いについて混乱する人が多いと思います。

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

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 フレームワークが支援できることです。しかし、なぜこのアプローチが価値があるのか​​ 理解できない場合は、最初に手動の依存性注入から始めることをお勧めします.

于 2013-01-13T08:22:29.607 に答える
40

他の回答が述べたように、依存性注入は、それを使用するクラスの外部に依存性を作成する方法です。それらを外部から注入し、クラスの内部からそれらの作成を制御します。これが、依存性注入が制御の反転(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 つは REAL のもので、もう 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# では関数を変数として渡すことができるため、ここでのインターフェイスとクラスは不要です。したがって、インターフェイスの代わりに a を使用しFunc<DateTime>て同じことを実現できます。または、動的なアプローチを取る場合は、同等のメソッド (ダック タイピング) を持つオブジェクトを渡すだけで、インターフェイスはまったく必要ありません。

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

そして... オブジェクトは、すべての依存関係が利用可能である場合にのみ、実行時にその目的を適切に果たすことができるため、プロパティ注入の設定にはあまり使用されないことにも注意してください。私の意見では、コンストラクターが呼び出されたときにすべての依存関係が満たされている必要があるため、constructor-injection を使用することをお勧めします。

于 2013-01-13T12:20:09.370 に答える
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 // switch ケースを作成する必要があると想像できます。すべてのメソッドでは、これはより複雑になりますが、依存性注入 (または制御の反転 - 時々呼ばれるように、プログラムの実行を制御する人は実行時にのみわかり、複雑ではありません) を使用すると、何かを達成できます。非常にきれいで保守可能。

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

** コードがコンパイルされないことはわかっています:)

于 2013-01-13T14:01:14.187 に答える
7

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

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

于 2013-01-13T09:46:12.657 に答える