it-swarm.com.de

Kritik und Nachteile der Abhängigkeitsinjektion

Die Abhängigkeitsinjektion (DI) ist ein bekanntes und modisches Muster. Die meisten Ingenieure kennen die Vorteile wie:

  • Isolation beim Unit-Test möglich/einfach machen
  • Abhängigkeiten einer Klasse explizit definieren
  • Erleichterung eines guten Designs ( Prinzip der Einzelverantwortung (SRP) zum Beispiel)
  • Schnelle Aktivierung von Switching-Implementierungen (z. B. DbLogger anstelle von ConsoleLogger)

Ich denke, es besteht branchenweiter Konsens darüber, dass DI ein gutes, nützliches Muster ist. Im Moment gibt es nicht zu viel Kritik. Die in der Community erwähnten Nachteile sind normalerweise gering. Manche von ihnen:

  • Erhöhte Anzahl von Klassen
  • Erstellung unnötiger Schnittstellen

Derzeit diskutieren wir mit meinem Kollegen über Architekturdesign. Er ist ziemlich konservativ, aber aufgeschlossen. Er hinterfragt gerne Dinge, die ich für gut halte, weil viele Leute in der IT einfach 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 das Erstellen neuer Objekte außer der Sprache/des Frameworks verbieten?
  • Ist das Einfügen einer einzelnen Implementierung eine schlechte Idee (sagen wir, wir haben nur eine Implementierung, damit wir keine "leere" Schnittstelle erstellen möchten), wenn wir nicht vorhaben, eine bestimmte Klasse einem Unit-Test zu unterziehen?
130
Landeeyo

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 für das untergeordnete Objekt erforderlichen Abhängigkeiten bereit.

Das ist es. Beachten Sie, dass nichts darin Schnittstellen, Frameworks, irgendeine Art von Injektion usw. erfordert. Um fair zu sein, habe ich vor 20 Jahren zum ersten Mal von diesem Muster erfahren. Es ist nicht neu.

Aufgrund der Verwirrung von mehr als 2 Personen über den Begriff Eltern und Kind im Zusammenhang mit der Abhängigkeitsinjektion:

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

Die Abhängigkeitsinjektion ist ein Muster für die Objektzusammensetzung .

Warum Schnittstellen?

Schnittstellen sind ein Vertrag. Sie existieren, um zu begrenzen, wie eng zwei Objekte gekoppelt sein können. Nicht jede Abhängigkeit benötigt eine Schnittstelle, aber sie helfen 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 stoppelige Objekt, das Sie zum Testen von Code verwenden, der vom Objekt abhängt. Das allein kann für die Schnittstelle ausreichend sein.

Warum Frameworks?

Das Initialisieren und Bereitstellen von Abhängigkeiten zu untergeordneten Objekten kann bei einer großen Anzahl von Objekten entmutigend sein. Frameworks bieten die folgenden Vorteile:

  • Autowiring-Abhängigkeiten zu Komponenten
  • Konfigurieren der Komponenten mit Einstellungen
  • Automatisieren Sie den Kesselplattencode, damit Sie ihn nicht an mehreren Stellen sehen müssen.

Sie haben auch die folgenden Nachteile:

  • Das übergeordnete Objekt ist ein "Container" und nichts in Ihrem Code
  • Dies erschwert das Testen, wenn Sie die Abhängigkeiten nicht direkt in Ihrem Testcode angeben können
  • Es kann die Initialisierung verlangsamen, da alle Abhängigkeiten mithilfe von Reflektion und vielen anderen Tricks aufgelöst werden
  • Das Debuggen zur Laufzeit kann schwieriger sein, insbesondere wenn der Container einen Proxy zwischen der Schnittstelle und der eigentlichen 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.

Alles in allem gibt es Kompromisse. Für kleine Projekte, bei denen nicht viele bewegliche Teile vorhanden sind und es kaum einen Grund gibt, ein DI-Framework zu verwenden. Bei komplizierteren Projekten, bei denen bestimmte Komponenten bereits für Sie erstellt wurden, kann das Framework jedoch gerechtfertigt sein.

Was ist mit [zufälliger Artikel im Internet]?

Was ist damit? Oft können Menschen übereifrig werden und eine Reihe von Einschränkungen hinzufügen und Sie beschimpfen, wenn Sie die Dinge nicht auf die "einzig wahre Weise" tun. Es gibt keinen wahren Weg. Sehen Sie nach, ob Sie etwas Nützliches aus dem Artikel extrahieren und die Dinge ignorieren können, mit denen Sie nicht einverstanden sind.

Kurz gesagt, denken Sie selbst und probieren Sie die Dinge aus.

Arbeiten mit "alten Köpfen"

Lerne so viel wie möglich. Was du bei vielen Entwicklern finden wirst, die bis in die 70er Jahre arbeiten, ist, dass sie gelernt haben, nicht dogmatisch zu sein viele Dinge. Sie haben Methoden, mit denen sie seit Jahrzehnten arbeiten und die zu korrekten Ergebnissen führen.

Ich hatte das Privileg, mit einigen von ihnen zu arbeiten, und sie können ein brutal ehrliches Feedback geben, das sehr viel Sinn macht. Und wo sie Wert sehen, erweitern sie ihr Repertoire um diese Werkzeuge.

168
Berin Loritsch

Die Abhängigkeitsinjektion ist, wie die meisten Muster, ein Lösung für Probleme. Fragen Sie zunächst, ob Sie überhaupt ein Problem haben. Wenn nicht, wird der Code durch die Verwendung des Musters höchstwahrscheinlich schlechter .

Überlegen Sie zunächst, 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 weg sind, wird die Frage des Injizierens oder Nicht-Spritzens strittig!

Stellen Sie sich ein Modul vor, das einige Daten von einem externen Dienst herunterlädt, analysiert, komplexe Analysen durchführt und als Ergebnis in eine Datei schreibt.

Wenn die Abhängigkeit vom externen Dienst fest codiert ist, ist es wirklich schwierig, die interne Verarbeitung dieses Moduls einem Komponententest zu unterziehen. Sie können sich also entscheiden, den externen Dienst und das Dateisystem als Schnittstellenabhängigkeiten einzufügen, sodass Sie stattdessen Mocks einfügen können, was wiederum das Testen der internen Logik durch Unit-Tests ermöglicht.

Eine viel bessere Lösung besteht jedoch darin, die Analyse einfach von der Eingabe/Ausgabe zu trennen. Wenn die Analyse in ein Modul ohne Nebenwirkungen extrahiert wird, ist das Testen viel einfacher. Beachten Sie, dass das Verspotten ein Code-Geruch ist - es ist nicht immer vermeidbar, aber im Allgemeinen ist es besser, wenn Sie testen können, ohne sich auf das Verspotten verlassen zu müssen. Indem Sie die Abhängigkeiten beseitigen, vermeiden Sie die Probleme, die DI lindern 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 die 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 ändern, um B in den Konstruktor von A einzufügen, haben Sie dieses Implementierungsdetail verfügbar gemacht und wissen nun, wie diese Abhängigkeit initialisiert wird 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 einen Logger.

Das Problem ist, wenn Muster und Architekturen eher zu Zielen als zu Werkzeugen werden. Ich frage nur "Sollen wir DI verwenden?" ist eine Art Wagen vor das Pferd zu stellen. Sie sollten fragen: "Haben wir ein Problem?" und "Was ist die beste Lösung für dieses Problem?"

Ein Teil Ihrer Frage lautet: "Sollten wir überflüssige Schnittstellen erstellen, um die Anforderungen des Musters zu erfüllen?" Sie erkennen wahrscheinlich bereits die Antwort darauf - 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 nachahmt, wird als "Header-Schnittstelle" bezeichnet, und dies ist ein bekanntes Antimuster.

92
JacquesB

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 testen, wie die zu testende Klasse den von der Schnittstelle bereitgestellten Vertrag verwendet. Dies bietet jedoch eine viel größere Sicherheit dafür, dass die Eingabe der zu testenden Klasse in die Schnittstelle wie erwartet erfolgt. Es bietet eine eher schlechte Sicherheit, dass die zu testende Klasse wie erwartet auf die Ausgabe über die Schnittstelle reagiert, da es sich um eine fast universelle Scheinausgabe handelt, die selbst Fehlern, übermäßigen Vereinfachungen usw. ausgesetzt ist. Kurz gesagt, Sie können NICHT überprüfen, ob sich die Klasse bei einer echten Implementierung der Schnittstelle wie erwartet verhält.

Zweitens macht es DI viel schwieriger, durch Code zu navigieren. Beim 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 größeren Zeitsenke (z. B. wenn eine übermäßig generische Schnittstelle wie IDisposable verwendet wird) reichen. beim Versuch, die tatsächlich verwendete Implementierung zu finden. Dies kann eine einfache Übung wie "Ich muss eine Nullreferenzausnahme im Code beheben, die direkt nach dem Drucken dieser Protokollierungsanweisung auftritt" in einen tagelangen Aufwand verwandeln.

Drittens ist die Verwendung von DI und Frameworks ein zweischneidiges Schwert. Dies kann die Menge an Kesselplattencode, die für den allgemeinen Betrieb benötigt wird, erheblich reduzieren. Dies geht jedoch zu Lasten der Notwendigkeit detaillierter Kenntnisse des jeweiligen DI-Frameworks, um zu verstehen, wie diese gemeinsamen Operationen tatsächlich miteinander verbunden sind. Um zu verstehen, wie Abhängigkeiten in das Framework geladen werden, und um dem zu injizierenden Framework eine neue Abhängigkeit hinzuzufügen, müssen Sie möglicherweise eine ganze Menge Hintergrundmaterial lesen und einige grundlegende Tutorials zum Framework befolgen. Dies kann einige einfache Aufgaben zu ziemlich zeitaufwändigen machen.

37
Eric

Ich folgte Mark Seemanns Rat aus "Dependency Injection in .NET" - um zu vermuten.

DI sollte verwendet werden, wenn Sie eine "flüchtige Abhängigkeit" haben, z. Es besteht eine vernünftige Chance, dass sich dies ändert.

Wenn Sie also glauben, dass Sie in Zukunft möglicherweise mehr als eine Implementierung haben oder sich die Implementierung ändern könnte, verwenden Sie DI. Ansonsten ist new in Ordnung.

15
Rob

Mein größter Ärger über DI wurde bereits in einigen Antworten nebenbei erwähnt, aber ich werde es hier etwas näher erläutern. DI (wie es heute meistens mit Containern usw. gemacht wird) schadet WIRKLICH der Lesbarkeit des Codes. Und die Lesbarkeit von Code ist wohl der Grund für die meisten heutigen Programminnovationen. Wie jemand sagte - das Schreiben von Code ist einfach. Code zu lesen ist schwer. Aber es ist auch äußerst wichtig, es sei denn, Sie schreiben ein winziges Dienstprogramm zum einmaligen Wegwerfen.

Das Problem mit DI in dieser Hinsicht ist, dass es undurchsichtig ist. Der Container ist eine Black Box. Objekte erscheinen einfach von irgendwoher 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, rauchen alle "Gehe zur Definition" -Funktionen Ihres IDE] in Rauch auf. Es ist furchtbar schwierig, den Programmfluss zu verfolgen, ohne ihn auszuführen und nur einen Schritt zu machen durch, um zu sehen, WELCHE Implementierung der Schnittstelle an DIESER bestimmten Stelle verwendet wurde. Und gelegentlich gibt es eine technische Hürde, die Sie daran hindert, durchzugehen. Und selbst wenn Sie können, wenn es darum geht, durch den verdrehten Darm des DI-Containers zu gehen, das Ganze Affäre wird schnell zu einer Übung in Frustration.

Um effizient mit einem Code zu arbeiten, der DI verwendet, müssen Sie damit vertraut sein und bereits wissen was wohin geht.

7
Vilx-

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 explizit und klar zu machen. Es macht keinen Sinn, ILogger als Abhängigkeit von fast jeder Klasse aufzulisten - 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 benötige.

Erhöhte Anzahl von Klassen

Dies ist ein Nachteil des gegebenen DI-Frameworks oder des Mocking-Frameworks, nicht des DI selbst. In den meisten Fällen hängen meine Klassen von konkreten Klassen ab, was bedeutet, dass keine Heizplatte benötigt wird. Guice (a Java DI Framework) bindet standardmäßig eine Klasse an sich selbst und ich muss die Bindung nur in Tests überschreiben (oder sie stattdessen manuell verkabeln).

Erstellung unnötiger Schnittstellen

Ich erstelle die Schnittstellen nur bei Bedarf (was eher selten ist). Dies 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 zusätzliche Kesselplatte vermeiden .

Sollten wir das Erstellen neuer Objekte außer der Sprache/des Frameworks verbieten?

Nein. Es wird viele Wertklassen (unveränderlich) und Datenklassen (veränderlich) geben, in denen die Instanzen nur erstellt und weitergegeben werden und in denen es keinen Sinn macht, sie zu injizieren, da sie niemals in einem anderen Objekt (oder nur in einem anderen) gespeichert werden solche Objekte).

Für sie müssen Sie möglicherweise stattdessen eine Fabrik injizieren, aber meistens macht dies keinen Sinn (stellen Sie sich beispielsweise @Value class NamedUrl {private final String name; private final URL url;} Vor; Sie brauchen hier wirklich keine Fabrik und es gibt nichts zu injizieren).

Ist das Einfügen einer einzelnen Implementierung eine schlechte Idee (sagen wir, wir haben nur eine Implementierung, damit wir keine "leere" Schnittstelle erstellen möchten), wenn wir nicht vorhaben, eine bestimmte Klasse einem Unit-Test zu unterziehen?

IMHO ist es in Ordnung, solange es kein Aufblähen des Codes 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), für die ich mich entschieden habe von DI ausschließen , 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 zur Kompilierungszeit verschoben werden (für Java gibt es Dolch , keine Ahnung von anderen Sprachen).

Ein weiterer Nachteil ist die Magie, die überall auftritt und die reduziert werden kann (z. B. habe ich die Proxy-Erstellung deaktiviert, wenn ich Guice verwende).

3
maaartinus