Ich lese über Abhängigkeitsinjektion (DI). Für mich ist es eine sehr komplizierte Sache, da ich las, dass es sich auch um eine Inversion der Kontrolle (IoC) handelte und ich das Gefühl hatte, dass ich auf eine Reise gehen würde.
Dies ist mein Verständnis: Anstatt ein Modell in der Klasse zu erstellen, das es auch verwendet, übergeben Sie das Modell (das bereits mit interessanten Eigenschaften gefüllt ist) dort, wo es benötigt wird (an eine neue Klasse, in der es als Parameter verwendet werden könnte) der Konstruktor).
Für mich ist dies nur eine Auseinandersetzung. Ich muss den Punkt verpasst haben? Vielleicht wird es bei größeren Projekten offensichtlicher?
Mein Verständnis ist nicht-DI (mit Pseudo-Code):
public void Start()
{
MyClass class = new MyClass();
}
...
public MyClass()
{
this.MyInterface = new MyInterface();
}
Und DI wäre
public void Start()
{
MyInterface myInterface = new MyInterface();
MyClass class = new MyClass(myInterface);
}
...
public MyClass(MyInterface myInterface)
{
this.MyInterface = myInterface;
}
Könnte jemand etwas Licht ins Dunkel bringen, ich bin mir sicher, dass ich hier durcheinander bin.
quelle
Antworten:
Nun ja, Sie fügen Ihre Abhängigkeiten entweder über einen Konstruktor oder über Eigenschaften ein.
Einer der Gründe dafür ist, MyClass nicht mit den Details zu belasten, wie eine Instanz von MyInterface erstellt werden muss. MyInterface kann eine ganze Liste von Abhängigkeiten enthalten, und der Code von MyClass würde sehr schnell hässlich, wenn Sie alle MyInterface-Abhängigkeiten in MyClass instanziieren würden.
Ein weiterer Grund ist zum Testen.
Wenn Sie eine Abhängigkeit von einer Dateireader-Schnittstelle haben und diese Abhängigkeit über einen Konstruktor beispielsweise in ConsumerClass einfügen, können Sie während des Tests eine speicherinterne Implementierung des Dateireader an ConsumerClass übergeben, ohne dass dies erforderlich ist I / O während des Testens.
quelle
Wie DI implementiert werden kann, hängt stark von der verwendeten Sprache ab.
Hier ist ein einfaches Nicht-DI-Beispiel:
Das nervt zB wenn ich für einen Test ein Scheinobjekt verwenden möchte
bar
. So können wir dies flexibler gestalten und die Übergabe von Instanzen über den Konstruktor ermöglichen:Dies ist bereits die einfachste Form der Abhängigkeitsinjektion. Das ist aber trotzdem schade, weil alles manuell erledigt werden muss (auch, weil der Anrufer einen Verweis auf unsere internen Objekte halten und damit unseren Zustand ungültig machen kann).
Wir können mehr Abstraktion einführen, indem wir Fabriken verwenden:
Eine Möglichkeit besteht darin
Foo
, dass die von einer abstrakten Factory generiert wirdEine andere Möglichkeit besteht darin, eine Factory an den Konstruktor zu übergeben:
Die verbleibenden Probleme dabei sind, dass wir
DependencyManager
für jede Konfiguration eine neue Unterklasse schreiben müssen und dass die Anzahl der Abhängigkeiten, die verwaltet werden können, ziemlich begrenzt ist (für jede neue Abhängigkeit ist eine neue Methode in der Schnittstelle erforderlich).Mit Funktionen wie Reflektion und dynamischem Laden von Klassen können wir dies umgehen. Dies hängt jedoch stark von der verwendeten Sprache ab. In Perl können Klassen anhand ihres Namens referenziert werden, was ich auch tun könnte
In Sprachen wie Java könnte sich der Abhängigkeitsmanager wie folgt verhalten
Map<Class, Object>
:Zu welcher tatsächlichen Klasse das
Bar.class
aufgelöste Objekt gehört, kann zur Laufzeit konfiguriert werden, zMap<Class, Class>
.Beim Schreiben des Konstruktors ist noch ein manuelles Element erforderlich. Wir können den Konstruktor aber komplett überflüssig machen, indem wir zB den DI über Annotationen ansteuern:
Hier ist eine Beispielimplementierung eines winzigen (und sehr eingeschränkten) DI-Frameworks: http://ideone.com/b2ubuF , obwohl diese Implementierung für unveränderliche Objekte völlig unbrauchbar ist (diese naive Implementierung kann keine Parameter für den Konstruktor annehmen).
quelle
Unsere Freunde von Stack Overflow haben eine gute Antwort darauf . Mein Favorit ist die zweite Antwort, einschließlich des Zitats:
aus James Shores Blog . Natürlich gibt es komplexere Versionen / Muster, die darüber geschichtet sind, aber es reicht aus, um im Grunde zu verstehen, was vor sich geht. Anstatt dass ein Objekt eigene Instanzvariablen erstellt, werden diese von außen übergeben.
quelle
Nehmen wir an, Sie gestalten das Interieur Ihres Wohnzimmers: Sie bringen einen schicken Kronleuchter an Ihrer Decke an und schließen eine edel passende Stehlampe an. Ein Jahr später beschließt Ihre reizende Frau, dass sie das Zimmer etwas weniger förmlich haben möchte. Welche Leuchte lässt sich leichter wechseln?
Die Idee hinter DI ist, den "Plug-In" -Ansatz überall zu verwenden. Dies scheint keine große Sache zu sein, wenn Sie in einer einfachen Hütte ohne Trockenbau leben und alle elektrischen Drähte freiliegen. (Sie sind ein Handwerker - Sie können alles ändern!) Und in ähnlicher Weise kann DI bei einer kleinen und einfachen Anwendung mehr Komplikationen verursachen, als es wert ist.
Für eine große und komplexe Anwendung ist DI jedoch für eine langfristige Wartung unverzichtbar . Sie können damit ganze Module aus Ihrem Code "entfernen" (z. B. das Datenbank-Backend) und sie mit verschiedenen Modulen austauschen, die das gleiche Ziel erreichen, dies jedoch auf eine völlig andere Weise tun (z. B. ein Cloud-Speichersystem).
Übrigens wäre eine Erörterung von DI unvollständig, ohne die Empfehlung von Dependency Injection in .NET von Mark Seeman. Wenn Sie mit .NET vertraut sind und jemals beabsichtigen, an einer umfangreichen SW-Entwicklung mitzuwirken, ist dies eine wichtige Lektüre. Er erklärt das Konzept viel besser als ich.
Lassen Sie mich Ihnen ein letztes wesentliches Merkmal von DI überlassen, das in Ihrem Codebeispiel übersehen wird. DI ermöglicht es Ihnen, das enorme Potenzial an Flexibilität zu nutzen, das in dem Sprichwort " Program to interfaces " enthalten ist. So ändern Sie Ihr Beispiel geringfügig:
Aber JETZT kann ich , da
MyRoom
es für die ILightFixture- Benutzeroberfläche entwickelt wurde , problemlos im nächsten Jahr eine Zeile in der Hauptfunktion ändern (eine andere "Abhängigkeit" "injizieren") und bekomme sofort neue Funktionen in MyRoom (ohne dies tun zu müssen) Erstellen Sie MyRoom neu oder stellen Sie es erneut bereit, falls es sich in einer separaten Bibliothek befindet. Dies setzt natürlich voraus, dass alle Ihre ILightFixture-Implementierungen unter Berücksichtigung der Liskov-Substitution entwickelt wurden. Alles mit nur einer einfachen Änderung:Außerdem kann ich jetzt Unit Test
MyRoom
(Sie sind Unit Test, oder?) Und eine Scheinleuchte verwenden, mit der ichMyRoom
völlig unabhängig testen kannClassyChandelier.
Natürlich gibt es noch viele weitere Vorteile, aber diese haben mir die Idee verkauft.
quelle
Die Abhängigkeitsinjektion übergibt nur einen Parameter, dies führt jedoch immer noch zu einigen Problemen, und der Name soll sich ein wenig darauf konzentrieren, warum der Unterschied zwischen dem Erstellen eines Objekts und dem Übergeben eines Objekts wichtig ist.
Dies ist wichtig, da (ziemlich offensichtlich) jedes Mal, wenn Objekt A Typ B aufruft, A davon abhängt, wie B funktioniert. Das heißt, wenn A die Entscheidung trifft, welchen konkreten Typ von B es verwenden soll, geht viel Flexibilität verloren, wie A von externen Klassen verwendet werden kann, ohne A zu ändern.
Ich erwähne dies, weil Sie davon sprechen, den Punkt der Abhängigkeitsinjektion zu verfehlen. Es kann nur bedeuten, einen Parameter zu übergeben, aber es kann wichtig sein, ob Sie dies tun.
Einige der Schwierigkeiten bei der tatsächlichen Implementierung von DI treten auch bei großen Projekten besser auf. Im Allgemeinen wird die tatsächliche Entscheidung, welche konkreten Klassen verwendet werden sollen, ganz nach oben verschoben, sodass Ihre Startmethode wie folgt aussehen kann:
aber mit weiteren 400 Zeilen dieser Art von Nietcode. Wenn Sie sich vorstellen, dies im Laufe der Zeit beizubehalten, wird die Attraktivität von DI-Containern deutlicher.
Stellen Sie sich auch eine Klasse vor, die beispielsweise IDisposable implementiert. Wenn die Klassen, die es verwenden, es per Injection erhalten, anstatt es selbst zu erstellen, woher wissen sie, wann sie Dispose () aufrufen müssen? (Ein weiteres Problem, mit dem sich ein DI-Container befasst.)
Sicher, die Abhängigkeitsinjektion übergibt nur einen Parameter, aber wenn die Leute Abhängigkeitsinjektion sagen, meinen sie "Übergeben eines Parameters an Ihr Objekt im Gegensatz zu der Alternative, dass Ihr Objekt selbst etwas instanziiert", und den Umgang mit allen Vorteilen der Nachteile von ein solcher Ansatz, möglicherweise mit Hilfe eines DI-Containers.
quelle
Auf Klassenebene ist es einfach.
"Abhängigkeitsinjektion" beantwortet einfach die Frage "Wie finde ich meine Mitarbeiter?" Mit "Sie werden an Sie herangeführt - Sie müssen sie nicht selbst holen". (Es ist ähnlich wie - aber nicht dasselbe wie - "Inversion of Control", wo die Frage "Wie ordne ich meine Operationen über meine Eingaben an?" Eine ähnliche Antwort hat.)
Der einzige Vorteil, den Ihre Mitarbeiter haben, besteht darin, dass der Client-Code Ihre Klasse verwenden kann, um ein Objektdiagramm zu erstellen, das seinen aktuellen Anforderungen entspricht. Sie haben die Form und die Wandlungsfähigkeit des Diagramms nicht willkürlich durch eine private Entscheidung festgelegt die konkreten Arten und den Lebenszyklus Ihrer Mitarbeiter.
(Alle anderen Vorteile der Testbarkeit, der losen Kopplung usw. ergeben sich größtenteils aus der Verwendung von Schnittstellen und weniger aus der Abhängigkeitsinjektion, obwohl DI natürlich die Verwendung von Schnittstellen fördert).
Es ist erwähnenswert, dass, wenn Sie vermeiden Sie Ihre eigenen Mitarbeiter instanziiert wird , Ihre Klasse muss daher seine Mitarbeiter von einem Konstruktor, eine Eigenschaft erhalten, oder ein Verfahren Argument (diese letztere Option wird oft übersehen, übrigens ... es tut Es ist nicht immer sinnvoll, dass die Kollaborateure einer Klasse Teil ihres "Staates" sind.
Und das ist auch gut so.
Auf der Anwendungsebene ...
Soviel zur klassenbezogenen Sicht der Dinge. Angenommen, Sie haben eine Reihe von Klassen, die der Regel "Ihre eigenen Mitarbeiter nicht instanziieren" folgen, und möchten aus ihnen einen Antrag stellen. Am einfachsten ist es, guten alten Code (ein wirklich nützliches Tool zum Aufrufen von Konstruktoren, Eigenschaften und Methoden!) Zu verwenden, um das gewünschte Objektdiagramm zu erstellen und Eingaben zu tätigen. (Ja, einige dieser Objekte in Ihrem Diagramm werden selbst Objektfabriken sein, die als Kollaborateure an andere langlebige Objekte im Diagramm weitergegeben wurden, die für den Service bereit sind ... Sie können nicht jedes Objekt vorkonstruieren! ).
... Sie müssen den Objektgraphen Ihrer App "flexibel" konfigurieren ...
Abhängig von Ihren anderen (nicht codebezogenen) Zielen möchten Sie dem Endbenutzer möglicherweise eine gewisse Kontrolle über das so bereitgestellte Objektdiagramm geben. Dies führt Sie in die eine oder andere Richtung eines Konfigurationsschemas, sei es eine Textdatei Ihres eigenen Designs mit einigen Name / Wert-Paaren, eine XML-Datei, eine benutzerdefinierte DSL, eine deklarative Graph-Beschreibungssprache wie YAML, eine zwingende Skriptsprache wie JavaScript oder etwas anderes, das für die jeweilige Aufgabe geeignet ist. Was auch immer erforderlich ist, um ein gültiges Objektdiagramm so zu erstellen, dass es den Anforderungen Ihrer Benutzer entspricht.
... kann eine bedeutende Designkraft sein.
Unter den extremsten Umständen dieses Typs können Sie sich für einen sehr allgemeinen Ansatz entscheiden und den Endbenutzern einen allgemeinen Mechanismus zum "Verkabeln" des von ihnen gewählten Objektgraphen an die Hand geben und ihnen sogar die Möglichkeit geben, konkrete Realisierungen von Schnittstellen zu erstellen Laufzeit! (Ihre Dokumentation ist ein glänzendes Juwel, Ihre Benutzer sind sehr schlau, kennen zumindest den groben Umriss des Objektgraphen Ihrer Anwendung, haben aber zufällig keinen Compiler zur Hand). Dieses Szenario kann theoretisch in einigen Unternehmenssituationen auftreten.
In diesem Fall verfügen Sie wahrscheinlich über eine deklarative Sprache, mit der Ihre Benutzer jeden Typ, die Zusammensetzung eines Objektgraphen dieser Typen und eine Palette von Schnittstellen, die der mythische Endbenutzer mischen und zuordnen kann, ausdrücken können. Um die kognitive Belastung Ihrer Benutzer zu verringern, bevorzugen Sie einen "Configuration by Convention" -Ansatz, sodass sie nur das interessierende Objekt-Graph-Fragment eingeben und überschreiben müssen, anstatt mit dem Ganzen zu ringen.
Du armer Arsch!
Da Sie nicht alles selbst aufschreiben wollten (aber im Ernst, sehen Sie sich eine YAML-Bindung für Ihre Sprache an), verwenden Sie eine Art DI-Framework.
Abhängig von der Reife dieses Frameworks haben Sie möglicherweise nicht die Möglichkeit, Konstruktor-Injection zu verwenden, auch wenn dies sinnvoll ist (die Mitarbeiter ändern sich nicht über die Lebensdauer eines Objekts), sodass Sie gezwungen sind, Setter Injection zu verwenden (auch wenn Kollaborateure ändern sich nicht über die Lebensdauer eines Objekts, und selbst wenn es keinen logischen Grund gibt, warum alle konkreten Implementierungen einer Schnittstelle Kollaborateure eines bestimmten Typs haben müssen . Wenn ja, befinden Sie sich derzeit in der Hölle der starken Kopplung, obwohl Sie in Ihrer gesamten Codebasis fleißig Schnittstellen verwendet haben - Horror!
Hoffentlich haben Sie ein DI-Framework verwendet, mit dem Sie Konstruktorinjektionen durchführen können, und Ihre Benutzer sind nur ein bisschen mürrisch , weil sie nicht mehr Zeit damit verbringen, über die spezifischen Dinge nachzudenken, die sie zum Konfigurieren benötigen, und ihnen eine für die Aufgabe besser geeignete Benutzeroberfläche zu geben zur Hand. (Um fair zu sein, haben Sie wahrscheinlich versucht, einen Weg zu finden, aber JavaEE hat Sie im Stich gelassen und Sie mussten auf diesen schrecklichen Hack zurückgreifen).
Bootnote
Zu keinem Zeitpunkt verwenden Sie Google Guice, wodurch der Codierer die Aufgabe, einen Objektgraphen mit Code zu erstellen, überflüssig macht ... indem er Code schreibt. Argh!
quelle
Viele Leute sagten hier, DI sei ein Mechanismus, um die Initialisierung von Instanzvariablen auszulagern.
Für offensichtliche und einfache Beispiele benötigen Sie keine "Abhängigkeitsinjektion". Sie können dies tun, indem Sie einen einfachen Parameter an den Konstruktor übergeben, wie Sie in Frage gestellt haben.
Ich denke jedoch, dass ein dedizierter "Abhängigkeitsinjektions" -Mechanismus nützlich ist, wenn Sie über Ressourcen auf Anwendungsebene sprechen, bei denen sowohl der Anrufer als auch der Angerufene nichts über diese Ressourcen wissen (und es nicht wissen wollen!).
Zum Beispiel: Datenbankverbindung, Sicherheitskontext, Protokollierungsfunktion, Konfigurationsdatei usw.
Darüber hinaus ist im Allgemeinen jeder Singleton ein guter Kandidat für DI. Je mehr davon Sie haben und verwenden, desto mehr Chancen haben Sie, DI zu benötigen.
Für diese Art von Ressourcen ist es ziemlich klar, dass es keinen Sinn macht, sie über Parameterlisten zu verschieben, und daher wurde der DI erfunden.
Letzter Hinweis: Sie benötigen auch einen DI-Container, der die eigentliche Injektion ausführt ... (erneut, um sowohl den Aufrufer als auch den Angerufenen von den Implementierungsdetails zu befreien).
quelle
Wenn Sie einen abstrakten Referenztyp übergeben, kann der aufrufende Code jedes Objekt übergeben, das den abstrakten Typ erweitert / implementiert. Daher könnte die Klasse, die das Objekt verwendet, in Zukunft ein neues Objekt verwenden, das erstellt wurde, ohne den Code zu ändern.
quelle
Dies ist eine weitere Option neben der Antwort von amon
Builder verwenden:
Das benutze ich für Abhängigkeiten. Ich benutze kein DI-Framework. Sie stehen im Weg.
quelle