Seitdem ich automatisiertes Testen gelernt (und geliebt) habe, habe ich festgestellt, dass ich das Abhängigkeitsinjektionsmuster in fast jedem Projekt verwendet habe. Ist es immer angebracht, dieses Muster zu verwenden, wenn Sie mit automatisierten Tests arbeiten? Gibt es Situationen, in denen Sie die Abhängigkeitsinjektion vermeiden sollten?
design-patterns
dependency-injection
Tom Squires
quelle
quelle
Antworten:
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 geht DI grundsätzlich davon aus, dass eine enge Kopplung von Objektimplementierungen IMMER schlecht ist . Dies ist die Essenz des Abhängigkeitsumkehrprinzips: "Eine Abhängigkeit sollte niemals von einer Konkretion, sondern nur von einer Abstraktion hergestellt werden."
Dadurch wird das abhängige zu ändernde Objekt aufgrund einer Änderung an der konkreten Implementierung geschlossen. Eine von ConsoleWriter abhängige Klasse muss sich ändern, wenn die Ausgabe stattdessen in eine Datei erfolgen soll. Wenn die Klasse jedoch nur von einem IWriter abhängt, der eine Write () -Methode verfügbar macht, können wir den derzeit verwendeten ConsoleWriter durch einen FileWriter und unseren ersetzen abhängige Klasse würde den Unterschied nicht kennen (Liskhov Substitution Principle).
Ein Design kann jedoch NIEMALS für alle Arten von Änderungen geschlossen werden. Wenn sich das Design der IWriter-Schnittstelle selbst ändert, um Write () einen Parameter hinzuzufügen, muss jetzt ein zusätzliches Codeobjekt (die IWriter-Schnittstelle) über das Implementierungsobjekt / die Implementierungsmethode und deren Verwendung (en) geändert werden. Wenn Änderungen an der tatsächlichen Schnittstelle wahrscheinlicher sind als Änderungen an der Implementierung der Schnittstelle, kann eine lose Kopplung (und das Entstehen lose gekoppelter Abhängigkeiten) mehr Probleme verursachen, als sie löst.
Zweitens geht DI davon aus, dass die abhängige Klasse NIEMALS ein guter Ort ist, um eine Abhängigkeit zu erstellen . Dies gilt für das Prinzip der einheitlichen Verantwortung. Wenn Sie Code haben, der eine Abhängigkeit erstellt und auch verwendet, kann es zwei Gründe geben, die die abhängige Klasse ändern muss (eine Änderung der Verwendung ODER der Implementierung), wodurch die SRP verletzt wird.
Das Hinzufügen von Indirektionsebenen für DI kann jedoch auch 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 derartige Implementierung einer Abhängigkeit ist, ist es schmerzhafter, die lose gekoppelte Auflösung der Abhängigkeit (Einspeisung, Servicestandort, Fabrik) zu codieren, als dies der Fall wäre einfach benutzen
new
und vergessen.Schließlich zentralisiert DI von Natur aus das Wissen über alle Abhängigkeiten UND ihre Implementierungen . Dies erhöht die Anzahl der Referenzen, die die Assembly haben muss, die die Injektion durchführt, und verringert in den meisten Fällen NICHT die Anzahl der Referenzen, die von den Assemblys der tatsächlich abhängigen Klassen benötigt werden.
Irgendetwas muss Kenntnis über die abhängige Schnittstelle, die Abhängigkeitsschnittstelle und die Abhängigkeitsimplementierung haben, um "die Punkte zu verbinden" und diese Abhängigkeit zu erfüllen. DI neigt dazu, all dieses Wissen auf einer sehr hohen Ebene 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 müssen (oder Factory-Methoden dafür bereitstellen müssen). Dies kann eine Menge notwendigerweise eng gekoppelten Codes und eine Menge Assembler-Referenzen auf hohen Ebenen Ihrer App platzieren, die nur dieses Wissen benötigen, um es vor den tatsächlich abhängigen Klassen zu "verbergen" (was aus einer sehr grundlegenden Perspektive das ist) der beste Ort, um dieses Wissen zu haben (wo es verwendet wird).
Normalerweise werden die Verweise auch nicht aus dem unteren Code-Bereich entfernt. Ein Abhängiger muss weiterhin auf die Bibliothek verweisen, die die Schnittstelle für seine Abhängigkeit enthält. Diese befindet sich an einer von drei Stellen:
All dies, um ein Problem an Orten zu lösen, an denen es möglicherweise keine gibt.
quelle
Außerhalb von Abhängigkeitsinjektions-Frameworks ist die Abhängigkeitsinjektion (über Konstruktorinjektion oder Setterinjektion) fast 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, eine Instanz von A sein konstruiere auch Objekt B.
Sie haben die Kopplung zwischen A und B geringfügig reduziert, aber die Einkapselung von A verringert 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 ebenso schädlich wie hilfreich.
Die zusätzlichen Kosten sind jedoch häufig leicht zu rechtfertigen: Wenn der Client-Code mehr über die Erstellung der Abhängigkeit als das Objekt selbst weiß, verringert die Abhängigkeitsinjektion die Kopplung tatsächlich. Ein Scanner weiß beispielsweise nicht viel darüber, wie ein Eingabestream abgerufen oder erstellt wird, von dem Eingaben analysiert werden sollen, oder von welcher Quelle der Client-Code Eingaben analysieren soll. Daher ist die Konstruktorinjektion eines Eingabestreams die naheliegende Lösung.
Testen ist eine weitere Rechtfertigung, um Scheinabhängigkeiten verwenden zu können. Das bedeutet, 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 die 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 fragen, ob der Testvorteil die Kosten wert ist und ob ich diese Abhängigkeit beim Testen wirklich verspotten möchte.
Wenn ein Abhängigkeitsinjektionsframework hinzugefügt und die Erstellung 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 unterschiedlich. Wenn Sie eine Abhängigkeit injizieren, verlieren Sie die Fähigkeit, leicht zu erkennen, auf welche Implementierung Sie sich verlassen, und die Verantwortung für die Entscheidung, auf welche Abhängigkeit Sie sich verlassen, auf einen automatisierten Lösungsprozess zu verlagern (z. B. wenn wir einen @ Inject'ed Foo benötigen) Es muss etwas geben, das @Provides Foo bereitstellt und dessen eingebundene Abhängigkeiten verfügbar sind, oder eine Konfigurationsdatei auf hoher Ebene, die angibt, welcher Anbieter für jede Ressource verwendet werden soll, oder eine Mischung aus beidem (z. B.) ein automatisierter Lösungsprozess für Abhängigkeiten sein, der bei Bedarf mithilfe einer Konfigurationsdatei überschrieben werden kann).
Wie bei der Konstruktorinjektion ist der Vorteil auch hier 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 zu suchen. Stellen Sie sicher, dass jeder Standort, an dem die Daten benötigt werden, nach allen potenziellen Anbietern usw. sucht, da all dies von der Abhängigkeitsinjektion auf hohem Niveau behandelt wird Plattform.
Ich persönlich habe nicht viel Erfahrung mit DI-Frameworks, aber ich habe den Eindruck, dass sie mehr Nutzen als Kosten bringen, wenn die Kopfschmerzen bei der Suche nach dem richtigen Anbieter der von Ihnen benötigten Daten oder Dienste höher sind als die Kopfschmerzen. Wenn etwas fehlschlägt, müssen Sie nicht sofort lokal wissen, welcher Code die fehlerhaften Daten geliefert hat, die einen späteren Fehler in Ihrem Code verursacht haben.
In einigen Fällen wurden andere Muster, die Abhängigkeiten verdecken (z. B. Service Locators), bereits übernommen (und haben sich möglicherweise auch bewährt), als DI-Frameworks auf dem Markt erschienen, und die DI-Frameworks wurden übernommen, weil sie einen Wettbewerbsvorteil boten, z. B. das Erfordernis weniger Boilerplate-Code oder möglicherweise weniger, um den Abhängigkeitsanbieter zu verdecken, wenn ermittelt werden muss, welcher Anbieter tatsächlich verwendet wird.
quelle
Wenn Sie Datenbankentitäten erstellen, sollten Sie lieber eine Factory-Klasse haben, die Sie stattdessen in Ihren Controller einfügen.
wenn Sie primitive Objekte wie Ints oder Longs erstellen müssen. Außerdem sollten Sie die meisten Standardbibliotheksobjekte wie Daten, Anleitungen usw. "von Hand" erstellen.
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 einzufügen: int temperatureInCelsiusDegrees -> CelciusDeegree temperature).
Und verwenden Sie den Service Locator nicht als Alternative zur Abhängigkeitsinjektion, er ist ein Anti-Pattern. Weitere Informationen: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
quelle
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 Sie Code immer wieder von verschiedenen Teammitgliedern und verschiedenen Projekten wiederverwenden können, da die Logik von der Implementierung getrennt wird. Die Protokollierung ist ein Paradebeispiel dafür. Eine in eine Klasse injizierte 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) eine überhaupt!).
Es gibt jedoch Zeiten, in denen es sich nicht um ein anwendbares Muster handelt. Beispielsweise können funktionale Einstiegspunkte, die in einem statischen Kontext von einem nicht überschreibbaren Initialisierer implementiert werden (WebMethods, ich sehe Sie, aber Ihre Main () - Methode in Ihrer Program-Klasse ist ein anderes Beispiel), bei der Initialisierung einfach keine Abhängigkeiten enthalten Zeit. Ich würde auch so weit gehen zu sagen, dass ein Prototyp oder ein Wegwerf-Code ebenfalls ein schlechter Kandidat ist. Die Vorteile von DI sind im Wesentlichen mittel- bis langfristige Vorteile (Testbarkeit und Wartbarkeit), wenn Sie sicher sind, dass Sie den größten Teil eines Codeteils innerhalb einer Woche wegwerfen werden, würde ich sagen, dass Sie durch das Isolieren nichts gewinnen Abhängigkeiten, verbringen Sie nur die Zeit, die Sie normalerweise damit verbringen würden, Abhängigkeiten zu testen und zu isolieren, um den Code zum Laufen zu bringen.
Alles in allem ist es sinnvoll, eine Methode oder ein Muster pragmatisch zu betrachten, da nichts zu 100% anwendbar ist.
Zu beachten ist Ihr Kommentar zu automatisierten Tests: Meine Definition hierfür sind automatisierte Funktionstests , z. B. Skript-Selentests, wenn Sie sich in einem Webkontext befinden. Hierbei handelt es sich in der Regel um vollständige Black-Box-Tests, bei denen die Funktionsweise 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 sich stark auf diese Art von White-Box-Tests stützt, da es Ihnen beispielsweise ermöglicht, Dinge wie zu testen Methoden, die die Datenbank berühren, ohne dass eine Datenbank vorhanden sein muss.
quelle
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 mehrere praktische Anforderungen erfüllt werden müssen, damit die Einführung von Dependency Injection erfolgreich sein kann.
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, ohne dass ein wirklicher Nutzen daraus entsteht. Wichtiger wäre hier das Testen der Integration.
Das Team muss geschult und an Bord sein.
Es sei denn, die Mehrheit des Teams ist an Bord und weiß, dass das Hinzufügen der Inversion des Kontrollcontainers eine weitere Möglichkeit ist, Dinge zu tun, und die Codebasis noch komplizierter macht.
Wenn der DI von einem neuen Mitglied des Teams eingeführt wird, weil sie ihn verstehen und mögen und nur zeigen wollen, dass sie gut sind, UND das Team nicht aktiv beteiligt ist, besteht das reale Risiko, dass er tatsächlich die Qualität von mindert der Code.
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. Fehler bei der Laufzeitauflösung 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.)
quelle
Dies ist keine vollständige Antwort, sondern nur ein weiterer Punkt.
Wenn Sie eine Anwendung haben, die einmal gestartet wurde, wird sie möglicherweise über einen längeren Zeitraum ausgeführt (wie eine Webanwendung). DI ist möglicherweise hilfreich.
Wenn Sie eine Anwendung haben, die häufig gestartet und kürzer ausgeführt wird (wie eine mobile Anwendung), möchten Sie den Container wahrscheinlich nicht.
quelle
Versuchen Sie, grundlegende OOP-Prinzipien zu verwenden: Verwenden Sie die Vererbung , um allgemeine Funktionen zu extrahieren, Dinge zu kapseln (verbergen), die durch private / interne / geschützte Mitglieder / Typen vor der Außenwelt geschützt werden sollen. Verwenden Sie ein leistungsfähiges Test-Framework, um Code nur für Tests einzufügen, z. B. https://www.typemock.com/ oder https://www.telerik.com/products/mocking.aspx .
Dann versuchen neu zu schreiben es mit DI und vergleicht Code, was Sie in der Regel mit DI sehen:
Ich würde sagen, nach allem, was ich gesehen habe, nimmt die Codequalität mit DI fast immer ab.
Wenn Sie jedoch in der Klassendeklaration nur den Zugriffsmodifikator "public" und / oder den Modifikator "public / private" für Mitglieder verwenden und / oder keine Wahl haben, teure Testtools zu kaufen, und gleichzeitig einen Komponententest benötigen, der Folgendes kann: t durch integrative Tests ersetzt werden, und / oder Sie haben bereits Schnittstellen für Klassen, die Sie zu injizieren denken, DI ist eine gute Wahl!
ps wahrscheinlich bekomme ich viele Minuspunkte für diesen Post, ich glaube, weil die meisten modernen Entwickler einfach nicht verstehen, wie und warum man interne Schlüsselwörter verwendet und wie man die Kopplung Ihrer Komponenten verringert und warum man sie schließlich verringert) versuche zu codieren und zu vergleichen
quelle
Eine Alternative zu Dependency Injection 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 , z. B. einer Datenbank, die Sie ansonsten an jedes Objekt in Ihrer Datenzugriffsebene übergeben müssten.
Beim Refactoring von Legacy-Code ist es häufig einfacher, eine Refactoring-Funktion für einen Service-Locator auszuführen als für die Abhängigkeitsinjektion. Alles, was Sie tun, ist, Instantiierungen durch Service-Lookups zu ersetzen und dann den Service in Ihrem Komponententest herauszufälschen.
Der Service Locator hat jedoch einige Nachteile . Das Erkennen der Abhängigkeiten einer Klasse ist schwieriger, da die Abhängigkeiten in der Implementierung der Klasse und nicht in Konstruktoren oder Setzern verborgen sind. Das Erstellen von zwei Objekten, für die unterschiedliche Implementierungen desselben Dienstes erforderlich sind, ist schwierig oder unmöglich.
TLDR : Wenn Ihre Klasse statische Abhängigkeiten aufweist oder Sie Legacy-Code überarbeiten , ist ein Service Locator wahrscheinlich besser als DI.
quelle