it-swarm.com.de

Wann ist es nicht angebracht, das Abhängigkeitsinjektionsmuster zu verwenden?

Seit ich automatisiertes Testen gelernt (und geliebt) habe, habe ich in fast jedem Projekt das Abhängigkeitsinjektionsmuster verwendet. Ist es immer angebracht, dieses Muster zu verwenden, wenn Sie mit automatisierten Tests arbeiten? Gibt es Situationen, in denen Sie die Verwendung der Abhängigkeitsinjektion vermeiden sollten?

133
Tom Squires

Grundsätzlich macht die Abhängigkeitsinjektion einige (normalerweise, aber nicht immer gültige) Annahmen über die Art Ihrer Objekte. Wenn diese falsch sind, ist DI möglicherweise nicht die beste Lösung:

  • Erstens im Grunde genommen DI geht davon aus, dass eine enge Kopplung von Objektimplementierungen IMMER schlecht ist. Dies ist die Essenz des Abhängigkeitsinversionsprinzips: "Eine Abhängigkeit sollte niemals von einer Konkretion hergestellt werden, sondern nur von einer Abstraktion.".

    Dies schließt das zu ändernde abhängige Objekt basierend auf einer Änderung der konkreten Implementierung. Eine Klasse, die speziell von ConsoleWriter abhängt, muss sich ändern, wenn die Ausgabe stattdessen in eine Datei gehen soll. Wenn die Klasse jedoch nur von einem IWriter abhängig ist, der eine Write () -Methode verfügbar macht, können wir den derzeit verwendeten ConsoleWriter durch einen FileWriter und unseren ersetzen Die abhängige Klasse würde den Unterschied nicht kennen (Liskhov-Substitutionsprinzip).

    Ein Design kann jedoch NIEMALS für alle Arten von Änderungen geschlossen werden. Wenn sich das Design der IWriter-Schnittstelle selbst ändert, um einen Parameter zu Write () hinzuzufügen, muss jetzt ein zusätzliches Codeobjekt (die IWriter-Schnittstelle) zusätzlich zu dem Implementierungsobjekt/der Implementierungsmethode und deren Verwendung (en) geändert werden. Wenn Änderungen an der tatsächlichen Schnittstelle wahrscheinlicher sind als Änderungen an der Implementierung dieser Schnittstelle, kann eine lose Kopplung (und das Lösen von lose gekoppelten Abhängigkeiten) mehr Probleme verursachen als lösen.

  • Zweitens und folglich DI geht davon aus, dass die abhängige Klasse NIE ein guter Ort ist, um eine Abhängigkeit zu erstellen. Dies gilt für das Prinzip der Einzelverantwortung. Wenn Sie Code haben, der eine Abhängigkeit erstellt und diese auch verwendet, muss die abhängige Klasse möglicherweise aus zwei Gründen geändert werden (eine Änderung der Verwendung OR die Implementierung), die gegen SRP verstößt.

    Das Hinzufügen von Indirektionsebenen für DI kann jedoch wiederum eine Lösung für ein nicht vorhandenes Problem sein. Wenn es logisch ist, Logik in eine Abhängigkeit zu kapseln, diese Logik jedoch die einzige solche Implementierung einer Abhängigkeit ist, ist es schmerzhafter, die lose gekoppelte Auflösung der Abhängigkeit (Injektion, Servicestandort, Factory) zu codieren, als dies der Fall wäre Verwenden Sie einfach new und vergessen Sie es.

  • Schließlich DI von Natur aus zentralisiert das Wissen über alle Abhängigkeiten UND deren Implementierungen. Dies erhöht die Anzahl der Referenzen, über die die Assembly, die die Injektion ausführt, verfügen muss, und verringert in den meisten Fällen NICHT die Anzahl der Referenzen, die für die Assemblys der tatsächlichen abhängigen Klassen erforderlich sind.

    ETWAS, ETWAS, muss Kenntnisse über die abhängige, die Abhängigkeitsschnittstelle und die Abhängigkeitsimplementierung haben, um "die Punkte zu verbinden" und diese Abhängigkeit zu erfüllen. DI tendiert dazu, all dieses Wissen auf einem sehr hohen Niveau zu platzieren, entweder in einem IoC-Container oder in dem Code, der "Haupt" -Objekte wie das Hauptformular oder den Controller erstellt, die die Abhängigkeiten hydratisieren (oder Factory-Methoden für sie bereitstellen) müssen. Dies kann eine Menge notwendigerweise eng gekoppelten Codes und eine Menge Assembly-Referenzen auf hoher Ebene Ihrer App platzieren, die dieses Wissen nur benötigt, um es vor den tatsächlich abhängigen Klassen zu "verbergen" (was aus einer sehr grundlegenden Perspektive die ist bester Ort, um dieses Wissen zu haben; wo es verwendet wird).

    Normalerweise werden diese Referenzen auch nicht weiter unten im Code entfernt. Ein Abhängiger muss weiterhin auf die Bibliothek verweisen, die die Schnittstelle für ihre Abhängigkeit enthält. Diese befindet sich an einer von drei Stellen:

    • alles in einer einzigen "Schnittstellen" -Baugruppe, die sehr anwendungsorientiert wird,
    • jeweils neben den primären Implementierungen, wodurch der Vorteil beseitigt wird, dass Abhängige nicht neu kompiliert werden müssen, wenn sich Abhängigkeiten ändern, oder
    • ein oder zwei pro Stück in hochkohäsiven Baugruppen, wodurch die Anzahl der Baugruppen erhöht wird, die "vollständigen Erstellungszeiten" drastisch erhöht und die Anwendungsleistung verringert werden.

    All dies, um ein Problem an Orten zu lösen, an denen es möglicherweise keine gibt.

124
KeithS

Außerhalb von Abhängigkeitsinjektions-Frameworks ist die Abhängigkeitsinjektion (über Konstruktorinjektion oder Setterinjektion) nahezu ein Nullsummenspiel: Sie verringern die Kopplung zwischen Objekt A und seiner Abhängigkeit B, aber jetzt muss jedes Objekt, das eine Instanz von A benötigt konstruiere auch Objekt B.

Sie haben die Kopplung zwischen A und B leicht reduziert, aber die Kapselung von A reduziert und die Kopplung zwischen A und jeder Klasse, die eine Instanz von A erstellen muss, erhöht, indem Sie sie auch an die Abhängigkeiten von A koppeln.

Die Abhängigkeitsinjektion (ohne Framework) ist also ungefähr genauso schädlich wie hilfreich.

Die zusätzlichen Kosten sind jedoch häufig leicht zu rechtfertigen: Wenn der Clientcode mehr über das Erstellen der Abhängigkeit weiß als das Objekt selbst, verringert die Abhängigkeitsinjektion die Kopplung tatsächlich. Ein Scanner weiß beispielsweise nicht viel darüber, wie er einen Eingabestream zum Analysieren von Eingaben erhält oder erstellt oder aus welcher Quelle der Clientcode Eingaben analysieren möchte. Daher ist die Konstruktorinjektion eines Eingabestreams die naheliegende Lösung.

Testen ist eine weitere Rechtfertigung, um Scheinabhängigkeiten verwenden zu können. Das sollte bedeuten, dass Sie einen zusätzlichen Konstruktor hinzufügen, der nur zum Testen verwendet wird und das Einfügen von Abhängigkeiten ermöglicht: Wenn Sie stattdessen Ihre Konstruktoren so ändern, dass immer Abhängigkeiten eingefügt werden müssen, müssen Sie plötzlich die Abhängigkeiten Ihrer Abhängigkeiten kennen, um Ihre Abhängigkeiten zu konstruieren direkte Abhängigkeiten, und Sie können keine Arbeit erledigen.

Es kann hilfreich sein, aber Sie sollten sich auf jeden Fall nach jeder Abhängigkeit fragen. Ist der Testvorteil die Kosten wert und möchte ich diese Abhängigkeit beim Testen wirklich verspotten?

Wenn ein Abhängigkeitsinjektionsframework hinzugefügt wird und die Konstruktion von Abhängigkeiten nicht an den Clientcode, sondern an das Framework delegiert wird, ändert sich die Kosten-Nutzen-Analyse erheblich.

In einem Abhängigkeitsinjektions-Framework sind die Kompromisse etwas anders. Was Sie verlieren, wenn Sie eine Abhängigkeit einfügen, ist die Fähigkeit, leicht zu wissen, auf welche Implementierung Sie sich verlassen, und die Verantwortung für die Entscheidung, auf welche Abhängigkeit Sie sich verlassen, auf einen automatisierten Auflösungsprozess zu verlagern (z. B. wenn wir ein @ Inject'ed Foo benötigen Es muss etwas geben, das @Provides Foo bietet und dessen injizierte Abhängigkeiten verfügbar sind, oder eine übergeordnete Konfigurationsdatei, die vorschreibt, welcher Anbieter für jede Ressource verwendet werden soll, oder eine Mischung aus beiden (z. B. möglicherweise) ein automatisierter Auflösungsprozess für Abhängigkeiten sein, der bei Bedarf mithilfe einer Konfigurationsdatei überschrieben werden kann).

Wie bei der Konstruktorinjektion ist der Vorteil davon wiederum sehr ähnlich zu den Kosten: Sie müssen nicht wissen, wer die Daten bereitstellt, auf die Sie sich verlassen, und ob es mehrere Potenziale gibt Anbieter müssen Sie nicht die bevorzugte Reihenfolge kennen, um nach Anbietern einzuchecken. Stellen Sie sicher, dass jeder Standort, an dem die Daten benötigt werden, für alle potenziellen Anbieter usw. überprüft wird, da all dies von der Abhängigkeitsinjektion auf hoher Ebene erledigt wird Plattform.

Ich persönlich habe zwar nicht viel Erfahrung mit DI-Frameworks, aber ich habe den Eindruck, dass sie mehr Nutzen als Kosten bieten, wenn die Kopfschmerzen beim Finden des richtigen Anbieters der von Ihnen benötigten Daten oder Dienste höhere Kosten verursachen als die Kopfschmerzen. Wenn etwas fehlschlägt, nicht sofort vor Ort zu wissen, welcher Code die fehlerhaften Daten geliefert hat, die einen späteren Fehler in Ihrem Code verursacht haben.

In einigen Fällen wurden bereits andere Muster, die Abhängigkeiten verschleiern (z. B. Service Locators), übernommen (und haben sich möglicherweise auch bewährt), als DI-Frameworks auf den Markt kamen, und die DI-Frameworks wurden übernommen, weil sie einen Wettbewerbsvorteil boten, z weniger Boilerplate-Code oder möglicherweise weniger, um den Anbieter von Abhängigkeiten zu verschleiern, wenn festgestellt werden muss, welcher Anbieter tatsächlich verwendet wird.

30
  1. wenn Sie Datenbankentitäten erstellen, sollten Sie lieber eine Factory-Klasse haben, die Sie stattdessen in Ihren Controller einfügen.

  2. wenn Sie primitive Objekte wie Ints oder Longs erstellen müssen. Außerdem sollten Sie die meisten Standardbibliotheksobjekte wie Datumsangaben, Hilfslinien usw. "von Hand" erstellen.

  3. wenn Sie Konfigurationszeichenfolgen einfügen möchten, ist es wahrscheinlich besser, einige Konfigurationsobjekte einzufügen (im Allgemeinen wird empfohlen, einfache Typen in aussagekräftige Objekte zu verpacken: int temperaturInCelsiusDegrees -> CelciusDeegree Temperatur)

Verwenden Sie den Service Locator nicht als Alternative zur Abhängigkeitsinjektion, sondern als Anti-Pattern. Weitere Informationen: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx

11
0lukasz0

Wenn Sie nichts gewinnen können, indem Sie Ihr Projekt wartbar und testbar machen.

Im Ernst, ich liebe IoC und DI im Allgemeinen und ich würde sagen, dass ich dieses Muster in 98% der Fälle unbedingt verwenden werde. Dies ist besonders wichtig in einer Mehrbenutzerumgebung, in der Ihr Code von verschiedenen Teammitgliedern und verschiedenen Projekten immer wieder verwendet werden kann, da er die Logik von der Implementierung trennt. Die Protokollierung ist ein hervorragendes Beispiel dafür. Eine in eine Klasse eingefügte ILog-Schnittstelle ist tausendmal wartbarer als das einfache Einstecken Ihres Protokollierungsframeworks, da Sie nicht garantieren können, dass ein anderes Projekt dasselbe Protokollierungsframework verwendet (sofern es verwendet wird) überhaupt eine!).

Es gibt jedoch Zeiten, in denen es sich nicht um ein anwendbares Muster handelt. Beispielsweise können für funktionale Einstiegspunkte, die von einem nicht überschreibbaren Initialisierer in einem statischen Kontext implementiert werden (WebMethods, ich sehe Sie, aber Ihre Main () -Methode in Ihrer Program-Klasse ist ein weiteres Beispiel), bei der Initialisierung einfach keine Abhängigkeiten eingefügt werden Zeit. Ich würde auch so weit gehen zu sagen, dass ein Prototyp oder ein wegwerfbarer Untersuchungscode auch ein schlechter Kandidat ist; Die Vorteile von DI sind so ziemlich mittel- bis langfristige Vorteile (Testbarkeit und Wartbarkeit), wenn Sie sicher sind, dass Sie den größten Teil eines Codes innerhalb einer Woche wegwerfen werden, würde ich sagen, dass Sie durch Isolieren nichts gewinnen Abhängigkeiten, verbringen Sie einfach die Zeit, die Sie normalerweise damit verbringen würden, Abhängigkeiten zu testen und zu isolieren, damit der Code funktioniert.

Alles in allem ist es sinnvoll, eine pragmatische Herangehensweise an eine Methodik oder ein Muster zu wählen, da in 100% der Fälle nichts anwendbar ist.

Eine Sache, die Sie beachten sollten, ist Ihr Kommentar zum automatisierten Testen: Meine Definition hierfür ist automatisiert funktional Tests, zum Beispiel Selenium-Tests mit Skript, wenn Sie sich in einem Webkontext befinden. Hierbei handelt es sich in der Regel um vollständig Black-Box-Tests, bei denen das Innenleben des Codes nicht bekannt sein muss. Wenn Sie sich auf Unit- oder Integrationstests beziehen, würde ich sagen, dass das DI-Muster fast immer auf jedes Projekt anwendbar ist, das stark auf diese Art von White-Box-Tests angewiesen ist, da Sie damit beispielsweise Dinge wie testen können Methoden, die die Datenbank berühren, ohne dass eine Datenbank vorhanden sein muss.

8
Ed James

Dies ist keine vollständige Antwort, sondern nur ein weiterer Punkt.

Wenn Sie eine Anwendung haben, die einmal gestartet wird und lange ausgeführt wird (wie eine Web-App), ist DI möglicherweise gut.

Wenn Sie eine Anwendung haben, die viele Male gestartet und kürzer ausgeführt wird (wie eine mobile App), möchten Sie den Container wahrscheinlich nicht.

2
Koray Tugay

Versuchen Sie, grundlegende OOP Prinzipien) zu verwenden: Verwenden Sie die Vererbung , um allgemeine Funktionen zu extrahieren. kapseln (verstecken) Dinge, die mit privaten/internen/geschützten Mitgliedern/Typen vor der Außenwelt geschützt werden sollen. Verwenden Sie ein leistungsfähiges Test-Framework, um Code nur für Tests einzufügen, zum Beispiel https://www.typemock.com/ oder https://www.telerik.com/products/mocking.aspx .

Dann versuchen Sie es mit DI neu zu schreiben und vergleichen Sie den Code, was Sie normalerweise mit DI sehen werden:

  1. Sie haben mehr Schnittstellen (mehr Typen)
  2. Sie haben Duplikate von Signaturen öffentlicher Methoden erstellt und müssen diese doppelt beibehalten (Sie können einige Parameter nicht einfach einmal ändern, sondern müssen sie zweimal ausführen, da im Grunde alle Refactoring- und Navigationsmöglichkeiten komplizierter werden).
  3. Sie haben einige Kompilierungsfehler verschoben, um Laufzeitfehler zu vermeiden (mit DI können Sie einige Abhängigkeiten beim Codieren einfach ignorieren und sind sich nicht sicher, ob sie beim Testen angezeigt werden).
  4. Sie haben Ihre Kapselung geöffnet. Jetzt wurden geschützte Mitglieder, interne Typen usw. öffentlich
  5. Die Gesamtmenge an Code wurde erhöht

Ich würde sagen, was ich gesehen habe, ist fast immer, dass die Codequalität mit DI abnimmt.

Wenn Sie jedoch nur den Modifikator "public" access in der Klassendeklaration und/oder den Modifikator public/private für Mitglieder verwenden und/oder keine Wahl haben, teure Testtools zu kaufen, und gleichzeitig Unit-Tests benötigen, die dies können. ' Wenn Sie nicht durch Integrationstests ersetzt werden und/oder bereits Schnittstellen für Klassen haben, die Sie injizieren möchten, ist DI eine gute Wahl!

p.s. Wahrscheinlich werde ich viele Minuspunkte für diesen Beitrag bekommen, glaube ich, weil die meisten modernen Entwickler einfach nicht verstehen, wie und warum sie internes Schlüsselwort und verwenden sollen Versuchen Sie einfach zu codieren und zu vergleichen, wie Sie die Kopplung Ihrer Komponenten verringern und warum Sie sie verringern können

1
Eugene Gorbovoy

Während sich die anderen Antworten auf die technischen Aspekte konzentrieren, möchte ich eine praktische Dimension hinzufügen.

Im Laufe der Jahre bin ich zu dem Schluss gekommen, dass einige praktische Anforderungen erfüllt sein müssen, wenn die Einführung der Abhängigkeitsinjektion erfolgreich sein soll.

  1. Es sollte einen Grund geben, es einzuführen.

    Das klingt offensichtlich, aber wenn Ihr Code nur Dinge aus der Datenbank erhält und diese ohne Logik zurückgibt, macht das Hinzufügen eines DI-Containers die Dinge komplexer und ohne wirklichen Nutzen. Integrationstests wären hier wichtiger.

  2. Das Team muss geschult und an Bord sein.

    Es sei denn, die Mehrheit des Teams ist an Bord und versteht, dass DI das Hinzufügen der Inversion des Kontrollcontainers zu einer weiteren Möglichkeit macht und die Codebasis noch komplizierter macht.

    Wenn der DI von einem neuen Mitglied des Teams eingeführt wird, weil er es versteht und mag und nur zeigen möchte, dass er gut ist UND das Team nicht aktiv beteiligt ist, besteht ein echtes Risiko, dass es tatsächlich die Qualität von verringert der Code.

  3. Sie müssen testen

    Während das Entkoppeln im Allgemeinen eine gute Sache ist, kann DI die Auflösung einer Abhängigkeit von der Kompilierungszeit zur Laufzeit verschieben. Dies ist eigentlich ziemlich gefährlich, wenn Sie nicht gut testen. Laufzeitauflösungsfehler können kostspielig sein, um sie zu verfolgen und zu beheben.
    (Aus Ihrem Test geht hervor, dass Sie dies tun, aber viele Teams testen nicht in dem von DI geforderten Umfang.)

1
tymtam

Ein Alternative zur Abhängigkeitsinjektion ist die Verwendung eines Service Locator . Ein Service Locator ist einfacher zu verstehen, zu debuggen und vereinfacht das Erstellen eines Objekts, insbesondere wenn Sie kein DI-Framework verwenden. Service Locators sind ein gutes Muster für die Verwaltung externer statischer Abhängigkeiten , zum Beispiel eine Datenbank, die Sie sonst übergeben müssten in jedes Objekt in Ihrer Datenzugriffsschicht.

Wenn Legacy-Code umgestaltet wird, ist es häufig einfacher, eine Umgestaltung auf einen Service Locator vorzunehmen als auf Dependency Injection. Alles, was Sie tun, ist, Instanziierungen durch Service-Lookups zu ersetzen und dann den Service in Ihrem Unit-Test vorzutäuschen.

Der Service Locator weist jedoch einige Nachteile auf. Das Erkennen der Abhängigkeiten einer Klasse ist schwieriger, da die Abhängigkeiten in der Implementierung der Klasse verborgen sind, nicht in Konstruktoren oder Setzern. Das Erstellen von zwei Objekten, die auf unterschiedlichen Implementierungen desselben Dienstes beruhen, ist schwierig oder unmöglich.

[~ # ~] tldr [~ # ~] : Wenn Ihre Klasse statische Abhängigkeiten aufweist oder Sie Legacy-Code umgestalten, ist ein Service Locator wahrscheinlich besser als DI.

0
Garrett Hall