Wir bereiten die Veröffentlichung der zweiten Ausgabe des legendären Buches von Mark Siman " Dependency Injection on the .NET Platform " vor.
Selbst in einem so umfangreichen Buch ist es kaum möglich, ein solches Thema vollständig zu behandeln. Wir bieten Ihnen jedoch eine Kurzübersetzung eines sehr leicht zugänglichen Artikels an, in dem das Wesentliche der Abhängigkeitsinjektion in einfacher Sprache beschrieben wird - mit Beispielen in C #.
In diesem Artikel wird das Konzept der Abhängigkeitsinjektion erläutert und gezeigt, wie es in einem bestimmten Projekt programmiert wird. Aus Wikipedia:
Die Abhängigkeitsinjektion ist ein Entwurfsmuster, das das Verhalten von der Abhängigkeitsauflösung trennt. Somit ist es möglich, stark voneinander abhängige Komponenten abzutrennen.
Mit Dependency Injection (oder DI) können Sie Implementierungen und Dienste für andere Klassen zum Verbrauch bereitstellen. Der Code bleibt sehr locker gekoppelt. Der Hauptpunkt in diesem Fall ist folgender: Anstelle von Implementierungen können Sie problemlos andere Implementierungen ersetzen, und gleichzeitig müssen Sie ein Minimum an Code ändern, da die Implementierung und der Verbraucher höchstwahrscheinlich nur durch einen Vertrag verbunden sind .
In C #, bedeutet dies , dass Ihre Service - Implementierungen müssen die Anforderungen der Schnittstelle gerecht zu werden , und wenn die Verbraucher für Ihre Dienste zu schaffen, müssen Sie das Ziel Schnittstelle , nicht die Implementierung und erfordern die Implementierung zur Verfügung gestellt oder werden Sie implementiert.damit Sie die Instanzen nicht selbst erstellen müssen. Mit diesem Ansatz müssen Sie sich auf Klassenebene keine Gedanken darüber machen, wie Abhängigkeiten erstellt werden und woher sie stammen. In diesem Fall ist nur der Vertrag wichtig.
Abhängigkeitsinjektion anhand eines Beispiels
Schauen wir uns ein Beispiel an, in dem DI nützlich sein kann. Erstellen wir zunächst eine Schnittstelle (Vertrag), über die wir eine Aufgabe ausführen können, z. B. eine Nachricht protokollieren:
public interface ILogger {
void LogMessage(string message);
}
Bitte beachten Sie: Diese Schnittstelle beschreibt nirgendwo, wie eine Nachricht protokolliert wird und wo sie protokolliert wird. Hier besteht die Absicht einfach darin, die Zeichenfolge in ein Repository zu schreiben. Als Nächstes erstellen wir eine Entität, die diese Schnittstelle verwendet. Angenommen, wir erstellen eine Klasse, die ein bestimmtes Verzeichnis auf der Festplatte verfolgt und die entsprechende Nachricht protokolliert, sobald eine Änderung am Verzeichnis vorgenommen wird:
public class DirectoryWatcher {
private ILogger _logger;
private FileSystemWatcher _watcher;
public DirectoryWatcher(ILogger logger) {
_logger = logger;
_watcher = new FileSystemWatcher(@ "C:Temp");
_watcher.Changed += new FileSystemEventHandler(Directory_Changed);
}
void Directory_Changed(object sender, FileSystemEventArgs e) {
_logger.LogMessage(e.FullPath + " was changed");
}
}
In diesem Fall ist es am wichtigsten zu beachten, dass wir den benötigten Konstruktor erhalten, der implementiert wird
ILogger
. Aber auch hier ist zu beachten: Es ist uns egal, wohin das Protokoll geht oder wie es erstellt wird. Wir können nur mit Blick auf die Benutzeroberfläche programmieren und an nichts anderes denken.
Um eine Instanz von uns zu erstellen
DirectoryWatcher
, benötigen wir daher auch eine vorgefertigte Implementierung
ILogger
. Lassen Sie uns fortfahren und eine Instanz erstellen, die Nachrichten in einer Textdatei protokolliert:
public class TextFileLogger: ILogger {
public void LogMessage(string message) {
using(FileStream stream = new FileStream("log.txt", FileMode.Append)) {
StreamWriter writer = new StreamWriter(stream);
writer.WriteLine(message);
writer.Flush();
}
}
}
Erstellen wir eine weitere, die Nachrichten in das Windows-Ereignisprotokoll schreibt:
public class EventFileLogger: ILogger {
private string _sourceName;
public EventFileLogger(string sourceName) {
_sourceName = sourceName;
}
public void LogMessage(string message) {
if (!EventLog.SourceExists(_sourceName)) {
EventLog.CreateEventSource(_sourceName, "Application");
}
EventLog.WriteEntry(_sourceName, message);
}
}
Wir haben jetzt zwei separate Implementierungen, die Nachrichten auf sehr unterschiedliche Weise protokollieren, aber beide
ILogger
, was bedeutet, dass beide verwendet werden können, wo immer eine Instanz benötigt wird
ILogger
. Als Nächstes können Sie eine Instanz erstellen
DirectoryWatcher
und sie anweisen, einen unserer Logger zu verwenden:
ILogger logger = new TextFileLogger();
DirectoryWatcher watcher = new DirectoryWatcher(logger);
Oder indem Sie einfach die rechte Seite der ersten Zeile ändern, können Sie eine andere Implementierung verwenden:
ILogger logger = new EventFileLogger();
DirectoryWatcher watcher = new DirectoryWatcher(logger);
All dies geschieht ohne Änderungen an der DirectoryWatcher-Implementierung, und dies ist das Wichtigste. Wir injizieren unsere Logger-Implementierung in den Consumer, damit der Consumer keine eigene Instanz erstellen muss. Das gezeigte Beispiel ist trivial, aber stellen Sie sich vor, wie es wäre, diese Techniken in einem Großprojekt zu verwenden, in dem Sie mehrere Abhängigkeiten haben und die von vielen Verbrauchern verwendet werden. Und dann gibt es plötzlich eine Anforderung, die Methode zum Protokollieren von Nachrichten zu ändern (z. B. sollten Nachrichten jetzt zu Überwachungszwecken auf dem SQL Server protokolliert werden). Wenn Sie die Abhängigkeitsinjektion in der einen oder anderen Form nicht verwenden, müssen Sie den Code sorgfältig überprüfen und Änderungen vornehmen, wo immer der Logger tatsächlich erstellt und dann verwendet wird. Bei einem großen Projekt kann eine solche Arbeit umständlich und fehleranfällig sein.Mit DI ändern Sie einfach die Abhängigkeit an einer Stelle, und der Rest der Anwendung übernimmt die Änderungen tatsächlich und beginnt sofort mit der Verwendung der neuen Protokollierungsmethode.
Im Wesentlichen wird das klassische Softwareproblem der starken Abhängigkeit gelöst, und mit DI können Sie lose gekoppelten Code erstellen, der äußerst flexibel und einfach zu ändern ist.
Abhängigkeitsinjektionsbehälter
Viele DI-Injection-Frameworks, die Sie einfach herunterladen und verwenden können, gehen noch einen Schritt weiter und verwenden einen Container für die Abhängigkeitsinjektion. Im Wesentlichen handelt es sich um eine Klasse, die Typzuordnungen speichert und eine registrierte Implementierung für einen bestimmten Typ zurückgibt. In unserem einfachen Beispiel können wir den Container nach einer Instanz abfragen
ILogger
und die Instanz
TextFileLogger
oder die Instanz , mit der wir den Container initialisiert haben, zurückgeben.
In diesem Fall haben wir den Vorteil, dass wir alle Typzuordnungen an einem Ort registrieren können, normalerweise dort, wo das Anwendungsstartereignis auftritt, und so schnell und klar erkennen können, welche Abhängigkeiten wir im System haben. Darüber hinaus können Sie in vielen professionellen Frameworks die Lebensdauer solcher Objekte konfigurieren, indem Sie entweder bei jeder neuen Anforderung neue Instanzen erstellen oder eine Instanz in mehreren Aufrufen wiederverwenden.
Der Container wird normalerweise so erstellt, dass wir von überall im Projekt auf den 'Resolver' (die Art von Entität, mit der wir Instanzen anfordern können) zugreifen können.
Schließlich unterstützen professionelle Rahmenbedingungen normalerweise das Phänomen der Subdependenzen.- In diesem Fall weist die Abhängigkeit selbst eine oder mehrere Abhängigkeiten von anderen Typen auf, die auch dem Container bekannt sind. In diesem Fall kann der Resolver auch diese Abhängigkeiten erfüllen, sodass Sie eine vollständige Kette korrekt erstellter Abhängigkeiten zurückerhalten, die Ihren Typzuordnungen entsprechen.
Lassen Sie uns selbst einen sehr einfachen DI-Container erstellen, um zu sehen, wie alles funktioniert. Eine solche Implementierung unterstützt keine verschachtelten Abhängigkeiten, ermöglicht es Ihnen jedoch, einer Implementierung eine Schnittstelle zuzuordnen und diese Implementierung später selbst anzufordern:
public class SimpleDIContainer {
Dictionary < Type, object > _map;
public SimpleDIContainer() {
_map = new Dictionary < Type, object > ();
}
/// <summary>
/// , .
/// </summary>
/// <typeparam name="TIn">The interface type</typeparam>
/// <typeparam name="TOut">The implementation type</typeparam>
/// <param name="args">Optional arguments for the creation of the implementation type.</param>
public void Map <TIn, TOut> (params object[] args) {
if (!_map.ContainsKey(typeof(TIn))) {
object instance = Activator.CreateInstance(typeof(TOut), args);
_map[typeof(TIn)] = instance;
}
}
/// <summary>
/// , T
/// </summary>
/// <typeparam name="T">The interface type</typeparam>
public T GetService<T> () where T: class {
if (_map.ContainsKey(typeof(T))) return _map[typeof(T)] as T;
else throw new ApplicationException("The type " + typeof(T).FullName + " is not registered in the container");
}
}
Als nächstes können wir ein kleines Programm schreiben, das einen Container erstellt, die Typen anzeigt und dann einen Dienst anfordert. Wieder ein einfaches und kompaktes Beispiel, aber stellen Sie sich vor, wie es in einer viel größeren Anwendung aussehen würde:
public class SimpleDIContainer {
Dictionary <Type, object> _map;
public SimpleDIContainer() {
_map = new Dictionary < Type, object > ();
}
/// <summary>
/// , .
/// </summary>
/// <typeparam name="TIn">The interface type</typeparam>
/// <typeparam name="TOut">The implementation type</typeparam>
/// <param name="args">Optional arguments for the creation of the implementation type.</param>
public void Map <TIn, TOut> (params object[] args) {
if (!_map.ContainsKey(typeof(TIn))) {
object instance = Activator.CreateInstance(typeof(TOut), args);
_map[typeof(TIn)] = instance;
}
}
/// <summary>
/// , T
/// </summary>
/// <typeparam name="T">The interface type</typeparam>
public T GetService <T> () where T: class {
if (_map.ContainsKey(typeof(T))) return _map[typeof(T)] as T;
else throw new ApplicationException("The type " + typeof(T).FullName + " is not registered in the container");
}
}
Ich empfehle, dieses Muster beizubehalten, wenn Sie Ihrem Projekt neue Abhängigkeiten hinzufügen. Wenn Ihr Projekt größer wird, werden Sie selbst sehen, wie einfach es ist, lose gekoppelte Komponenten zu verwalten. Es wird beträchtliche Flexibilität gewonnen, und das Projekt selbst ist letztendlich viel einfacher zu warten, zu modifizieren und an neue Bedingungen anzupassen.