Warum verwendet man Abhängigkeitsinjektion?

536

Ich versuche, Abhängigkeitsinjektionen (DI) zu verstehen , und bin erneut gescheitert. Es scheint nur albern. Mein Code ist niemals ein Chaos. Ich schreibe kaum virtuelle Funktionen und Schnittstellen (obwohl ich es einmal in einem blauen Mond mache) und meine gesamte Konfiguration wird mit json.net (manchmal mit einem XML-Serializer) auf magische Weise in eine Klasse serialisiert.

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

Was sind einige reale Situationen beim Erstellen einer Website oder einer Desktop-Anwendung, in der DI verwendet wird? Ich kann mir leicht Fälle einfallen lassen, 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), dies in Nicht-Spiel-Code zu verwenden.

TJ Crowder
quelle
3
Dies kann auch eine nützliche Information sein: martinfowler.com/articles/injection.html
ta.speot.is
1
Siehe auch .
BlueRaja - Danny Pflughoeft
41
Ich bin mit diesem Typen zusammen: jamesshore.com/Blog/Dependency-Injection-Demystified.html .
Eric Lippert
5
Eine weitere sehr einfache Erklärung von DI: codearsenal.net/2015/03/…
ybonda

Antworten:

840

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 eine Schnittstelle zum Beispiel etwas wie IDisposable, IEnumerableoder IPrintable. Eine Klasse ist eine tatsächliche Implementierung einer oder mehrerer dieser Schnittstellen: Listoder Mapbeide können Implementierungen von sein IEnumerable.

Um es auf den Punkt zu bringen: Oft hängen Ihre Klassen voneinander ab. Sie könnten beispielsweise eine DatabaseKlasse haben, die auf Ihre Datenbank zugreift (hah, Überraschung! ;-)), aber Sie möchten auch, dass diese Klasse den Zugriff auf die Datenbank protokolliert. Angenommen, Sie haben eine andere Klasse Loggerund haben dann Databaseeine Abhängigkeit von Logger.

So weit, ist es gut.

Sie können diese Abhängigkeit innerhalb Ihrer DatabaseKlasse mit der folgenden Zeile modellieren :

var logger = new Logger();

und alles ist gut. Es ist bis zu dem Tag in Ordnung, an dem Sie feststellen, dass Sie eine Reihe von Protokollierern benötigen: Manchmal möchten Sie sich an der Konsole, manchmal am Dateisystem, manchmal mit TCP / IP und einem Remote-Protokollierungsserver usw. anmelden ...

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, eine Schnittstelle ICanLog(oder ähnliches) einzuführen , die von allen verschiedenen Loggern implementiert wird. Schritt 1 in Ihrem Code ist also, dass Sie Folgendes tun:

ICanLog logger = new Logger();

Jetzt ändert die Typinferenz den Typ nicht mehr, Sie haben immer eine einzige Schnittstelle, gegen die Sie entwickeln können. Der nächste Schritt ist, dass Sie nicht immer new Logger()und immer wieder haben wollen. Sie setzen also die Zuverlässigkeit zum Erstellen neuer Instanzen auf eine einzelne zentrale Factory-Klasse und erhalten Code wie:

ICanLog logger = LoggerFactory.Create();

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

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

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

Irgendwo benötigt diese TypeFactory Konfigurationsdaten, deren tatsächliche Klasse instanziiert werden muss, wenn ein bestimmter Schnittstellentyp angefordert wird. Daher benötigen Sie eine Zuordnung. Natürlich können Sie diese Zuordnung in Ihrem Code vornehmen, aber dann bedeutet eine Typänderung eine Neukompilierung. Sie können diese Zuordnung aber auch in eine XML-Datei einfügen, z. Auf diese Weise können Sie die tatsächlich verwendete Klasse auch nach der Kompilierungszeit (!) Ändern, dh dynamisch, ohne sie neu zu kompilieren!

Um Ihnen ein nützliches Beispiel dafür zu geben: Stellen Sie sich eine Software vor, die sich 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 Ihren Kunden zu helfen.

Und jetzt, wenn Sie Namen ein wenig ersetzen, erhalten Sie eine einfache Implementierung eines Service Locator , eines von zwei Mustern für die Umkehrung der Kontrolle (da Sie die Kontrolle darüber umkehren, wer genau entscheidet, welche Klasse instanziiert werden soll).

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

Die Abhängigkeitsinjektion ist jetzt der nächste Schritt in dieser Zeile: Beseitigen Sie einfach diese einzelne Abhängigkeit vom Service Locator: Anstatt dass verschiedene Klassen den Service Locator nach einer Implementierung für eine bestimmte Schnittstelle fragen, geben Sie erneut die Kontrolle darüber zurück, wer was instanziiert .

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

public Database(ICanLog logger) { ... }

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

Und hier kommt ein DI-Framework ins Spiel: Sie konfigurieren Ihre Zuordnungen erneut und bitten dann Ihr DI-Framework, Ihre Anwendung für Sie zu instanziieren. Da die ApplicationKlasse eine ICanPersistDataImplementierung erfordert , wird eine Instanz von Databaseinjiziert. Dazu muss jedoch zuerst eine Instanz der Art von Logger erstellt werden, für die konfiguriert ist ICanLog. 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 Kompilierungszeit und eignet sich hervorragend für Unit-Tests (da es das Einspritzen von Stubs und / oder Mocks sehr einfach macht).

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 fügt immer nur eine Instanz pro Parameter ein, aber Sie können 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.

PS: Was ich hier beschrieben habe, ist eine Technik namens Konstruktorinjektion . Es gibt auch eine 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ängigkeiten vor. Eine Diskussion darüber würde jedoch den Rahmen dieser Frage sprengen.

Golo Roden
quelle
7
Natürlich können Sie dies auch auf diese Weise tun, aber dann müssen Sie diese Logik in jeder einzelnen Klasse implementieren, die die Austauschbarkeit der Implementierung unterstützen soll. Dies bedeutet viel duplizierten, redundanten Code, und dies bedeutet auch, dass Sie eine vorhandene Klasse berühren und teilweise neu schreiben müssen, sobald Sie sich entschieden haben, dass Sie sie jetzt benötigen. Mit DI können Sie dies für jede beliebige Klasse verwenden. Sie müssen sie nicht auf spezielle Weise schreiben (außer beim Definieren von Abhängigkeiten als Parameter im Konstruktor).
Golo Roden
137
Folgendes verstehe ich bei DI nie: Es macht die Architektur erheblich komplizierter. Und doch ist die Verwendung meines Erachtens ziemlich begrenzt. Die Beispiele sind sicherlich immer dieselben: austauschbare Logger, austauschbarer Modell- / Datenzugriff. Manchmal austauschbare Ansicht. Aber das ist es. Rechtfertigen diese wenigen Fälle wirklich eine weitaus komplexere Softwarearchitektur? - Vollständige Offenlegung: Ich habe DI bereits mit großer Wirkung verwendet, aber das war für eine ganz besondere Plug-In-Architektur, die ich nicht verallgemeinern würde.
Konrad Rudolph
17
@GoloRoden, warum rufst du die Schnittstelle ICanLog anstelle von ILogger auf? Ich habe mit einem anderen Programmierer zusammengearbeitet, der dies oft tat, und ich konnte die Konvention nie verstehen? Für mich ist es wie IEnumerable ICanEnumerate aufzurufen?
DermFrench
28
Ich habe es ICanLog genannt, weil wir zu oft mit Wörtern (Substantiven) arbeiten, die nichts bedeuten. ZB was ist ein Broker? Ein Manager? Auch ein Repository ist nicht eindeutig definiert. Und all diese Dinge als Substantive zu haben, ist eine typische Krankheit der OO-Sprachen (siehe steve-yegge.blogspot.de/2006/03/… ). Was ich ausdrücken möchte, ist, dass ich eine Komponente habe, die für mich protokollieren kann - warum also nicht so nennen? Natürlich spielt dies auch mit dem Ich als erster Person, daher ICanLog (ForYou).
Golo Roden
18
@ David Unit-Tests funktionieren einwandfrei - schließlich ist eine Einheit unabhängig von anderen Dingen (ansonsten ist sie keine Einheit). Was ohne DI-Container nicht funktioniert, sind Scheinprüfungen. Fairerweise bin ich nicht davon überzeugt, dass der Vorteil des Verspottens in allen Fällen die zusätzliche Komplexität des Hinzufügens von DI-Containern überwiegt. Ich mache strenge Unit-Tests. Ich verspotte selten.
Konrad Rudolph
499

Ich denke, die Leute sind oft verwirrt über den Unterschied zwischen der Abhängigkeitsinjektion und einem Abhängigkeitsinjektions- Framework (oder einem Container, wie er 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();
}

Und das ist es. Ernsthaft. Dies gibt Ihnen eine Menge Vorteile. Zwei wichtige sind die Fähigkeit, die Funktionalität von einem zentralen Ort (der Main()Funktion) aus zu steuern, anstatt sie im gesamten Programm zu verbreiten, und die Fähigkeit, jede Klasse einfacher isoliert zu testen (da Sie stattdessen Mocks oder andere gefälschte Objekte an ihren Konstruktor übergeben können von einem realen Wert).

Der Nachteil ist natürlich, dass Sie jetzt eine Megafunktion haben, die alle von Ihrem Programm verwendeten Klassen kennt. Hier können DI-Frameworks helfen. Wenn Sie jedoch Probleme haben zu verstehen, warum dieser Ansatz wertvoll ist, würde ich empfehlen, zuerst mit der manuellen Abhängigkeitsinjektion zu beginnen, damit Sie besser einschätzen können, was die verschiedenen Frameworks für Sie tun können.

Daniel Pryden
quelle
7
Warum sollte ich den zweiten Code dem ersten vorziehen? Der erste hat nur das neue Schlüsselwort. Wie hilft das?
user962206
17
@ user962206 Überlegen Sie, wie Sie A unabhängig von B
jk
66
@ user962206 Denken Sie auch darüber nach, was passieren würde, wenn B einige Parameter in seinem Konstruktor benötigt: Um sie zu instanziieren, müsste A über diese Parameter Bescheid wissen, was möglicherweise völlig unabhängig von A ist (es möchte nur von B abhängen , nicht davon, wovon B abhängt). Das Übergeben eines bereits konstruierten B (oder einer Unterklasse oder eines Modells von B) an den Konstruktor von A löst dies und macht A nur von B abhängig :)
Epidemie
17
@ acidzombie24: Wie viele Designmuster ist DI nur dann wirklich nützlich, wenn Ihre Codebasis groß genug ist, damit der einfache Ansatz zum Problem wird. Mein Bauchgefühl ist, dass DI erst dann eine Verbesserung darstellt, wenn Ihre Anwendung mehr als 20.000 Codezeilen und / oder mehr als 20 Abhängigkeiten von anderen Bibliotheken oder Frameworks aufweist. Wenn Ihre Anwendung kleiner ist, bevorzugen Sie möglicherweise immer noch das Programmieren im DI-Stil, aber der Unterschied ist bei weitem nicht so dramatisch.
Daniel Pryden
2
@ DanielPryden Ich denke nicht, dass die Codegröße so wichtig ist wie die Dynamik Ihres Codes. Wenn Sie regelmäßig neue Module hinzufügen, die zur gleichen Schnittstelle passen, müssen Sie den abhängigen Code nicht so oft ändern.
FistOfFury
35

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

IoC ist das Prinzip, wobei DI das Muster ist. Der Grund, dass Sie "mehr als einen Logger benötigen", wird meiner Erfahrung nach nie erreicht, aber der eigentliche Grund ist, dass Sie ihn wirklich brauchen, wenn Sie etwas testen. Ein Beispiel:

Mein Feature:

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

Sie können 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 in der OfferWeasel 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 eingestellte Datum vom festgesetzten Datum abweicht, selbst wenn Sie es nur eingeben DateTime.Now den Testcode eingeben, kann es sein, dass er um einige Millisekunden und daher nicht funktioniert 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 erlaubt es Ihnen, einige Zeit dort zu fälschen, wo es gebraucht 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 "Inversion der Kontrolle" angewendet, indem Sie eine Abhängigkeit eingefügt haben (Abrufen der aktuellen Zeit). Der Hauptgrund dafür ist das einfachere Testen isolierter Einheiten. Es gibt andere Möglichkeiten, dies zu tun. Beispielsweise sind eine Schnittstelle und eine Klasse hier nicht erforderlich, da in C # -Funktionen als Variablen übergeben werden können, sodass Sie anstelle einer Schnittstelle eine verwenden können Func<DateTime> , um dasselbe zu erreichen. Wenn Sie einen dynamischen Ansatz wählen, übergeben Sie einfach ein Objekt mit der entsprechenden Methode ( Ententypisierung ), und Sie benötigen überhaupt keine Schnittstelle.

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

Und... Es sollte auch beachtet werden, dass ein Objekt seinen Zweck zur Laufzeit nur dann ordnungsgemäß erfüllen kann, wenn alle seine Abhängigkeiten verfügbar sind, sodass das Einrichten der Eigenschaftsinjektion nicht viel Sinn macht. Meiner Meinung nach sollten alle Abhängigkeiten erfüllt sein, wenn der Konstruktor aufgerufen wird. Daher ist die Konstruktorinjektion die richtige Wahl.

Ich hoffe das hat geholfen.

cessor
quelle
4
Das sieht tatsächlich nach einer schrecklichen Lösung aus. Ich würde definitiv Code schreiben, der eher der Antwort von Daniel Pryden entspricht , aber für diesen speziellen Komponententest würde ich einfach DateTime durchführen. Jetzt vor und nach der Funktion und prüfen, ob die Zeit dazwischen liegt? Das Hinzufügen von mehr Schnittstellen / viel mehr Codezeilen scheint mir eine schlechte Idee zu sein.
3
Ich mag keine generischen A (B) -Beispiele und ich hatte nie das Gefühl, dass ein Logger 100 Implementierungen benötigt. Dies ist ein Beispiel, auf das ich kürzlich gestoßen bin, und es ist eine von fünf Möglichkeiten, es zu lösen, wobei eine tatsächlich die Verwendung von PostSharp beinhaltete. Es zeigt einen klassischen klassenbasierten Ansatz zur CTOR-Injektion. Könnten Sie ein besseres Beispiel aus der Praxis liefern, wo Sie auf eine gute Verwendung von DI gestoßen sind?
Cessor
2
Ich habe noch nie eine gute Verwendung für DI gesehen. Deshalb habe ich die Frage geschrieben.
2
Ich habe es nicht hilfreich gefunden. Mein Code ist immer einfach zu testen. Es scheint, dass DI gut für große Codebasen mit schlechtem Code ist.
1
Beachten Sie, dass Sie selbst in kleinen Funktionsprogrammen immer dann, wenn Sie f (x), g (f) haben, die Abhängigkeitsinjektion verwenden, sodass jede Fortsetzung in JS als Abhängigkeitsinjektion zählt. Ich vermute, dass Sie es bereits verwenden;)
cessor
15

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

Zum Beispiel sind wir ein zentraler Zahlungsanbieter, der mit vielen Zahlungsanbietern auf der ganzen Welt zusammenarbeitet. Wenn jedoch eine Anfrage gestellt wird, habe ich keine Ahnung, welchen Zahlungsprozessor ich anrufen werde. Ich könnte eine Klasse mit einer Menge 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 nun vor, Sie müssen den gesamten Code in einer einzigen Klasse verwalten, 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 für erstellen müssen Bei jeder Methode wird dies jedoch nur durch die Verwendung von 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 zur Komplikation bekannt ist) etwas komplizierter 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ß :)

Itai Sagi
quelle
+1, weil es so klingt, als würden Sie sagen, dass Sie es brauchen, wenn Sie virtuelle Methoden / Schnittstellen verwenden. Aber das ist immer noch selten. Ich würde es immer noch als ein new ThatProcessor()Framework
@ItaiS Mit dem Designmuster der Klasse Factory können Sie die unzähligen Schalter vermeiden. Verwenden Sie Reflection System.Reflection.Assembly.GetExecutingAssembly (). CreateInstance ()
domenicr
@domenicr natürlich! aber ich wollte es in einem vereinfachten Beispiel erklären
Itai Sagi
Ich stimme der obigen Erklärung zu, mit Ausnahme der Notwendigkeit einer Fabrikklasse. In dem Moment, in dem wir die Fabrikklasse implementieren, ist es einfach eine grobe Auswahl. Die beste Erklärung dazu fand ich im Kapitel über Poymorphismus und virtuelle Funktion von Bruce Erkel. Der wahre DI sollte frei von der Auswahl sein und der Objekttyp sollte zur Laufzeit automatisch über die Schnittstelle festgelegt werden. Das ist auch das wahre polymorphe Verhalten.
Arvind Krmar
Zum Beispiel (gemäß c ++) haben wir eine gemeinsame Schnittstelle, die nur den Verweis auf die Basisklasse verwendet und das Verhalten ihrer Ableitungsklasse ohne Auswahl implementiert. Leere Melodie (Instrument & i) {i.play (middleC); } int main () {Windflöte; Melodie (Flöte); } Instrument ist die Basisklasse, Wind wird daraus abgeleitet. Gemäß c ++ ermöglichen virtuelle Funktionen das Implementieren des Verhaltens abgeleiteter Klassen über eine gemeinsame Schnittstelle.
Arvind Krmar
6

Der Hauptgrund für die Verwendung von DI ist, dass Sie die Verantwortung für das Wissen der Implementierung dort platzieren möchten, wo das Wissen vorhanden ist. Die Idee von DI steht im Einklang mit der Kapselung und dem Design nach 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 bei OOP schon lange üblich. Oft erstellen Sie Codeteile wie:

I_Dosomething x = new Impl_Dosomething();

Der Nachteil ist, dass die Implementierungsklasse immer noch fest codiert ist und daher im Frontend das Wissen hat, welche Implementierung verwendet wird. DI geht mit dem Design by Interface noch einen Schritt weiter. Das einzige, was das Frontend wissen muss, ist das Wissen über die Schnittstelle. 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 gelöst werden kann. Beispiel für einen Service Locator:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

DI Beispiel:

I_Dosomething x = DIContainer.returnThat();

Eine der Anforderungen von DI ist, dass der Container herausfinden muss, welche Klasse die Implementierung welcher Schnittstelle ist. Daher erfordert ein DI-Container ein stark typisiertes Design und nur eine Implementierung für jede Schnittstelle gleichzeitig. Wenn Sie mehr Implementierungen einer Schnittstelle gleichzeitig benötigen (z. B. einen Taschenrechner), benötigen Sie den Service Locator oder das Factory-Design-Muster.

D (b) I: Abhängigkeitsinjektion und Design durch Schnittstelle. 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 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 Kapselungsebene ist. Wenn Sie es jedoch 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 diese im Laufe der Zeit viel weniger kommunizierbar als wenn Sie sie mit D (b) I explizit machen. Es läuft alles auf Wartung hinaus, die im Laufe der Zeit und nicht gleichzeitig erfolgt. :-)

Loek Bergman
quelle
1
"Der Nachteil ist, dass die Implementierungsklasse immer noch fest codiert ist." <- Meistens gibt es nur eine Implementierung, und wie gesagt, ich kann mir keinen Nongame-Code vorstellen, für den eine Schnittstelle erforderlich ist, die noch nicht integriert ist (.NET) ).
@ acidzombie24 Kann sein ... aber vergleichen Sie den Aufwand für die Implementierung einer Lösung mit DI von Anfang an mit dem Aufwand, eine Nicht-DI-Lösung später zu ändern, wenn Sie Schnittstellen benötigen. Ich würde fast immer mit der ersten Option gehen. Es ist besser, jetzt 100 $ zu zahlen, als morgen 100.000 $ bezahlen zu müssen.
Golo Roden
1
@GoloRoden In der Tat ist die Wartung das Hauptproblem bei der Verwendung von Techniken wie D (b) I. Das sind 80% der Kosten einer Anwendung. Ein Design, bei dem das erforderliche Verhalten von Anfang an mithilfe von Schnittstellen explizit dargestellt wird, spart dem Unternehmen später viel Zeit und Geld.
Loek Bergman
Ich werde es nicht wirklich verstehen, bis ich es bezahlen muss, weil ich bisher 0 $ bezahlt habe und bis jetzt nur noch 0 $ bezahlen muss. Aber ich zahle 0,05 Dollar, um jede Leitung oder Funktion sauber zu halten.