Dependency Injection (DI) ist ein bekanntes und modisches Muster. Die meisten Ingenieure kennen die Vorteile:
- Isolation bei Unit-Tests möglich / einfach machen
- Abhängigkeiten einer Klasse explizit definieren
- Gutes Design ermöglichen ( Single Responsibility Prinzip (SRP) zum Beispiel)
- Schnelle Aktivierung von Switching-Implementierungen (
DbLogger
anstelle vonConsoleLogger
beispielsweise)
Meiner Meinung nach besteht branchenweit Konsens darüber, dass DI ein gutes und nützliches Muster ist. Es gibt im Moment nicht zu viel Kritik. Die in der Community genannten Nachteile sind in der Regel gering. Manche von ihnen:
- Anzahl der Klassen erhöht
- Schaffung unnötiger Schnittstellen
Derzeit diskutieren wir mit meinem Kollegen über Architekturdesign. Er ist recht konservativ, aber aufgeschlossen. Er hinterfragt gerne Dinge, die ich für gut halte, weil viele Leute in der IT nur den neuesten Trend kopieren, die Vorteile wiederholen und im Allgemeinen nicht zu viel nachdenken - nicht zu tief analysieren.
Die Dinge, die ich fragen möchte, sind:
- Sollten wir die Abhängigkeitsinjektion verwenden, wenn wir nur eine Implementierung haben?
- Sollten wir verbieten, neue Objekte zu erstellen, mit Ausnahme von Sprach- / Framework-Objekten?
- Ist das Injizieren einer einzelnen Implementierung eine schlechte Idee (sagen wir, wir haben nur eine Implementierung, um keine "leere" Schnittstelle zu erstellen), wenn wir nicht vorhaben, eine bestimmte Klasse einem Komponententest zu unterziehen?
UserService
, ist diese Klasse nur ein Halter für Logik. Es wird eine Datenbankverbindung injiziert und Tests werden innerhalb einer Transaktion ausgeführt, für die ein Rollback durchgeführt wird. Viele würden dies als schlechte Praxis bezeichnen, aber ich fand, dass dies sehr gut funktioniert. Sie müssen Ihren Code nicht nur zum Testen verfälschen, und Sie erhalten die Fehlererkennungsleistung von Integrationstests.Antworten:
Zunächst möchte ich den Designansatz vom Konzept der Frameworks trennen. Die Abhängigkeitsinjektion auf ihrer einfachsten und grundlegendsten Ebene ist einfach:
Das ist es. Beachten Sie, dass dazu keine Schnittstellen, Frameworks, Injektionsarten usw. erforderlich sind. Um fair zu sein, habe ich dieses Muster vor 20 Jahren kennengelernt. Es ist nicht neu
Aufgrund der Verwirrung von mehr als 2 Personen über den Begriff Elternteil und Kind im Zusammenhang mit der Abhängigkeitsinjektion:
Abhängigkeitsinjektion ist ein Muster für das Objekt Zusammensetzung .
Warum Schnittstellen?
Schnittstellen sind ein Vertrag. Sie existieren, um zu begrenzen, wie eng zwei Objekte miteinander verbunden sein können. Nicht jede Abhängigkeit benötigt eine Schnittstelle, aber sie hilft beim Schreiben von modularem Code.
Wenn Sie das Konzept des Komponententests hinzufügen, haben Sie möglicherweise zwei konzeptionelle Implementierungen für eine bestimmte Schnittstelle: das reale Objekt, das Sie in Ihrer Anwendung verwenden möchten, und das verspottete oder gestoppelte Objekt, das Sie zum Testen von Code verwenden, der vom Objekt abhängt. Das allein kann für die Schnittstelle eine Rechtfertigung sein.
Warum Frameworks?
Das Initialisieren und Bereitstellen von Abhängigkeiten für untergeordnete Objekte kann bei einer großen Anzahl von Objekten schwierig sein. Frameworks bieten die folgenden Vorteile:
Sie haben auch die folgenden Nachteile:
Trotzdem gibt es Kompromisse. Für kleine Projekte, bei denen es nicht viele bewegliche Teile gibt und es keinen Grund gibt, ein DI-Framework zu verwenden. Bei komplizierteren Projekten, bei denen bestimmte Komponenten bereits für Sie erstellt wurden, kann der Rahmen jedoch gerechtfertigt sein.
Was ist mit [zufälliger Artikel im Internet]?
Was ist damit? Oft können die Leute übereifrig werden und eine Reihe von Einschränkungen hinzufügen und dich beschimpfen, wenn du Dinge nicht auf die "einzig wahre Weise" tust. Es gibt keinen wahren Weg. Sehen Sie nach, ob Sie aus dem Artikel etwas Nützliches extrahieren und die Dinge ignorieren können, mit denen Sie nicht einverstanden sind.
Kurz gesagt, denken Sie selbst und probieren Sie es aus.
Arbeiten mit "alten Köpfen"
Lerne so viel wie du kannst. Bei vielen Entwicklern, die in den Siebzigern arbeiten, werden Sie feststellen, dass sie gelernt haben, in vielen Dingen nicht dogmatisch zu sein. Sie haben Methoden, mit denen sie jahrzehntelang gearbeitet haben und die korrekte Ergebnisse liefern.
Ich hatte das Privileg, mit einigen von ihnen zu arbeiten, und sie können einige brutal ehrliche Rückmeldungen geben, die sehr viel Sinn ergeben. Und wo sie Wert sehen, erweitern sie ihr Repertoire um diese Werkzeuge.
quelle
Die Abhängigkeitsinjektion ist, wie die meisten Muster, eine Lösung für Probleme . Fragen Sie also zunächst, ob Sie überhaupt das Problem haben. Wenn nicht, verschlechtert sich der Code wahrscheinlich, wenn Sie das Muster verwenden .
Überlegen Sie zuerst, ob Sie Abhängigkeiten reduzieren oder beseitigen können. Wenn alle anderen Dinge gleich sind, möchten wir, dass jede Komponente in einem System so wenig Abhängigkeiten wie möglich aufweist. Und wenn die Abhängigkeiten verschwunden sind, wird die Frage des Injizierens oder Nicht-Injizierens strittig!
Stellen Sie sich ein Modul vor, das einige Daten von einem externen Dienst herunterlädt, analysiert, komplexe Analysen durchführt und die Ergebnisse in eine Datei schreibt.
Wenn nun die Abhängigkeit vom externen Dienst fest codiert ist, ist es wirklich schwierig, die interne Verarbeitung dieses Moduls zu testen. Sie können sich also dafür entscheiden, den externen Service und das Dateisystem als Schnittstellenabhängigkeiten einzuschleusen, wodurch Sie stattdessen Mocks einschleusen können, die wiederum das Testen der internen Logik durch Einheiten ermöglichen.
Eine viel bessere Lösung besteht darin, die Analyse einfach von der Eingabe / Ausgabe zu trennen. Wenn die Analyse in ein Modul ohne Nebenwirkungen extrahiert wird, kann sie viel einfacher getestet werden. Beachten Sie, dass das Verspotten ein Codegeruch ist - es ist nicht immer vermeidbar, aber im Allgemeinen ist es besser, wenn Sie testen können, ohne sich auf das Verspotten zu verlassen. Wenn Sie also die Abhängigkeiten beseitigen, vermeiden Sie die Probleme, die DI lösen soll. Beachten Sie, dass ein solches Design auch viel besser an SRP haftet.
Ich möchte betonen, dass DI nicht unbedingt SRP oder andere gute Entwurfsprinzipien wie Trennung von Bedenken, hohe Kohäsion / niedrige Kopplung usw. erleichtert. Es könnte genauso gut den gegenteiligen Effekt haben. Stellen Sie sich eine Klasse A vor, die intern eine andere Klasse B verwendet. B wird nur von A verwendet und ist daher vollständig gekapselt und kann als Implementierungsdetail betrachtet werden. Wenn Sie dies so ändern, dass B in den Konstruktor von A eingefügt wird, haben Sie dieses Implementierungsdetail verfügbar gemacht und wissen nun, wie diese Abhängigkeit besteht und wie B initialisiert wird. Die Lebensdauer von B usw. muss an einer anderen Stelle im System separat vorhanden sein von A. Sie haben also eine insgesamt schlechtere Architektur mit undichten Bedenken.
Andererseits gibt es einige Fälle, in denen DI wirklich nützlich ist. Zum Beispiel für globale Dienste mit Nebenwirkungen wie einem Logger.
Das Problem besteht darin, dass Muster und Architekturen eher Ziele als Werkzeuge werden. Ich frage nur "Sollen wir DI verwenden?" stellt den Karren vor das Pferd. Sie sollten fragen: "Haben wir ein Problem?" und "Was ist die beste Lösung für dieses Problem?"
Ein Teil Ihrer Frage lautet: "Sollen wir überflüssige Schnittstellen erstellen, um die Anforderungen des Musters zu erfüllen?" Die Antwort darauf haben Sie wahrscheinlich schon erkannt - absolut nicht ! Jeder, der Ihnen etwas anderes sagt, versucht, Ihnen etwas zu verkaufen - höchstwahrscheinlich teure Beratungsstunden. Eine Schnittstelle hat nur dann einen Wert, wenn sie eine Abstraktion darstellt. Eine Schnittstelle, die nur die Oberfläche einer einzelnen Klasse imitiert, wird als "Header-Schnittstelle" bezeichnet, und dies ist ein bekanntes Antipattern.
quelle
A
verwendetB
in der Produktion, sondern wurde nur getestet gegenMockB
unsere Tests sagen uns nicht , ob es in der Produktion arbeiten. Wenn reine (keine Nebenwirkungen) Komponenten eines Domain-Modells sich gegenseitig verspotten und verspotten, ist das Ergebnis eine enorme Zeitverschwendung, eine aufgeblähte und fragile Codebasis und ein geringes Vertrauen in das resultierende System. Verspotten Sie die Grenzen des Systems, nicht zwischen beliebigen Teilen desselben Systems.Nach meiner Erfahrung hat die Abhängigkeitsinjektion eine Reihe von Nachteilen.
Erstens vereinfacht die Verwendung von DI das automatisierte Testen nicht so sehr wie angekündigt. Durch Unit-Tests einer Klasse mit einer Scheinimplementierung einer Schnittstelle können Sie überprüfen, wie diese Klasse mit der Schnittstelle interagiert. Das heißt, Sie können anhand von Einheiten testen, wie die getestete Klasse den von der Schnittstelle bereitgestellten Vertrag verwendet. Dies bietet jedoch eine viel größere Sicherheit, dass die Eingabe der zu testenden Klasse in die Schnittstelle wie erwartet erfolgt. Es bietet eine eher schlechte Sicherheit, dass die getestete Klasse erwartungsgemäß auf die Ausgabe über die Schnittstelle reagiert, da es sich fast ausschließlich um eine Scheinausgabe handelt, die selbst Fehlern, Vereinfachungen usw. unterworfen ist. Kurz gesagt, Sie können NICHT überprüfen, ob sich die Klasse bei einer tatsächlichen Implementierung der Schnittstelle erwartungsgemäß verhält.
Zweitens macht es DI viel schwerer, durch Code zu navigieren. Bei dem Versuch, zur Definition von Klassen zu navigieren, die als Eingabe für Funktionen verwendet werden, kann eine Schnittstelle von einer geringfügigen Störung (z. B. bei einer einzelnen Implementierung) bis zu einer großen Zeitsenke (z. B. bei Verwendung einer übermäßig generischen Schnittstelle wie IDisposable) reichen. beim Versuch, die tatsächlich verwendete Implementierung zu finden. Dies kann eine einfache Übung wie "Ich muss eine Nullreferenz-Ausnahme im Code beheben, die unmittelbar nach dem Drucken dieser Protokollierungsanweisung auftritt" zu einem Tagesaufwand machen.
Drittens ist die Verwendung von DI und Frameworks ein zweischneidiges Schwert. Dies kann die Menge an Code auf der Kesselplatte, die für allgemeine Vorgänge benötigt wird, erheblich reduzieren. Dies hat jedoch den Nachteil, dass detaillierte Kenntnisse des jeweiligen DI-Frameworks erforderlich sind, um zu verstehen, wie diese allgemeinen Vorgänge tatsächlich miteinander verbunden sind. Um zu verstehen, wie Abhängigkeiten in das Framework geladen werden, und um eine neue Abhängigkeit in das zu injizierende Framework einzufügen, müssen Sie eine ganze Menge Hintergrundmaterial lesen und einige grundlegende Tutorials zum Framework befolgen. Dies kann einige einfache Aufgaben zu zeitaufwändigen machen.
quelle
Ich folgte Mark Seemanns Rat von "Dependency Injection in .NET" - zu vermuten.
DI sollte verwendet werden, wenn Sie eine 'flüchtige Abhängigkeit' haben, z. B. besteht eine vernünftige Chance, dass sich diese ändert.
Wenn Sie also glauben, dass Sie in Zukunft mehr als eine Implementierung haben oder die Implementierung sich ändern könnte, verwenden Sie DI. Ansonsten
new
ist in Ordnung.quelle
Mein größter Ärger über DI wurde bereits in einigen Antworten erwähnt, aber ich werde ihn hier etwas näher erläutern. DI (wie es heute meistens gemacht wird, mit Containern usw.) schadet wirklich WIRKLICH der Lesbarkeit des Codes. Und die Lesbarkeit des Codes ist wohl der Grund für die meisten heutigen Programminnovationen. Wie jemand sagte - das Schreiben von Code ist einfach. Das Lesen von Code ist schwierig. Aber es ist auch extrem wichtig, es sei denn, Sie schreiben eine Art kleines Einmal-Schreib-Dienstprogramm.
Das Problem mit DI in dieser Hinsicht ist, dass es undurchsichtig ist. Der Container ist eine Blackbox. Objekte tauchen einfach irgendwo auf und Sie haben keine Ahnung - wer hat sie wann gebaut? Was wurde an den Konstrukteur übergeben? Mit wem teile ich diese Instanz? Wer weiß...
Und wenn Sie hauptsächlich mit Schnittstellen arbeiten, werden alle "Gehe zu Definition" -Funktionen Ihrer IDE in Rauch aufgehen. Es ist furchtbar schwierig, den Programmfluss aufzuspüren, ohne ihn auszuführen und nur durchzugehen, um zu sehen, WELCHE Implementierung der Schnittstelle an DIESEM bestimmten Punkt verwendet wurde. Und gelegentlich gibt es eine technische Hürde, die Sie daran hindert, durchzukommen. Und selbst wenn Sie es können, wenn es darum geht, durch den verdrehten Darm des DI-Behälters zu gehen, wird die ganze Angelegenheit schnell zu einer Übung in Frustration.
Um effizient mit einem Teil des Codes zu arbeiten, der DI verwendet, müssen Sie damit vertraut sein und bereits wissen, was wohin geht.
quelle
Während DI im Allgemeinen sicherlich eine gute Sache ist, würde ich vorschlagen, es nicht blind für alles zu verwenden. Zum Beispiel injiziere ich niemals Logger. Einer der Vorteile von DI besteht darin, die Abhängigkeiten klar und deutlich zu machen. Es hat keinen Sinn, eine Auflistung
ILogger
als Abhängigkeit von fast jeder Klasse vorzunehmen - es ist nur Unordnung. Es liegt in der Verantwortung des Loggers, die Flexibilität bereitzustellen, die Sie benötigen. Alle meine Logger sind statische Endmitglieder. Ich kann in Betracht ziehen, einen Logger zu injizieren, wenn ich einen nicht statischen brauche.Dies ist ein Nachteil des gegebenen DI-Frameworks oder Mocking-Frameworks, nicht von DI selbst. In den meisten Orten hängen meine Klassen von konkreten Klassen ab, was bedeutet, dass kein Boilerplate benötigt wird. Guice (ein Java DI-Framework) bindet standardmäßig eine Klasse an sich selbst und ich muss die Bindung nur in Tests überschreiben (oder sie stattdessen manuell verbinden).
Ich erstelle die Schnittstellen nur bei Bedarf (was eher selten vorkommt). Das bedeutet, dass ich manchmal alle Vorkommen einer Klasse durch eine Schnittstelle ersetzen muss, aber die IDE kann dies für mich tun.
Ja, aber keine zusätzlichen Heizplatten verwenden .
Nein. Es wird viele Wertklassen (unveränderlich) und Datenklassen (veränderlich) geben, bei denen die Instanzen nur erstellt und weitergegeben werden und bei denen es keinen Sinn macht, sie zu injizieren - da sie nie in einem anderen Objekt (oder nur in einem anderen) gespeichert werden solche Objekte).
Für sie mag es sein, dass Sie stattdessen eine Fabrik injizieren müssen, aber die meiste Zeit macht es keinen Sinn (stellen
@Value class NamedUrl {private final String name; private final URL url;}
Sie sich zum Beispiel vor, Sie brauchen hier wirklich keine Fabrik und es gibt nichts zu injizieren).IMHO ist es in Ordnung, solange es kein Aufblähen von Code verursacht. Fügen Sie die Abhängigkeit ein, aber erstellen Sie nicht die Schnittstelle (und kein verrücktes Konfigurations-XML!), Da Sie dies später problemlos tun können.
Tatsächlich gibt es in meinem aktuellen Projekt vier Klassen (von Hunderten), die ich aus DI ausgeschlossen habe, da es sich um einfache Klassen handelt, die an zu vielen Stellen verwendet werden, einschließlich Datenobjekten.
Ein weiterer Nachteil der meisten DI-Frameworks ist der Laufzeitaufwand. Dies kann verschoben werden, um die Zeit zu kompilieren (für Java gibt es Dolch , keine Ahnung über andere Sprachen).
Ein weiterer Nachteil ist die überall auftretende Magie, die reduziert werden kann (z. B. habe ich die Proxy-Erstellung bei Verwendung von Guice deaktiviert).
quelle
Ich muss sagen, dass meiner Meinung nach der gesamte Begriff der Abhängigkeitsinjektion überbewertet ist.
DI ist das moderne Äquivalent globaler Werte. Die Dinge, die Sie injizieren, sind globale Singletons und reine Codeobjekte, ansonsten könnten Sie sie nicht injizieren. Die meisten Verwendungen von DI werden Ihnen aufgezwungen, um eine bestimmte Bibliothek (JPA, Spring Data usw.) zu verwenden. DI bietet größtenteils die perfekte Umgebung für die Pflege und den Anbau von Spaghetti.
Ehrlich gesagt ist der einfachste Weg, eine Klasse zu testen, sicherzustellen, dass alle Abhängigkeiten in einer Methode erstellt werden, die überschrieben werden kann. Erstellen Sie dann eine Testklasse, die von der tatsächlichen Klasse abgeleitet ist, und überschreiben Sie diese Methode.
Dann instanziieren Sie die Test-Klasse und testen alle ihre Methoden. Dies wird einigen von Ihnen nicht klar sein - die Methoden, die Sie testen, gehören zu der getesteten Klasse. Alle diese Methodentests finden in einer einzigen Klassendatei statt - der Unit-Test-Klasse, die der zu testenden Klasse zugeordnet ist. Hier gibt es keinen Overhead - so funktioniert Unit Testing.
Im Code sieht dieses Konzept so aus ...
Das Ergebnis entspricht genau der Verwendung von DI - das heißt, der ClassUnderTest ist zum Testen konfiguriert.
Die einzigen Unterschiede bestehen darin, dass dieser Code äußerst präzise, vollständig gekapselt, einfacher zu codieren, einfacher zu verstehen, schneller, speicherärmer, keine alternative Konfiguration erforderlich, keine Frameworks erforderlich ist und niemals die Ursache für 4 Seiten ist (WTF!) Stack-Trace, der genau NULL (0) Klassen enthält, die Sie geschrieben haben, und für jeden, der auch nur das geringste OO-Wissen hat, vom Anfänger bis zum Guru, völlig offensichtlich ist (Sie würden denken, aber sich täuschen).
Davon abgesehen können wir es natürlich nicht verwenden - es ist zu offensichtlich und nicht trendy genug.
Letztendlich ist meine größte Sorge mit DI jedoch, dass die Projekte, die ich gesehen habe, kläglich gescheitert sind. Sie alle waren massive Codebasen, in denen DI der Klebstoff war, der alles zusammenhält. DI ist keine Architektur - es ist wirklich nur in wenigen Situationen relevant, von denen die meisten Ihnen aufgezwungen werden, um eine andere Bibliothek (JPA, Spring Data usw.) zu verwenden. In einer gut gestalteten Codebasis würden die meisten Verwendungen von DI in einer Ebene stattfinden, die unter der Ebene liegt, in der Ihre täglichen Entwicklungsaktivitäten stattfinden.
quelle
bar
nur danach aufgerufen werdenfoo
können, handelt es sich um eine fehlerhafte API. Es behauptet , Funktionalität (bar
) bereitzustellen , aber wir können sie nicht wirklich verwenden (da siefoo
möglicherweise nicht aufgerufen wurde). Wenn Sie sich an IhrinitializeDependencies
(Anti?) - Muster halten möchten , sollten Sie es zumindest als privat / geschützt kennzeichnen und es automatisch vom Konstruktor aufrufen, damit die API aufrichtig ist.Wirklich läuft Ihre Frage darauf hinaus, ob das Testen von Einheiten schlecht ist.
99% Ihrer alternativen Klassen, die injiziert werden sollen, sind Mocks, die Unit-Tests ermöglichen.
Wenn Sie Unit-Tests ohne DI durchführen, haben Sie das Problem, wie Sie die Klassen dazu bringen, gespielte Daten oder einen gespielten Service zu verwenden. Sagen wir 'Teil der Logik', da Sie diese möglicherweise nicht in Services aufteilen.
Es gibt verschiedene Möglichkeiten, dies zu tun, aber DI ist gut und flexibel. Sobald Sie es zum Testen haben, sind Sie praktisch gezwungen, es überall zu verwenden, da Sie ein anderes Stück Code benötigen, sogar das sogenannte DI des armen Mannes, das die konkreten Typen instanziiert.
Es ist kaum vorstellbar, dass ein Nachteil so groß ist, dass der Vorteil von Unit-Tests überwältigt ist.
quelle
new
Methode verwenden , dass während der Tests derselbe Code ausgeführt wurde, der in der Produktion ausgeführt wurde.Order
Beispiel ein Komponententest: Es testet eine Methode (dietotal
Methode vonOrder
). Sie beschweren sich, dass es Code aus 2 Klassen aufruft. Meine Antwort lautet also was ? Wir testen nicht "2 Klassen auf einmal", wir testen dietotal
Methode. Es sollte uns egal sein, wie eine Methode ihre Aufgabe erfüllt: Dies sind Implementierungsdetails. Das Testen verursacht Zerbrechlichkeit, enge Kopplung usw. Wir kümmern uns nur um das Verhalten der Methode (Rückgabewert und Nebenwirkungen), nicht um die Klassen / Module / CPU-Register usw. es verwendet in den prozess.