c# - Warum benutzt man Dependency Injection?


Ich versuche zu verstehen, Dependency Injections (DI), und wieder einmal scheiterte ich. Es wirkt nur albern. Mein Code ist niemals ein Durcheinander. Ich schreibe kaum virtuelle Funktionen und Interfaces (obwohl ich es einmal in einem Blue Moon mache) und alle meine Konfigurationen werden magisch mit json.net in eine Klasse serialisiert (manchmal mit einem XML Serializer).

Ich verstehe nicht ganz, welches Problem es löst. Es sieht wie eine Weise aus, um zu sagen: "hallo. Wenn Sie in diese Funktion laufen, geben Sie ein Objekt zurück, das von diesem Typ ist und verwendet diese Parameter / Daten."
Aber ... warum sollte ich das jemals benutzen? Hinweis: Ich habe nie gebraucht object zu verwenden, aber ich verstehe, was das ist.

Was sind einige reale Situationen beim Entwerfen einer Website oder einer Desktop-Anwendung, in der man DI verwenden würde? Ich kann mit Fällen leicht kommen, warum jemand Interfaces / virtuelle Funktionen in einem Spiel verwenden möchte, aber es ist extrem selten (selten genug, dass ich mich an keine einzelne Instanz erinnern kann), um das in Nicht-Spiel-Code zu verwenden.


Answers


Zuerst möchte ich eine Annahme erklären, die ich für diese Antwort mache. Es ist nicht immer wahr, aber oft:

Schnittstellen sind Adjektive; Klassen sind Substantive.

(Eigentlich gibt es Schnittstellen, die auch Substantive sind, aber ich möchte hier verallgemeinern.)

So kann zB eine Schnittstelle so etwas wie IDisposable , IEnumerable oder IPrintable . Eine Klasse ist eine tatsächliche Implementierung einer oder mehrerer dieser Schnittstellen: List oder Map können beide Implementierungen von IEnumerable .

Um auf den Punkt zu kommen: Oft sind Ihre Klassen voneinander abhängig. ZB könnten Sie eine Database Klasse haben, die auf Ihre Datenbank zugreift (hah, surprise! ;-)), aber Sie möchten auch, dass diese Klasse über den Zugriff auf die Datenbank protokolliert. Angenommen, Sie haben einen anderen Klassen- Logger , dann hat die Database eine Abhängigkeit von Logger .

So weit, ist es gut.

Sie können diese Abhängigkeit in Ihrer Database Klasse mit der folgenden Zeile modellieren:

var logger = new Logger();

und alles ist gut. Bis zu dem Tag, an dem Sie merken, dass Sie eine Reihe von Loggern benötigen, ist es in Ordnung: Manchmal möchten Sie sich bei der Konsole anmelden, manchmal im Dateisystem, manchmal mit TCP / IP und einem entfernten Protokollierungsserver, und so weiter ...

Und natürlich möchten Sie nicht Ihren gesamten Code ändern (inzwischen haben Sie zig Millionen davon) und alle Zeilen ersetzen

var logger = new Logger();

durch:

var logger = new TcpLogger();

Erstens macht das keinen Spaß. Zweitens ist dies fehleranfällig. Drittens ist dies eine dumme, sich wiederholende Arbeit für einen trainierten Affen. Also, was machst du?

Offensichtlich ist es eine gute Idee, eine Schnittstelle ICanLog (oder ähnliches) ICanLog , die von allen verschiedenen Loggern implementiert wird. Also Schritt 1 in Ihrem Code ist, dass Sie tun:

ICanLog logger = new Logger();

Jetzt ändert der Typrückschluss den Typ nicht mehr, Sie haben immer eine einzige Schnittstelle, gegen die Sie sich entwickeln können. Der nächste Schritt ist, dass Sie keinen new Logger() immer und immer wieder haben wollen. Sie legen also die Zuverlässigkeit fest, um neue Instanzen in einer einzigen zentralen Factory-Klasse zu erstellen, und Sie erhalten Code wie:

ICanLog logger = LoggerFactory.Create();

Die Fabrik selbst entscheidet, welche Art von Logger erstellt werden soll. Ihr Code kümmert sich nicht mehr, und wenn Sie den Typ des verwendeten Loggers ändern möchten, ändern Sie ihn einmal : In der Fabrik.

Nun können Sie natürlich diese Fabrik verallgemeinern und für jeden Typ arbeiten lassen:

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

Irgendwo benötigt diese TypeFactory Konfigurationsdaten, die von der tatsächlichen Klasse instanziiert werden, wenn ein bestimmter Schnittstellentyp angefordert wird, so dass Sie eine Zuordnung benötigen. Natürlich können Sie dieses Mapping in Ihrem Code ausführen, aber dann bedeutet eine Typänderung das Rekompilieren. Sie könnten diese Zuordnung aber auch in eine XML-Datei einfügen, z. Dadurch können Sie die tatsächlich verwendete Klasse auch nach der Kompilierungszeit (!) Ändern, dh dynamisch, ohne erneutes Kompilieren!

Um Ihnen ein nützliches Beispiel dafür zu geben: Denken Sie an eine Software, die nicht normal protokolliert, aber wenn Ihr Kunde anruft und um Hilfe bittet, weil er ein Problem hat, ist alles, was Sie ihm schicken, eine aktualisierte XML-Konfigurationsdatei. Protokollierung aktiviert, und Ihre Unterstützung kann die Protokolldateien verwenden, um Ihren Kunden zu helfen.

Und jetzt, wenn Sie Namen ein wenig ersetzen, enden Sie mit einer einfachen Implementierung eines Service Locator , das eines von zwei Mustern für Inversion of Control ist (da Sie die Kontrolle darüber umkehren, wer entscheidet, welche exakte Klasse zu instanziieren ist).

Alles in allem reduziert dies Abhängigkeiten in Ihrem Code, aber jetzt ist Ihr gesamter Code von dem zentralen, einzelnen Service-Locator abhängig.

Dependency Injection ist nun der nächste Schritt in dieser Zeile: Befreien Sie sich von dieser einzigen Abhängigkeit zum Service Locator: Anstatt verschiedene Klassen den Service Locator nach einer Implementierung für eine bestimmte Schnittstelle zu fragen, können Sie wieder kontrollieren, wer was instanziiert ..

Mit der Dependency Injection hat Ihre Database Klasse jetzt einen Konstruktor, der einen Parameter vom Typ ICanLog :

public Database(ICanLog logger) { ... }

Jetzt hat Ihre Datenbank immer einen Logger zu verwenden, weiß aber nicht mehr, woher dieser Logger kommt.

Und hier kommt ein DI-Framework ins Spiel: Sie konfigurieren Ihre Mappings noch einmal und bitten dann Ihr DI-Framework, Ihre Anwendung für Sie zu instanziieren. Da die Application Klasse eine ICanPersistData Implementierung erfordert, wird eine Instanz von Database injiziert - aber dafür muss zuerst eine Instanz der Art von Logger erstellt werden, die für ICanLog konfiguriert ICanLog . Und so weiter ...

Um es kurz zu machen: Dependency Injection ist eine von zwei Möglichkeiten, um Abhängigkeiten in Ihrem Code zu entfernen. Es ist sehr nützlich für Konfigurationsänderungen nach der Kompilierung, und es ist eine großartige Sache für Unit-Tests (da es sehr einfach ist, Stubs und / oder Mocks zu injizieren).

In der Praxis gibt es Dinge, die Sie ohne einen Service Locator nicht tun können (zB wenn Sie nicht im Voraus wissen, wie viele Instanzen Sie eine bestimmte Schnittstelle benötigen: Ein DI-Framework injiziert immer nur eine Instanz pro Parameter, aber Sie können anrufen ein Service-Locator innerhalb einer Schleife, natürlich), daher stellt meistens jedes DI-Framework auch einen Service-Locator bereit.

Aber im Grunde ist es das.

Ich hoffe, das hilft.

PS: Was ich hier beschrieben habe, ist eine Technik, die Konstruktor-Injektion genannt wird. Es gibt auch eine Eigenschaftseinspritzung, bei der keine Konstruktorparameter, sondern Eigenschaften zum Definieren und Auflösen von Abhängigkeiten verwendet werden. Denken Sie an die Eigenschaftsinjektion als optionale Abhängigkeit und an die Konstruktorinjektion als obligatorische Abhängigkeiten. Die Diskussion darüber geht jedoch über den Rahmen dieser Frage hinaus.




Ich denke, die Leute sind oft verwirrt über den Unterschied zwischen Dependency Injection und einem Dependency Injection Framework (oder einem Container, wie er oft genannt wird).

Dependency Injection ist ein sehr einfaches Konzept. Anstelle dieses Codes:

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

Sie schreiben Code wie folgt:

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

Und das ist es. Ernst. Das gibt Ihnen eine Menge Vorteile. Zwei wichtige sind die Fähigkeit, die Funktionalität von einem zentralen Ort aus zu steuern (die Funktion Main() ) anstatt sie in Ihrem Programm zu verbreiten und die Fähigkeit, jede Klasse isoliert zu testen (weil Sie Mocks oder andere gefälschte Objekte in sein Konstruktor anstelle eines realen Wertes).

Der Nachteil ist natürlich, dass Sie jetzt eine Mega-Funktion haben, die alle von Ihrem Programm verwendeten Klassen kennt. Darum können DI-Frameworks helfen. Wenn Sie jedoch nicht verstehen, warum dieser Ansatz sinnvoll ist, sollten Sie zunächst mit der manuellen Dependency Injection beginnen, damit Sie besser verstehen können, was die verschiedenen Frameworks für Sie tun können.




Wie die anderen Antworten sagten, ist die Dependency Injection eine Möglichkeit, Abhängigkeiten außerhalb der Klasse zu erzeugen, die sie verwendet. Du injizierst sie von außen und übernimmst die Kontrolle über ihre Schöpfung aus dem Inneren deiner Klasse. Deswegen ist die Dependency Injection eine Realisierung des Inversion of Control (IoC) -Prinzips.

IoC ist das Prinzip, wobei DI das Muster ist. Der Grund, warum Sie "mehr als einen Logger brauchen", ist meines Wissens nach nie wirklich erfüllt, aber der eigentliche Grund ist, dass Sie ihn wirklich brauchen, wenn Sie etwas testen. Ein Beispiel:

Meine Eigenschaft:

Wenn ich mir ein Angebot anschaue, möchte ich darauf hinweisen, dass ich es automatisch angeschaut habe, damit ich es nicht vergesse.

Sie könnten dies folgendermaßen testen:

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

Also irgendwo im OfferWeasel , baut es dir ein Angebot Objekt wie OfferWeasel :

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

Das Problem hier ist, dass dieser Test höchstwahrscheinlich immer scheitern wird, da das Datum, das eingestellt wird, vom Datum DateTime.Now , das behauptet wird, selbst wenn Sie nur DateTime.Now im DateTime.Now setzen, es könnte um ein paar Millisekunden aus sein und wird daher immer scheitern. Eine bessere Lösung wäre jetzt, eine Schnittstelle dafür zu erstellen, mit der Sie steuern können, welche Zeit eingestellt wird:

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

Die Schnittstelle ist die Abstraktion. Einer ist die ECHTE Sache, und der andere erlaubt Ihnen, etwas Zeit zu fälschen, wo es gebraucht wird. Der Test kann dann wie folgt geändert werden:

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

Auf diese Weise haben Sie das Prinzip der "Inversion of Control" angewendet, indem Sie eine Abhängigkeit injiziert haben (wobei die aktuelle Zeit ermittelt wurde). Der Hauptgrund dafür ist das einfachere Testen von isolierten Einheiten. Es gibt andere Möglichkeiten, dies zu tun. Zum Beispiel ist eine Schnittstelle und eine Klasse hier unnötig, da in C # -Funktionen als Variablen übergeben werden können, so dass anstelle einer Schnittstelle ein Func<DateTime> , um dasselbe zu erreichen. Wenn Sie einen dynamischen Ansatz wählen, übergeben Sie einfach ein beliebiges Objekt mit der entsprechenden Methode ( Enten-Typisierung ), und Sie benötigen überhaupt keine Schnittstelle.

Sie werden kaum mehr als einen Logger benötigen. Dennoch ist die Abhängigkeitseinleitung für statisch typisierten Code wie Java oder C # von wesentlicher Bedeutung.

Und ... Es sollte auch beachtet werden, dass ein Objekt seinen Zweck zur Laufzeit nur dann richtig erfüllen kann, wenn alle Abhängigkeiten verfügbar sind, so dass es wenig Sinn macht, die Eigenschaftseinspritzung einzurichten. Meiner Meinung nach sollten alle Abhängigkeiten erfüllt sein, wenn der Konstruktor aufgerufen wird, also ist Konstruktor-Einspritzung das Ding, zum mit zu gehen.

Ich hoffe das hat geholfen.




Ich denke, die klassische Antwort besteht darin, eine entkoppelte Anwendung zu erstellen, die nicht weiß, welche Implementierung zur Laufzeit verwendet wird.

Wir sind zum Beispiel ein zentraler Zahlungsanbieter und arbeiten mit vielen Zahlungsanbietern auf der ganzen Welt zusammen. Wenn jedoch eine Anfrage gestellt wird, habe ich keine Ahnung, welchen Zahlungsprozessor ich anrufen werde. Ich könnte eine Klasse mit einer Tonne Switch-Cases programmieren, wie zum Beispiel:

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

Stellen Sie sich jetzt vor, dass Sie diesen gesamten Code in einer einzigen Klasse verwalten müssen, da er nicht richtig entkoppelt ist. Sie können sich vorstellen, dass Sie für jeden neuen Prozessor, den Sie unterstützen, einen neuen if // switch-Fall erstellen müssen. Jede Methode, dies wird jedoch nur komplizierter, wenn man Dependency Injection (oder Inversion of Control - wie es manchmal genannt wird, was bedeutet, dass derjenige, der die Ausführung des Programms steuert, nur zur Laufzeit und nicht als Komplikation kennt) verwendet, könnte man etwas erreichen. sehr gepflegt und wartungsfreundlich.

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

** Der Code wird nicht kompiliert, ich weiß :)




Der Hauptgrund für die Verwendung von DI liegt darin, dass Sie die Verantwortung für das Wissen über die Implementierung dort setzen wollen, wo das Wissen vorhanden ist. Die Idee von DI ist sehr eng mit der Kapselung und dem Design durch die Schnittstelle verbunden. Wenn das Frontend vom Backend nach einigen Daten fragt, ist es für das Frontend unwichtig, wie das Backend diese Frage löst. Das ist Sache des Requesthandlers.

Das ist in OOP schon lange üblich. Viele Male erstellen Sie Code-Teile wie:

I_Dosomething x = new Impl_Dosomething();

Der Nachteil ist, dass die Implementierungsklasse immer noch hartcodiert ist, daher hat das Frontend das Wissen, welche Implementierung verwendet wird. DI geht das Design per Interface einen Schritt weiter, dass das Frontend nur das Wissen der Schnittstelle wissen muss. Zwischen dem DYI und dem DI befindet sich das Muster eines Dienstlokalisierers, da das Frontend einen Schlüssel (in der Registrierung des Dienstlokalisierers vorhanden) bereitstellen muss, damit seine Anforderung aufgelöst werden kann. Service-Locator-Beispiel:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

DI Beispiel:

I_Dosomething x = DIContainer.returnThat();

Eine der Anforderungen von DI ist, dass der Container in der Lage sein muss herauszufinden, welche Klasse die Implementierung welcher Schnittstelle ist. Daher erfordert ein DI-Container stark typisiertes Design und nur eine Implementierung für jede Schnittstelle gleichzeitig. Wenn Sie mehrere Implementierungen einer Schnittstelle gleichzeitig benötigen (wie ein Taschenrechner), benötigen Sie den Service Locator oder das Factory Design Pattern.

D (b) I: Dependency Injection und Design by Interface. Diese Einschränkung ist jedoch kein sehr großes praktisches Problem. Der Vorteil der Verwendung von D (b) I besteht darin, dass es der Kommunikation zwischen dem Client und dem Anbieter dient. Eine Schnittstelle ist eine Perspektive auf ein Objekt oder eine Reihe von Verhalten. Letztere ist hier entscheidend.

Ich bevorzuge die Verwaltung von Dienstleistungsaufträgen zusammen mit D (b) I in der Codierung. Sie sollten zusammen gehen. Die Verwendung von D (b) I als technische Lösung ohne organisatorische Verwaltung von Serviceverträgen ist aus meiner Sicht wenig vorteilhaft, da DI dann nur eine zusätzliche Verkapselungsebene darstellt. Aber wenn man es zusammen mit der organisatorischen Verwaltung verwenden kann, kann man wirklich das Organisationsprinzip D (b) nutzen, das ich anbiete. Es kann Ihnen auf lange Sicht helfen, die Kommunikation mit dem Kunden und anderen technischen Abteilungen in Themen wie Testen, Versionieren und die Entwicklung von Alternativen zu strukturieren. Wenn Sie eine implizite Schnittstelle wie in einer hartcodierten Klasse haben, dann ist sie mit der Zeit viel weniger kommunizierbar, wenn Sie sie mit D (b) I explizit machen. Es läuft alles auf Wartung hinaus, die im Laufe der Zeit und nicht zu einer Zeit ist. :-)