it-swarm.com.de

Warum benutzt man Dependency Injection?

Ich versuche zu verstehen Abhängigkeitsinjektionen (DI), und ich habe erneut versagt. Es scheint nur albern. Mein Code ist nie ein Durcheinander; Ich schreibe kaum virtuelle Funktionen und Interfaces (obwohl ich es einmal in einem blauen Mond getan habe) und meine gesamte Konfiguration wird mithilfe von json.net (manchmal mithilfe eines XML-Serializers) magisch in eine Klasse serialisiert.

Ich verstehe nicht ganz, welches Problem es löst. Es sieht so aus, als würde man sagen: "hi. Wenn Sie diese Funktion aufrufen, geben Sie ein Objekt dieses Typs zurück, das diese Parameter/Daten verwendet."
Aber ... warum sollte ich das jemals benutzen? Hinweis Ich musste object noch nie verwenden, aber ich verstehe, wofür das ist.

Was sind einige reale Situationen beim Erstellen einer Website oder einer Desktopanwendung, in denen DI verwendet wird? Ich kann mir leicht Fälle ausdenken, warum jemand Schnittstellen/virtuelle Funktionen in einem Spiel verwenden möchte, aber es ist äußerst selten (selten genug, dass ich mich nicht an eine einzelne Instanz erinnern kann), diese in Nicht-Spiel-Code zu verwenden.

510
user34537

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

Schnittstellen sind Adjektive; Klassen sind Substantive.

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

So kann z.B. Eine Schnittstelle kann beispielsweise IDisposable, IEnumerable oder IPrintable sein. Eine Klasse ist eine tatsächliche Implementierung einer oder mehrerer dieser Schnittstellen: List oder Map können beide Implementierungen von IEnumerable sein.

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

So weit, ist es gut.

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

var logger = new Logger();

und alles ist gut. Bis zu dem Tag, an dem Sie feststellen, dass Sie eine Reihe von Loggern benötigen: Manchmal möchten Sie sich an der Konsole anmelden, manchmal im Dateisystem, manchmal über TCP/IP und einen Remote-Protokollierungsserver usw.

Und natürlich möchten Sie NICHT Ihren gesamten Code ändern (in der Zwischenzeit haben Sie Unmengen 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 ausgebildeten Affen. Also, was machst du?

Offensichtlich ist es eine gute Idee, ein Interface ICanLog (oder ähnliches) einzuführen, das von allen verschiedenen Loggern implementiert wird. Schritt 1 in Ihrem Code ist also, dass Sie Folgendes tun:

ICanLog logger = new Logger();

Jetzt ändert sich die Typinferenz nicht mehr, Sie müssen immer eine einzige Schnittstelle entwickeln. Der nächste Schritt ist, dass Sie nicht immer und immer wieder new Logger() haben möchten. Sie haben also die Zuverlässigkeit, neue Instanzen in einer einzigen zentralen Factory-Klasse zu erstellen, und erhalten Code wie den folgenden:

ICanLog logger = LoggerFactory.Create();

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

Jetzt können Sie diese Factory natürlich verallgemeinern und für jeden Typ verwenden:

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

Irgendwo in dieser TypeFactory sind Konfigurationsdaten erforderlich, deren tatsächliche Klasse instanziiert werden soll, wenn ein bestimmter Schnittstellentyp angefordert wird. Sie benötigen daher ein Mapping. Natürlich können Sie diese Zuordnung in Ihrem Code vornehmen, aber dann bedeutet eine Änderung des Typs eine Neukompilierung. Sie können dieses Mapping aber auch in eine XML-Datei einfügen, z. B.. Auf diese Weise können Sie die tatsächlich verwendete Klasse auch nach der Kompilierungszeit (!) Ändern, dh dynamisch, ohne erneut zu kompilieren!

Ein nützliches Beispiel hierfür: Stellen Sie sich eine Software vor, die nicht normal protokolliert. Wenn Ihr Kunde jedoch anruft und um Hilfe bittet, weil er ein Problem hat, senden Sie ihm lediglich eine aktualisierte XML-Konfigurationsdatei Die Protokollierung ist aktiviert, und Ihr Support kann die Protokolldateien verwenden, um Ihrem Kunden zu helfen.

Und jetzt, wenn Sie Namen ein wenig ersetzen, erhalten Sie eine einfache Implementierung von Service Locator, einem von zwei Mustern für Inversion of Control (seit Sie) Kontrolle darüber umkehren, wer genau entscheidet, welche Klasse instanziiert werden soll.

Alles in allem werden dadurch die Abhängigkeiten in Ihrem Code verringert, aber jetzt ist Ihr gesamter Code vom zentralen, einzelnen Service-Locator abhängig.

Dependency Injection ist nun der nächste Schritt in dieser Zeile: Beseitigen Sie einfach diese einzige Abhängigkeit zum Service Locator: Anstatt verschiedene Klassen den Service Locator nach einer Implementierung für eine bestimmte Schnittstelle zu fragen, müssen Sie - noch einmal - die Kontrolle darüber zurückgeben, wer was instanziiert.

Mit der Abhängigkeitsinjektion verfügt Ihre Klasse Database jetzt über einen Konstruktor, für den ein Parameter vom Typ ICanLog erforderlich ist:

public Database(ICanLog logger) { ... }

Jetzt hat Ihre Datenbank immer einen Logger zur Verfügung, aber sie weiß nicht mehr, woher dieser Logger kommt.

Und hier kommt ein DI-Framework ins Spiel: Sie konfigurieren Ihre Zuordnungen erneut und fordern dann Ihr DI-Framework auf, Ihre Anwendung für Sie zu instanziieren. Da die Klasse Application eine Implementierung von ICanPersistData erfordert, wird eine Instanz von Database injiziert. Dazu muss jedoch zuerst eine Instanz des Protokollierertyps erstellt werden, der für ICanLog konfiguriert ist. Und so weiter ...

Um es kurz zu machen: Die Abhängigkeitsinjektion 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 eignet sich hervorragend für Komponententests (da es sehr einfach ist, Stubs und/oder Mocks einzufügen).

In der Praxis gibt es Dinge, die Sie ohne einen Service Locator nicht tun können (z. B. wenn Sie nicht im Voraus wissen, wie viele Instanzen Sie für eine bestimmte Schnittstelle benötigen: Ein DI-Framework injiziert immer nur eine Instanz pro Parameter, aber Sie können sie aufrufen natürlich ein Service-Locator innerhalb einer Schleife), 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 namens Konstruktorinjektion, es gibt auch Eigenschaftsinjektion, bei der keine Konstruktorparameter, sondern Eigenschaften zum Definieren und Auflösen von Abhängigkeiten verwendet werden. Stellen Sie sich die Eigenschaftsinjektion als optionale Abhängigkeit und die Konstruktorinjektion als obligatorische Abhängigkeit vor. Eine Diskussion darüber würde den Rahmen dieser Frage sprengen.

820
Golo Roden

Ich denke, viele Leute sind verwirrt über den Unterschied zwischen Abhängigkeitsinjektion und einem Abhängigkeitsinjektions Framework (oder einem container wie es oft genannt wird).

Die Abhängigkeitsinjektion 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();
}

nd das war's. Ernsthaft. Dies 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 im gesamten Programm zu verbreiten, und die Fähigkeit, jede Klasse einfacher für sich zu testen (weil Sie Mocks oder andere gefälschte Tests bestehen können) Objekte in seinen Konstruktor (anstelle eines realen Wertes).

Der Nachteil ist natürlich, dass Sie jetzt eine Mega-Funktion haben, die alle Klassen kennt, die von Ihrem Programm verwendet werden. Das ist es, womit DI-Frameworks helfen können. Wenn Sie jedoch Schwierigkeiten haben zu verstehen, warum dieser Ansatz von Nutzen ist, sollten Sie zunächst mit der manuellen Abhängigkeitsinjektion beginnen, damit Sie besser einschätzen können, was die verschiedenen Frameworks für Sie tun können.

485
Daniel Pryden

Wie in den anderen Antworten angegeben, ist die Abhängigkeitsinjektion eine Möglichkeit, Abhängigkeiten außerhalb der Klasse zu erstellen, die sie verwendet. Sie injizieren sie von außen und übernehmen die Kontrolle über ihre Kreation außerhalb Ihrer Klasse. Dies ist auch der Grund, warum die Abhängigkeitsinjektion eine Verwirklichung des Inversion of Control (IoC) -Prinzips ist.

IoC ist das Prinzip, wobei DI das Muster ist. Der Grund, dass Sie "mehr als einen Logger benötigen", ist meines Erachtens eigentlich nie gegeben, aber der eigentliche Grund ist, dass Sie ihn wirklich brauchen, wenn Sie etwas testen. Ein Beispiel:

Meine Funktion:

Wenn ich mir ein Angebot ansehe, möchte ich markieren, dass ich es automatisch angesehen habe, damit ich es nicht vergesse.

Sie könnten dies so 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));
}

Irgendwo in OfferWeasel erstellt es Ihnen ein Angebotsobjekt wie das folgende:

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

Das Problem hierbei ist, dass dieser Test höchstwahrscheinlich immer fehlschlägt, da das Datum, das festgelegt wird, von dem Datum abweicht, das bestätigt wird, auch wenn Sie nur DateTime.Now in den Testcode eingeben, der möglicherweise von einem Paar verschoben wurde von Millisekunden 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. Eines ist das ECHTE, und das andere ermöglicht es Ihnen, einige Zeit zu fälschen, wo es benötigt wird. Der Test kann dann folgendermaßen 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 "Umkehrung der Kontrolle" angewendet, indem Sie eine Abhängigkeit injiziert haben (um die aktuelle Zeit zu erhalten). Der Hauptgrund dafür ist das leichtere Testen von isolierten Einheiten. Es gibt auch andere Möglichkeiten, dies zu tun. Eine Schnittstelle und eine Klasse sind hier beispielsweise nicht erforderlich, da in C # Funktionen als Variablen übergeben werden können, sodass Sie anstelle einer Schnittstelle einen Func<DateTime> verwenden können, um dasselbe zu erreichen. Wenn Sie dynamisch vorgehen, übergeben Sie einfach ein Objekt mit der entsprechenden Methode ( duck typing ), und Sie benötigen überhaupt keine Schnittstelle.

Sie werden kaum mehr als einen Logger benötigen. Für statisch typisierten Code wie Java oder C # ist die Abhängigkeitsinjektion jedoch unerlässlich.

Und ... Es sollte auch beachtet werden, dass ein Objekt seinen Zweck zur Laufzeit nur dann ordnungsgemäß erfüllen kann, wenn alle Abhängigkeiten verfügbar sind Viel Gebrauch beim Einrichten der Immobilieninjektion. Meiner Meinung nach sollten alle Abhängigkeiten erfüllt sein, wenn der Konstruktor aufgerufen wird, daher ist Konstruktorinjektion die richtige Wahl.

Ich hoffe das hat geholfen.

35
cessor

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

Zum Beispiel sind wir ein zentraler Zahlungsanbieter und arbeiten mit vielen Zahlungsanbietern auf der ganzen Welt zusammen. Wenn jedoch eine Anfrage gestellt wird, habe ich keine Ahnung, welchen Zahlungsabwickler ich anrufen werde. Ich könnte eine Klasse mit einer Menge Schalterkästen 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 nun vor, dass Sie den gesamten Code in einer einzelnen Klasse verwalten müssen, da er nicht ordnungsgemäß entkoppelt ist. Sie können sich vorstellen, dass Sie für jeden neuen Prozessor, den Sie unterstützen, eine neue if // switch-Schutzhülle für erstellen müssen Bei jeder Methode wird dies nur noch komplizierter. Durch die Verwendung von Dependency Injection (oder Inversion of Control - wie es manchmal genannt wird, dh wer die Ausführung des Programms steuert, ist nur zur Laufzeit bekannt und keine Komplikation) können Sie jedoch etwas erreichen sehr ordentlich und wartbar.

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ß :)

12
Itai Sagi

Der Hauptgrund für die Verwendung von DI ist, dass Sie die Verantwortung für das Wissen der Implementierung dort ablegen möchten, wo das Wissen vorhanden ist. Die Idee von DI passt sehr gut zur Kapselung und zum Design per Schnittstelle. Wenn das Front-End vom Back-End nach Daten fragt, ist es für das Front-End unwichtig, wie das Back-End diese Frage löst. Das liegt beim Requesthandler.

Das ist in OOP schon lange üblich. Oftmals werden Code-Teile erstellt wie:

I_Dosomething x = new Impl_Dosomething();

Der Nachteil ist, dass die Implementierungsklasse immer noch fest codiert ist und das Front-End daher weiß, welche Implementierung verwendet wird. DI geht das Design über die Benutzeroberfläche noch einen Schritt weiter. Das einzige, was das Front-End wissen muss, ist das Wissen über die Benutzeroberfläche. Zwischen DYI und DI befindet sich das Muster eines Service-Locators, da das Front-End einen Schlüssel (in der Registrierung des Service-Locators 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 ein stark typisiertes Design und gleichzeitig nur eine Implementierung für jede Schnittstelle. Wenn Sie mehrere Implementierungen einer Schnittstelle gleichzeitig benötigen (z. B. einen Taschenrechner), benötigen Sie den Service Locator oder das Factory Design Pattern.

D (b) I: Abhängigkeitsinjektion und Entwurf durch Schnittstelle. Diese Einschränkung ist jedoch kein sehr großes praktisches Problem. Der Vorteil von D (b) I ist, dass es der Kommunikation zwischen dem Kunden und dem Anbieter dient. Eine Schnittstelle ist eine Perspektive auf ein Objekt oder eine Reihe von Verhaltensweisen. Letzteres ist hier entscheidend.

Ich bevorzuge die Verwaltung von Serviceverträgen zusammen mit D (b) I bei 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 nicht sehr vorteilhaft, da DI dann nur eine zusätzliche Verkapselungsschicht ist. Aber wenn Sie es zusammen mit der Organisationsverwaltung verwenden können, können Sie das Organisationsprinzip D (b), das ich anbiete, wirklich anwenden. Es kann Ihnen langfristig helfen, die Kommunikation mit dem Kunden und anderen technischen Abteilungen in Themen wie Testen, Versionieren und Entwickeln von Alternativen zu strukturieren. Wenn Sie eine implizite Schnittstelle wie in einer fest codierten Klasse haben, ist sie im Laufe der Zeit viel weniger kommunizierbar als wenn Sie sie mit D (b) I explizit machen. Es läuft alles auf eine Wartung hinaus, die im Laufe der Zeit und nicht auf einmal erfolgt. :-)

6
Loek Bergman