Abhängigkeitsinjektionsframeworks wie Google Guice geben die folgende Motivation für ihre Verwendung ( Quelle ):
Um ein Objekt zu erstellen, erstellen Sie zuerst seine Abhängigkeiten. Aber um jede Abhängigkeit aufzubauen, benötigen Sie ihre Abhängigkeiten und so weiter. Wenn Sie also ein Objekt erstellen, müssen Sie wirklich ein Objektdiagramm erstellen.
Das Erstellen von Objektdiagrammen von Hand ist arbeitsintensiv (...) und erschwert das Testen.
Aber dieses Argument kaufe ich mir nicht: Selbst ohne ein Framework für die Abhängigkeitsinjektion kann ich Klassen schreiben, die einfach zu instanziieren und bequem zu testen sind. Das Beispiel auf der Guice-Motivationsseite könnte beispielsweise folgendermaßen umgeschrieben werden:
class BillingService
{
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;
// constructor for tests, taking all collaborators as parameters
BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
this.processor = processor;
this.transactionLog = transactionLog;
}
// constructor for production, calling the (productive) constructors of the collaborators
public BillingService()
{
this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
{
...
}
}
Es kann also andere Argumente für Abhängigkeitsinjektions-Frameworks geben ( die für diese Frage nicht in Frage kommen !), Aber die einfache Erstellung testbarer Objektgraphen ist keines davon, oder?
new ShippingService()
.Antworten:
Es gibt eine alte, alte Debatte darüber, wie man Abhängigkeitsinjektionen am besten durchführen kann.
Der ursprüngliche Schnitt der Feder instanziierte ein einfaches Objekt und injizierte dann Abhängigkeiten durch Setter-Methoden.
Aber dann bestand eine große Anzahl von Leuten darauf, dass das Einfügen von Abhängigkeiten durch Konstruktorparameter der richtige Weg war, dies zu tun.
Dann, in letzter Zeit, als die Verwendung von Reflektion immer häufiger wurde, wurde es zur Wut, die Werte privater Mitglieder direkt ohne Setter oder Konstrukteure festzulegen.
Ihr erster Konstruktor entspricht also dem zweiten Ansatz für die Abhängigkeitsinjektion. Es ermöglicht Ihnen, nette Dinge wie Injektions-Mocks zum Testen auszuführen.
Der Konstruktor ohne Argumente hat dieses Problem. Da die Implementierungsklassen für
PaypalCreditCardProcessor
und instanziiert werdenDatabaseTransactionLog
, entsteht eine schwierige Abhängigkeit zum Zeitpunkt der Kompilierung von PayPal und der Datenbank. Es übernimmt die Verantwortung für die korrekte Erstellung und Konfiguration des gesamten Abhängigkeitsbaums.Stellen Sie sich vor, dass der PayPay-Prozessor ein wirklich kompliziertes Subsystem ist und zusätzlich viele Unterstützungsbibliotheken umfasst. Indem Sie eine Abhängigkeit zur Kompilierungszeit für diese Implementierungsklasse erstellen, stellen Sie eine unzerbrechliche Verknüpfung zu dieser gesamten Abhängigkeitsbaumstruktur her. Die Komplexität Ihres Objektgraphen ist gerade um eine Größenordnung gestiegen, vielleicht um zwei.
Viele dieser Elemente im Abhängigkeitsbaum sind transparent, aber viele müssen auch instanziiert werden. Wahrscheinlich können Sie a nicht einfach instanziieren
PaypalCreditCardProcessor
.Zusätzlich zur Instanziierung müssen für jedes Objekt Eigenschaften aus der Konfiguration übernommen werden.
Wenn Sie nur eine Abhängigkeit von der Schnittstelle haben und einer externen Factory erlauben, die Abhängigkeit zu erstellen und einzufügen, schneiden Sie den gesamten PayPal-Abhängigkeitsbaum ab, und die Komplexität Ihres Codes hört an der Schnittstelle auf.
Es gibt noch weitere Vorteile, z. B. die Angabe von Implementierungsklassen bei der Konfiguration (dh zur Laufzeit anstatt zur Kompilierungszeit) oder eine dynamischere Abhängigkeitsspezifikation, die sich beispielsweise je nach Umgebung (Test, Integration, Produktion) ändert.
Angenommen, der PayPal-Prozessor hatte drei abhängige Objekte und jede dieser Abhängigkeiten hatte zwei weitere. Und all diese Objekte müssen Eigenschaften aus der Konfiguration abrufen. Der aktuelle Code übernimmt die Verantwortung für das Erstellen all dessen, das Festlegen von Eigenschaften aus der Konfiguration usw. usw. - alles Anliegen, die das DI-Framework erledigen wird.
Es mag auf den ersten Blick nicht offensichtlich erscheinen, wovor Sie sich mit einem DI-Framework abschirmen, aber es summiert sich und wird mit der Zeit schmerzhaft offensichtlich. (lol ich spreche aus der Erfahrung, es auf die harte Tour versucht zu haben)
...
In der Praxis sehe ich, dass ich selbst für ein sehr kleines Programm am Ende in einem DI-Stil schreibe und die Klassen in Implementierungs- / Factory-Paare aufteile. Das heißt, wenn ich kein DI-Framework wie Spring verwende, füge ich einfach einige einfache Factory-Klassen zusammen.
Das sorgt für die Trennung der Bedenken, sodass meine Klasse genau das Richtige tun kann, und die Factory-Klasse übernimmt die Verantwortung für das Bauen und Konfigurieren von Dingen.
Kein notwendiger Ansatz, aber FWIW
...
Im Allgemeinen reduziert das DI / Interface-Muster die Komplexität Ihres Codes, indem es zwei Dinge ausführt:
Abstraktion nachgelagerter Abhängigkeiten in Schnittstellen
Upstream-Abhängigkeiten aus dem Code in eine Art Container "heben"
Da die Instanziierung und Konfiguration von Objekten eine bekannte Aufgabe ist, kann das DI-Framework durch standardisierte Notation und Verwendung von Tricks wie Reflektion eine Menge Skaleneffekte erzielen. Das Verstreuen der gleichen Bedenken im Unterricht führt zu mehr Unordnung, als man denkt.
quelle
Indem Sie Ihren Objektgraphen früh im Prozess binden, dh fest in den Code einbinden, müssen sowohl der Vertrag als auch die Implementierung vorhanden sein. Wenn jemand anderes (vielleicht sogar Sie) diesen Code für einen etwas anderen Zweck verwenden möchte, muss er das gesamte Objektdiagramm neu berechnen und neu implementieren.
Mit DI-Frameworks können Sie eine Reihe von Komponenten zur Laufzeit zusammenführen. Dies macht das System "modular", das aus einer Anzahl von Modulen besteht, die an den Schnittstellen des jeweils anderen arbeiten, anstatt an den Implementierungen des jeweils anderen.
quelle
Der Ansatz "Eigenen Mitbearbeiter instanziieren" kann für Abhängigkeitsbäume verwendet werden, eignet sich jedoch mit Sicherheit nicht für Abhängigkeitsdiagramme, bei denen es sich um allgemein gerichtete azyklische Diagramme (DAG) handelt. In einer Abhängigkeits-DAG können mehrere Knoten auf denselben Knoten verweisen. Dies bedeutet, dass zwei Objekte dasselbe Objekt als Collaborator verwenden. Dieser Fall kann in der Tat nicht mit dem in der Frage beschriebenen Ansatz konstruiert werden.
Wenn einige meiner Mitbearbeiter (oder Mitbearbeiter des Mitbearbeiters) ein bestimmtes Objekt gemeinsam nutzen sollen, muss dieses Objekt instanziiert und an meine Mitbearbeiter übergeben werden. Ich müsste also tatsächlich mehr wissen als meine direkten Mitarbeiter, und das ist offensichtlich nicht skalierbar.
quelle
UnitOfWork
das ein einzelnesDbContext
und mehrere Repositorys hat. Alle diese Repositorys sollten dasselbeDbContext
Objekt verwenden. Mit der von OP vorgeschlagenen "Selbstinstanziierung" wird dies unmöglich.Ich habe Google Guice nicht verwendet, aber ich habe viel Zeit gebraucht, um alte N-Tier-Anwendungen in .NET auf IoC-Architekturen wie Onion Layer zu migrieren, die von Dependency Injection abhängen, um Dinge zu entkoppeln.
Warum Abhängigkeitsinjektion?
Der Zweck von Dependency Injection besteht eigentlich nicht in der Testbarkeit, sondern darin, eng gekoppelte Anwendungen zu verwenden und die Kopplung so weit wie möglich zu lösen. (Was das wünschenswerte Nebenprodukt hat, Ihren Code viel einfacher anpassbar zu machen, um korrekte Komponententests durchführen zu können)
Warum sollte ich mich überhaupt um das Koppeln kümmern?
Kopplung oder enge Abhängigkeiten können eine sehr gefährliche Sache sein. (Besonders in kompilierten Sprachen) In diesen Fällen kann es vorkommen, dass Sie eine Bibliothek, eine DLL usw. haben, die sehr selten verwendet wird und ein Problem aufweist, das die gesamte Anwendung effektiv offline schaltet. (Ihre gesamte Anwendung stirbt, weil ein unwichtiges Teil ein Problem hat ... das ist schlimm ... WIRKLICH schlimm) Wenn Sie nun Dinge entkoppeln, können Sie Ihre Anwendung so einrichten, dass sie auch dann ausgeführt werden kann, wenn diese DLL oder Bibliothek vollständig fehlt! Sicher, dass ein Teil, das diese Bibliothek oder DLL benötigt, nicht funktionieren wird, aber der Rest der Anwendung tuckert glücklich darüber.
Warum brauche ich Dependency Injection zum Testen?
Wirklich, Sie möchten nur lose gekoppelten Code, die Abhängigkeitsinjektion ermöglicht dies. Sie können Dinge ohne IoC locker koppeln, aber normalerweise ist es mehr Arbeit und weniger anpassungsfähig (ich bin sicher, dass jemand da draußen eine Ausnahme hat).
In dem Fall, den Sie angegeben haben, ist es meiner Meinung nach viel einfacher, nur die Abhängigkeitsinjektion einzurichten, damit ich Code nachahmen kann, den ich nicht als Teil dieses Tests zählen möchte. Nur sagen Ihnen Methode „Hey ich weiß , ich habe dir gesagt , das Repository zu nennen, sondern hier sind die Daten , die sie‚sollte‘Rückkehr zwinkert jetzt“ , weil diese Daten nie ändern Sie wissen , dass Sie nur den Teil zu testen , dass verwendet diese Daten, nicht den tatsächlicher Abruf der Daten.
Grundsätzlich sollten Sie beim Testen Integrationstests (Funktionalitätstests) durchführen, bei denen ein Teil der Funktionalität von Anfang bis Ende getestet wird, und vollständige Komponententests, bei denen jeder einzelne Teil des Codes (normalerweise auf Methoden- oder Funktionsebene) unabhängig getestet wird.
Die Idee ist, dass Sie sicherstellen möchten, dass die gesamte Funktionalität funktioniert, wenn Sie nicht den genauen Teil des Codes kennen möchten, der nicht funktioniert.
Dies KANN ohne Dependency Injection geschehen. In der Regel wird es jedoch mit zunehmendem Projektwachstum immer schwieriger, dies ohne Dependency Injection zu tun. (Gehen Sie IMMER davon aus, dass Ihr Projekt wachsen wird! Es ist besser, unnötige Übung in nützlichen Fertigkeiten zu haben, als ein Projekt zu finden, das schnell anläuft und ernsthaft überarbeitet und überarbeitet werden muss, nachdem die Dinge bereits angelaufen sind.)
quelle
Wie ich in einer anderen Antwort erwähne , besteht das Problem darin, dass Sie möchten, dass die Klasse
A
von einer KlasseB
ohne Hardcodierung abhängt, welche KlasseB
im Quellcode von A verwendet wird. Dies ist in Java und C # nicht möglich, da nur so eine Klasse importiert werden kann bezieht sich auf einen global eindeutigen Namen.Mithilfe eines Interfaces können Sie die hartcodierte Klassenabhängigkeit umgehen, aber Sie müssen noch eine Instanz des Interfaces in den Griff bekommen, und Sie können keine Konstruktoren aufrufen, oder Sie befinden sich wieder in Feld 1. Nun also zum Code Das könnte sonst zu Abhängigkeiten führen, die diese Verantwortung auf andere übertragen. Und seine Abhängigkeiten machen das Gleiche. So , jetzt jedes Mal müssen Sie eine Instanz einer Klasse , die Sie den gesamten Abhängigkeitsbaum manuell am Ende bauen, während in dem Fall , wo die Klasse A auf B hängt direkt konnte man nur rufen ,
new A()
dass Konstruktoraufruf und hastnew B()
, und so weiter.Ein Abhängigkeitsinjektionsframework versucht, dies zu umgehen, indem Sie die Zuordnungen zwischen Klassen angeben und den Abhängigkeitsbaum für Sie erstellen. Der Haken dabei ist, dass Sie dies zur Laufzeit herausfinden, wenn Sie die Zuordnungen vermasseln, und nicht zur Kompilierungszeit, wie Sie es in Sprachen tun würden, die Zuordnungsmodule als erstklassiges Konzept unterstützen.
quelle
Ich denke, das ist hier ein großes Missverständnis.
Guice ist eine Abhängigkeitsinjektion Framework für die . Es macht DI automatisch . Der Punkt, den sie in dem von Ihnen zitierten Auszug angesprochen haben, ist, dass Guice nicht mehr den "testbaren Konstruktor" manuell erstellen muss, den Sie in Ihrem Beispiel vorgestellt haben. Es hat absolut nichts mit der Abhängigkeitsinjektion selbst zu tun.
Dieser Konstruktor:
verwendet bereits Abhängigkeitsinjektion. Sie haben gerade gesagt, dass die Verwendung von DI einfach ist.
Das Problem, das Guice löst, ist, dass Sie, um diesen Konstruktor zu verwenden, irgendwo einen Objektgraph-Konstruktorcode haben müssen , der die bereits instanziierten Objekte manuell als Argumente dieses Konstruktors übergibt. Mit Guice können Sie an einem einzigen Ort konfigurieren, welche realen Implementierungsklassen diesen
CreditCardProcessor
und entsprechenTransactionLog
Schnittstellen entsprechen. Nach dieser Konfiguration werdenBillingService
diese Klassen bei jeder Erstellung mit Guice automatisch an den Konstruktor übergeben.Dies ist , was Dependency Injection Framework tut. Aber der Konstruktor selbst , die Sie präsentiert ist bereits eine Implementierung von depencency Injektionsprinzip . IoC-Container und DI-Frameworks sind Mittel, um die entsprechenden Prinzipien zu automatisieren, aber nichts hindert Sie daran, alles von Hand zu erledigen, das war der springende Punkt.
quelle