Kritik und Nachteile der Abhängigkeitsinjektion

118

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 ( DbLoggeranstelle von ConsoleLoggerbeispielsweise)

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?
Landeeyo
quelle
33
Fragen Sie wirklich nach der Abhängigkeitsinjektion als Muster oder nach der Verwendung von DI-Frameworks? Dies sind wirklich unterschiedliche Dinge. Sie sollten klären, an welchem ​​Teil des Problems Sie interessiert sind, oder explizit nach beiden fragen.
Frax
10
@Frax über Muster, nicht Frameworks
Landeeyo
10
Sie verwechseln die Abhängigkeitsinversion mit der Abhängigkeitsinjektion. Ersteres ist ein Gestaltungsprinzip. Letzteres ist eine Technik (normalerweise mit einem vorhandenen Tool implementiert) zum Erstellen von Hierarchien von Objekten.
jpmc26
3
Ich schreibe oft Tests unter Verwendung der realen Datenbank und überhaupt ohne Scheinobjekte. Funktioniert in vielen Fällen sehr gut. Und dann brauchen Sie die meiste Zeit keine Schnittstellen. Wenn Sie eine haben 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.
usr
3
Der DI ist fast immer gut. Das Schlimme daran ist, dass viele Leute denken, sie kennen DI, aber alles, was sie wissen, ist, wie man ein seltsames Framework benutzt, ohne sich überhaupt sicher zu sein, was sie tun. DI leidet heutzutage sehr unter Cargo Cult Programming.
T. Sar

Antworten:

160

Zunächst möchte ich den Designansatz vom Konzept der Frameworks trennen. Die Abhängigkeitsinjektion auf ihrer einfachsten und grundlegendsten Ebene ist einfach:

Ein übergeordnetes Objekt stellt alle Abhängigkeiten bereit, die für das untergeordnete Objekt erforderlich sind.

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:

  • Das übergeordnete Objekt instanziiert und konfiguriert das verwendete untergeordnete Objekt
  • Das Kind ist die Komponente, die passiv instanziiert werden soll. Das heißt, es wurde entwickelt, um die vom übergeordneten Element bereitgestellten Abhängigkeiten zu verwenden, und es instanziiert keine eigenen Abhängigkeiten.

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:

  • Autodraht-Abhängigkeiten zu Komponenten
  • Konfiguration der Komponenten mit irgendwelchen Einstellungen
  • Automatisieren Sie den Kesselschildcode, damit Sie ihn nicht an mehreren Stellen schreiben müssen.

Sie haben auch die folgenden Nachteile:

  • Das übergeordnete Objekt ist ein "Container" und enthält keinen Code
  • Das Testen wird komplizierter, wenn Sie die Abhängigkeiten nicht direkt in Ihrem Testcode angeben können
  • Die Initialisierung kann verlangsamt werden, da alle Abhängigkeiten mithilfe von Reflexion und vielen anderen Tricks aufgelöst werden
  • Das Debuggen zur Laufzeit kann schwieriger sein, insbesondere wenn der Container einen Proxy zwischen die Schnittstelle und die eigentliche Komponente einfügt, die die Schnittstelle implementiert (in Spring integrierte aspektorientierte Programmierung). Der Container ist eine Black Box, und sie werden nicht immer mit dem Konzept erstellt, den Debugging-Prozess zu vereinfachen.

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.

Berin Loritsch
quelle
6
@CarlLeth, ich habe mit einer Reihe von Frameworks von Spring bis .net gearbeitet. Mit Spring können Sie Implementierungen mit etwas Reflection / Classloader Black Magic direkt in private Felder einfügen. Die einzige Möglichkeit, solche Komponenten zu testen, ist die Verwendung des Containers. Spring verfügt zwar über JUnit-Läufer, um die Testumgebung zu konfigurieren, dies ist jedoch komplizierter als das Einrichten. Also ja, ich habe nur ein praktisches Beispiel gegeben.
Berin Loritsch
17
Es gibt noch einen weiteren Nachteil, den ich mit DI durch Frameworks als Hürde empfinde, wenn ich meinen Troubleshooter / Maintainer-Hut trage: Die gespenstische Aktion in der Ferne macht das Offline-Debuggen schwieriger. Im schlimmsten Fall muss ich den Code ausführen, um zu sehen, wie Abhängigkeiten initialisiert und übergeben werden. Sie erwähnen dies im Kontext des "Testens", aber es ist tatsächlich viel schlimmer, wenn Sie erst anfangen, den Quellcode zu betrachten, niemals stört es zu versuchen, es zum Laufen zu bringen (was eine Menge Setup beinhalten kann). Es ist eine schlechte Sache, meine Fähigkeit zu beeinträchtigen, durch einen kurzen Blick zu erkennen, was Code tut.
Jeroen Mostert
1
Schnittstellen sind keine Verträge, sie sind einfach APIs. Verträge implizieren Semantik. Diese Antwort verwendet eine sprachspezifische Terminologie und Java / C # -spezifische Konventionen.
Frank Hileman
2
@BerinLoritsch Der Hauptpunkt Ihrer eigenen Antwort ist, dass das DI-Prinzip! = Ein beliebiges DI-Framework. Die Tatsache, dass Spring schreckliche, unverzeihliche Dinge tun kann, ist ein Nachteil von Spring und nicht von DI-Frameworks im Allgemeinen. Ein gutes DI-Framework hilft Ihnen dabei, dem DI-Prinzip ohne üble Tricks zu folgen.
Carl Leth
1
@CarlLeth: Alle DI-Frameworks wurden entwickelt, um einige Dinge zu entfernen oder zu automatisieren, die der Programmierer nicht buchstabieren möchte. Sie unterscheiden sich lediglich in der Art und Weise. Nach meinem besten Wissen entfernen sie alle die Fähigkeit zu wissen, wie (oder ob ) Klasse A und B interagieren, indem sie nur A und B betrachten - zumindest müssen Sie sich auch das DI-Setup / Konfiguration / ansehen. Konventionen. Kein Problem für den Programmierer (eigentlich genau das, was er will), aber ein potenzielles Problem für einen Betreuer / Debugger (möglicherweise derselbe Programmierer, später). Dies ist ein Kompromiss, den Sie eingehen, auch wenn Ihr DI-Framework "perfekt" ist.
Jeroen Mostert
88

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.

JacquesB
quelle
15
Ich könnte nicht mehr zustimmen! Beachten Sie auch, dass das Verspotten von Dingen dazu führt, dass wir die tatsächlichen Implementierungen nicht testen. Wenn Averwendet Bin der Produktion, sondern wurde nur getestet gegen MockBunsere 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.
Warbo
17
@CarlLeth Warum macht DI Code "testbar und wartbar" und Code ohne weniger? JacquesB hat Recht, dass Nebenwirkungen die Testbarkeit / Wartbarkeit beeinträchtigen . Wenn Code keine Nebenwirkungen hat, ist es uns egal, was / wo / wann / wie er anderen Code aufruft. wir können es einfach und direkt halten. Wenn Code Nebenwirkungen hat, müssen wir uns darum kümmern. DI kann Nebenwirkungen aus Funktionen herausholen und in Parameter einfügen, wodurch diese Funktionen testbarer, das Programm jedoch komplexer werden. Manchmal ist das unvermeidlich (zB DB-Zugriff). Wenn Code keine Nebenwirkungen hat, ist DI nur nutzlose Komplexität.
Warbo
13
@CarlLeth: DI ist eine Lösung für das Problem, Code isoliert testbar zu machen, wenn er Abhängigkeiten aufweist, die dies verbieten. Es reduziert jedoch weder die Gesamtkomplexität noch macht es den Code besser lesbar, was bedeutet, dass die Wartbarkeit nicht unbedingt erhöht wird. Wenn jedoch alle diese Abhängigkeiten durch eine bessere Trennung von Bedenken beseitigt werden können, werden die Vorteile von DI auf perfekte Weise "aufgehoben", da die Notwendigkeit von DI aufgehoben wird. Dies ist häufig eine bessere Lösung, um Code gleichzeitig testbarer und wartbarer zu machen.
Doc Brown
5
@Warbo Dies war das Original und wahrscheinlich immer noch die einzig gültige Verwendung von Spott. Selbst an Systemgrenzen wird es selten benötigt. Die Leute verschwenden wirklich viel Zeit damit, fast wertlose Tests zu erstellen und zu aktualisieren.
Frank Hileman
6
@CarlLeth: ok, jetzt sehe ich wo das missverständnis herkommt. Sie sprechen über die Abhängigkeit Inversion . Aber die Frage, diese Antwort und meine Kommentare beziehen sich auf DI = Depency Injection .
Doc Brown
36

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.

Eric
quelle
Ich möchte auch hinzufügen, dass Ihre Startzeiten umso länger werden, je mehr Sie injizieren. Die meisten DI-Frameworks erstellen alle injizierbaren Singleton-Instanzen zum Startzeitpunkt, unabhängig davon, wo sie verwendet werden.
Rodney P. Barbati
7
Wenn Sie eine Klasse mit realer Implementierung testen möchten (kein Mock), können Sie Funktionstests schreiben - Tests, die Unit-Tests ähneln, aber keine Mocks verwenden.
BЈови18
2
Ich denke, Ihr zweiter Absatz muss nuancierter sein: DI an sich macht es nicht schwierig, durch Code zu navigieren. Im einfachsten Fall ist DI einfach eine Folge der Befolgung von SOLID. Was die Komplexität erhöht, ist die Verwendung unnötiger Indirektions- und DI-Frameworks . Davon abgesehen trifft diese Antwort den Nagel auf den Kopf.
Konrad Rudolph
4
Die Abhängigkeitsinjektion ist auch außerhalb der Fälle, in denen sie wirklich benötigt wird, ein Warnsignal dafür, dass möglicherweise ein anderer überflüssiger Code in großer Menge vorhanden ist. Führungskräfte sind oft überrascht, dass Entwickler der Komplexität zuliebe Komplexität hinzufügen.
Frank Hileman
1
+1. Dies ist die eigentliche Antwort auf die gestellte Frage und sollte die akzeptierte Antwort sein.
Mason Wheeler
13

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 newist in Ordnung.

rauben
quelle
5
Beachten Sie, dass er auch unterschiedliche Ratschläge für OO und funktionale Sprachen gibt blog.ploeh.dk/2017/01/27/…
jk.
1
Das ist ein guter Punkt. Wenn wir standardmäßig Schnittstellen für jede Abhängigkeit erstellen, dann ist das irgendwie gegen YAGNI.
Landeeyo
4
Können Sie einen Verweis auf "Dependency Injection in .NET" geben ?
Peter Mortensen
1
Wenn Sie Unit-Tests durchführen, ist Ihre Abhängigkeit höchstwahrscheinlich volatil.
Jaquez
11
Das Gute ist, dass Entwickler die Zukunft immer mit höchster Genauigkeit vorhersagen können.
Frank Hileman
5

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.

Vilx-
quelle
3

Schnelle Aktivierung von Switching-Implementierungen (z. B. DbLogger anstelle von ConsoleLogger)

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 ILoggerals 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.

Anzahl der Klassen erhöht

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).

Schaffung unnötiger Schnittstellen

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.

Sollten wir die Abhängigkeitsinjektion verwenden, wenn wir nur eine Implementierung haben?

Ja, aber keine zusätzlichen Heizplatten verwenden .

Sollten wir verbieten, neue Objekte zu erstellen, mit Ausnahme von Sprach- / Framework-Objekten?

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).

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?

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).

maaartinus
quelle
-4

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 ...

class ClassUnderTest {

   protected final ADependency;
   protected final AnotherDependency;

   // Call from a factory or use an initializer 
   public void initializeDependencies() {
      aDependency = new ADependency();
      anotherDependency = new AnotherDependency();
   }
}

class TestClassUnderTest extends ClassUnderTest {

    @Override
    public void initializeDependencies() {
      aDependency = new MockitoObject();
      anotherDependency = new MockitoObject();
    }

    // Unit tests go here...
    // Unit tests call base class methods
}

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.

Rodney P. Barbati
quelle
6
Sie haben nicht das Äquivalent, sondern das Gegenteil der Abhängigkeitsinjektion beschrieben. In Ihrem Modell muss jedes Objekt die konkrete Implementierung aller seiner Abhängigkeiten kennen, aber mit DI, das in die Verantwortung der "Haupt" -Komponente fällt, müssen die entsprechenden Implementierungen zusammengeklebt werden. Bedenken Sie, dass DI Hand in Hand mit der anderen DI - Abhängigkeitsinversion geht, bei der Komponenten auf hoher Ebene keine starken Abhängigkeiten von Komponenten auf niedriger Ebene haben sollen.
Logan Pickup
1
Es ist ordentlich, wenn Sie nur eine Ebene der Klassenvererbung und eine Ebene der Abhängigkeiten haben. Wahrlich, es wird zur Hölle auf Erden, wenn es sich ausdehnt.
Ewan
4
Wenn Sie Interfaces verwenden und initializeDependencies () in den Konstruktor verschieben, ist es dasselbe, aber übersichtlicher. Im nächsten Schritt können Sie durch Hinzufügen von Konstruktionsparametern alle Ihre TestClasses entfernen.
Ewan
5
Es ist so viel falsch daran. Wie andere bereits gesagt haben, handelt es sich bei Ihrem Beispiel mit dem DI-Äquivalent überhaupt nicht um eine Abhängigkeitsinjektion, sondern um die Antithese. Es zeigt einen völligen Mangel an Verständnis für das Konzept und führt auch andere potenzielle Fallstricke ein: Teilweise initialisierte Objekte sind ein Codegeruch wie Ewan schlägt vor, die Initialisierung in den Konstruktor zu verschieben und sie über Konstruktorparameter zu übergeben. Dann haben Sie DI ...
Mr.Mindor
3
Hinzufügen zu @ Mr.Mindor: Es gibt eine allgemeinere "sequentielle Kopplung" gegen Muster, die nicht nur für die Initialisierung gilt. Wenn Methoden eines Objekts (oder allgemeiner Aufrufe einer API) in einer bestimmten Reihenfolge ausgeführt werden müssen, z. B. barnur danach aufgerufen werden fookönnen, handelt es sich um eine fehlerhafte API. Es behauptet , Funktionalität ( bar) bereitzustellen , aber wir können sie nicht wirklich verwenden (da sie foomöglicherweise nicht aufgerufen wurde). Wenn Sie sich an Ihr initializeDependencies(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.
Warbo
-6

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.

Ewan
quelle
13
Ich bin nicht einverstanden mit Ihrer Behauptung, dass es bei DI um Unit-Tests geht. Das Ermöglichen von Unit-Tests ist nur einer der Vorteile von DI und wahrscheinlich nicht der wichtigste.
Robert Harvey
5
Ich bin mit Ihrer Annahme nicht einverstanden, dass Unit Testing und DI so nahe beieinander liegen. Mit einem Mock / Stub bringen wir die Testsuite ein bisschen mehr zum Lügen: Das zu testende System entfernt sich weiter vom realen System. Das ist objektiv schlecht. Manchmal überwiegt ein Plus: Verspottete FS-Aufrufe erfordern keine Bereinigung. Gespielte HTTP-Anforderungen sind schnell, deterministisch und funktionieren offline. usw. Im Gegensatz dazu wissen wir jedes Mal, wenn wir eine fest programmierte newMethode verwenden , dass während der Tests derselbe Code ausgeführt wurde, der in der Produktion ausgeführt wurde.
Warbo
8
Nein, es ist nicht "ist Unit Testing schlecht?" Unit-Tests sind nicht schlecht (niemand argumentiert dies) und es lohnt sich auf jeden Fall im Allgemeinen. Aber nicht alle Unit-Tests erfordern das Verspotten, und das Verspotten ist mit erheblichen Kosten verbunden, so dass es zumindest mit Bedacht eingesetzt werden sollte.
Konrad Rudolph
6
@Ewan Nach deinem letzten Kommentar glaube ich nicht, dass wir uns einig sind. Ich sage, dass die meisten Komponententests kein DI [Frameworks] benötigen , da die meisten Komponententests kein Verspotten erfordern. Tatsächlich würde ich dies sogar als Heuristik für die Codequalität verwenden: Wenn der größte Teil Ihres Codes ohne DI / Mock-Objekte nicht Unit-getestet werden kann, haben Sie fehlerhaften Code geschrieben, der zu richtig gekoppelt war. Die meisten Codes sollten stark entkoppelt sein, eine Verantwortung tragen und universell einsetzbar und für sich genommen trivial testbar sein.
Konrad Rudolph
5
@Ewan Ihr Link gibt eine gute Definition für Unit-Tests. Nach dieser Definition ist mein OrderBeispiel ein Komponententest: Es testet eine Methode (die totalMethode von Order). Sie beschweren sich, dass es Code aus 2 Klassen aufruft. Meine Antwort lautet also was ? Wir testen nicht "2 Klassen auf einmal", wir testen die totalMethode. 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.
Warbo