Was ist Abhängigkeitsinjektion (DI)?
Wie bereits erwähnt, entfällt durch Dependency Injection (DI) die Verantwortung für die direkte Erstellung und Verwaltung der Lebensdauer anderer Objektinstanzen, von denen unsere Interessenklasse (Verbraucherklasse) abhängig ist (im UML-Sinne ). Diese Instanzen werden stattdessen an unsere Consumer-Klasse übergeben, normalerweise als Konstruktorparameter oder über Eigenschaftssetter (die Verwaltung der Instanzierung des Abhängigkeitsobjekts und die Übergabe an die Consumer-Klasse erfolgt normalerweise über einen IoC- Container (Inversion of Control) , aber das ist ein anderes Thema). .
DI, DIP und SOLID
Insbesondere in dem Paradigma von Robert C Martin SOLID Prinzipien des Gegenstandes Designorientierte , DI
ist eine der möglichen Implementierungen des Dependency Inversion Principle (DIP) . Das DIP ist das D
des SOLID
Mantras - andere DIP - Implementierungen den Service Locator umfassen, und Plugin - Muster.
Ziel des DIP ist es, enge, konkrete Abhängigkeiten zwischen Klassen zu entkoppeln und stattdessen die Kopplung durch eine Abstraktion zu lösen, die über ein interface
, abstract class
oder erreicht werden kannpure virtual class
in Abhängigkeit von der verwendeten Sprache und Ansatz.
Ohne das DIP ist unser Code (ich habe diese "konsumierende Klasse" genannt) direkt an eine konkrete Abhängigkeit gekoppelt und oft mit der Verantwortung belastet, zu wissen, wie eine Instanz dieser Abhängigkeit erhalten und verwaltet wird, dh konzeptionell:
"I need to create/use a Foo and invoke method `GetBar()`"
Nach Anwendung des DIP wird die Anforderung gelockert, und die Sorge, die Lebensdauer der Foo
Abhängigkeit zu ermitteln und zu verwalten, wurde beseitigt:
"I need to invoke something which offers `GetBar()`"
Warum DIP (und DI) verwenden?
Das Entkoppeln von Abhängigkeiten zwischen Klassen auf diese Weise ermöglicht das einfache Ersetzen dieser Abhängigkeitsklassen durch andere Implementierungen, die auch die Voraussetzungen der Abstraktion erfüllen (z. B. kann die Abhängigkeit mit einer anderen Implementierung derselben Schnittstelle ausgetauscht werden). Darüber hinaus, wie andere erwähnt haben, möglicherweise die häufigste Grund für die Entkopplung von Klassen über das DIP darin, dass eine konsumierende Klasse isoliert getestet werden kann, da dieselben Abhängigkeiten jetzt gestoppt und / oder verspottet werden können.
Eine Konsequenz von DI ist, dass die Lebensdauerverwaltung von Abhängigkeitsobjektinstanzen nicht mehr von einer konsumierenden Klasse gesteuert wird, da das Abhängigkeitsobjekt jetzt an die konsumierende Klasse übergeben wird (über Konstruktor- oder Setter-Injection).
Dies kann auf verschiedene Arten betrachtet werden:
- Wenn die Lebensdauerkontrolle von Abhängigkeiten durch die konsumierende Klasse beibehalten werden muss, kann die Kontrolle wiederhergestellt werden, indem eine (abstrakte) Factory zum Erstellen der Abhängigkeitsklasseninstanzen in die Consumer-Klasse eingefügt wird. Der Verbraucher kann bei Bedarf Instanzen über a
Create
im Werk abrufen und diese Instanzen nach Abschluss entsorgen.
- Oder die Lebensdauerkontrolle von Abhängigkeitsinstanzen kann an einen IoC-Container übergeben werden (mehr dazu weiter unten).
Wann DI verwenden?
- Wenn wahrscheinlich eine gleichwertige Implementierung durch eine Abhängigkeit ersetzt werden muss,
- Jedes Mal, wenn Sie die Methoden einer Klasse isoliert von ihren Abhängigkeiten testen müssen,
- Wo die Unsicherheit über die Lebensdauer einer Abhängigkeit ein Experimentieren rechtfertigen kann (z. B. Hey,
MyDepClass
ist Thread sicher - was ist, wenn wir es zu einem Singleton machen und allen Verbrauchern dieselbe Instanz injizieren?)
Beispiel
Hier ist eine einfache C # -Implementierung. Angesichts der folgenden Konsumklasse:
public class MyLogger
{
public void LogRecord(string somethingToLog)
{
Console.WriteLine("{0:HH:mm:ss} - {1}", DateTime.Now, somethingToLog);
}
}
Obwohl es scheinbar harmlos ist, hat es zwei static
Abhängigkeiten von zwei anderen Klassen, System.DateTime
und System.Console
was nicht nur die Protokollierungsausgabeoptionen einschränkt (die Protokollierung an der Konsole ist wertlos, wenn niemand zuschaut), sondern schlimmer noch, es ist schwierig, angesichts der Abhängigkeit von automatisch zu testen eine nicht deterministische Systemuhr.
Wir können uns jedoch DIP
auf diese Klasse beziehen, indem wir das Problem der Zeitstempelung als Abhängigkeit abstrahieren und MyLogger
nur an eine einfache Schnittstelle koppeln:
public interface IClock
{
DateTime Now { get; }
}
Wir können auch die Abhängigkeit von Console
einer Abstraktion wie a lösen TextWriter
. Die Abhängigkeitsinjektion wird normalerweise entweder als constructor
Injektion (Übergabe einer Abstraktion an eine Abhängigkeit als Parameter an den Konstruktor einer konsumierenden Klasse) oder Setter Injection
(Übergabe der Abhängigkeit über einen setXyz()
Setter oder eine .Net-Eigenschaft mit {set;}
definiert) implementiert . Die Konstruktorinjektion wird bevorzugt, da dies garantiert, dass sich die Klasse nach der Erstellung in einem korrekten Zustand befindet und die internen Abhängigkeitsfelder als readonly
(C #) oder final
(Java) markiert werden können . Wenn wir also die Konstruktorinjektion für das obige Beispiel verwenden, haben wir Folgendes:
public class MyLogger : ILogger // Others will depend on our logger.
{
private readonly TextWriter _output;
private readonly IClock _clock;
// Dependencies are injected through the constructor
public MyLogger(TextWriter stream, IClock clock)
{
_output = stream;
_clock = clock;
}
public void LogRecord(string somethingToLog)
{
// We can now use our dependencies through the abstraction
// and without knowledge of the lifespans of the dependencies
_output.Write("{0:yyyy-MM-dd HH:mm:ss} - {1}", _clock.Now, somethingToLog);
}
}
(Es Clock
muss ein Beton bereitgestellt werden, auf den natürlich zurückgegriffen werden kann DateTime.Now
, und die beiden Abhängigkeiten müssen von einem IoC-Container über eine Konstruktorinjektion bereitgestellt werden.)
Es kann ein automatisierter Komponententest erstellt werden, der definitiv beweist, dass unser Logger ordnungsgemäß funktioniert, da wir jetzt die Kontrolle über die Abhängigkeiten - die Zeit - haben und die schriftliche Ausgabe ausspionieren können:
[Test]
public void LoggingMustRecordAllInformationAndStampTheTime()
{
// Arrange
var mockClock = new Mock<IClock>();
mockClock.Setup(c => c.Now).Returns(new DateTime(2015, 4, 11, 12, 31, 45));
var fakeConsole = new StringWriter();
// Act
new MyLogger(fakeConsole, mockClock.Object)
.LogRecord("Foo");
// Assert
Assert.AreEqual("2015-04-11 12:31:45 - Foo", fakeConsole.ToString());
}
Nächste Schritte
Die Abhängigkeitsinjektion ist ausnahmslos mit einem Inversion of Control-Container (IoC) verbunden , um die konkreten Abhängigkeitsinstanzen zu injizieren (bereitzustellen) und Lebensdauerinstanzen zu verwalten. Während des Konfigurations- / Bootstrapping-Prozesses können in IoC
Containern folgende Elemente definiert werden:
- Zuordnung zwischen jeder Abstraktion und der konfigurierten konkreten Implementierung (z. B. "Wenn ein Verbraucher eine anfordert
IBar
, eine ConcreteBar
Instanz zurückgibt" )
- Es können Richtlinien für die Lebensdauerverwaltung jeder Abhängigkeit eingerichtet werden, z. B. um ein neues Objekt für jede Verbraucherinstanz zu erstellen, eine Singleton-Abhängigkeitsinstanz für alle Verbraucher freizugeben, dieselbe Abhängigkeitsinstanz nur für denselben Thread zu verwenden usw.
- In .Net kennen IoC-Container Protokolle wie
IDisposable
und übernehmen die Verantwortung für Disposing
Abhängigkeiten gemäß der konfigurierten Lebensdauerverwaltung.
Sobald IoC-Container konfiguriert / gebootet wurden, arbeiten sie normalerweise nahtlos im Hintergrund, sodass sich der Codierer auf den jeweiligen Code konzentrieren kann, anstatt sich um Abhängigkeiten zu kümmern.
Der Schlüssel zu DI-freundlichem Code besteht darin, eine statische Kopplung von Klassen zu vermeiden und new () nicht zum Erstellen von Abhängigkeiten zu verwenden
Wie im obigen Beispiel erfordert das Entkoppeln von Abhängigkeiten einen gewissen Entwurfsaufwand, und für den Entwickler ist ein Paradigmenwechsel erforderlich, um die Gewohnheit, new
Abhängigkeiten direkt zu ändern, aufzuheben und stattdessen dem Container zu vertrauen, um Abhängigkeiten zu verwalten.
Es gibt jedoch viele Vorteile, insbesondere in der Möglichkeit, Ihre Interessenklasse gründlich zu testen.
Hinweis : Die Erstellung / Zuordnung / Projektion (über new ..()
) von POCO / POJO / Serialisierungs-DTOs / Entitätsdiagrammen / anonymen JSON-Projektionen ua - dh "Nur Daten" -Klassen oder Datensätze -, die von Methoden verwendet oder zurückgegeben werden, gelten nicht als Abhängigkeiten (in der UML-Sinn) und nicht DI unterworfen. Das Projizieren new
ist in Ordnung.