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.
quelle
Antworten:
Zunächst möchte ich eine Annahme erklären, die ich für diese Antwort mache. Es ist nicht immer wahr, aber ziemlich oft:
(Eigentlich gibt es Schnittstellen, die auch Substantive sind, aber ich möchte hier verallgemeinern.)
So kann eine Schnittstelle zum Beispiel etwas wie
IDisposable
,IEnumerable
oderIPrintable
. Eine Klasse ist eine tatsächliche Implementierung einer oder mehrerer dieser Schnittstellen:List
oderMap
beide können Implementierungen von seinIEnumerable
.Um es auf den Punkt zu bringen: Oft hängen Ihre Klassen voneinander ab. Sie könnten beispielsweise eine
Database
Klasse 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 KlasseLogger
und haben dannDatabase
eine Abhängigkeit vonLogger
.So weit, ist es gut.
Sie können diese Abhängigkeit innerhalb Ihrer
Database
Klasse mit der folgenden Zeile modellieren :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
durch:
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: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: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:
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
Database
Klasse jetzt über einen Konstruktor, für den ein Parameter vom Typ erforderlich istICanLog
: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
Application
Klasse eineICanPersistData
Implementierung erfordert , wird eine Instanz vonDatabase
injiziert. Dazu muss jedoch zuerst eine Instanz der Art von Logger erstellt werden, für die konfiguriert istICanLog
. 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.
quelle
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:
Sie schreiben Code wie folgt:
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.
quelle
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:
Sie können dies folgendermaßen testen:
Also irgendwo in der
OfferWeasel
es Ihnen ein Angebotsobjekt wie das folgende: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: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:
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.
quelle
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:
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.
** Der Code wird nicht kompiliert, ich weiß :)
quelle
new ThatProcessor()
FrameworkDer 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:
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:
DI Beispiel:
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. :-)
quelle