.NET Framework
Abhängigkeitsspritze
Suche…
Bemerkungen
Probleme, die durch Abhängigkeitseinspritzung gelöst werden
Wenn wir keine Abhängigkeitsinjektion verwendet haben, könnte die Greeter
Klasse eher wie Greeter
aussehen:
public class ControlFreakGreeter
{
public void Greet()
{
var greetingProvider = new SqlGreetingProvider(
ConfigurationManager.ConnectionStrings["myConnectionString"].ConnectionString);
var greeting = greetingProvider.GetGreeting();
Console.WriteLine(greeting);
}
}
Es ist ein "Kontrollfreak", weil er das Erstellen der Klasse, die die Begrüßung bereitstellt, steuert, wo die SQL-Verbindungszeichenfolge herkommt und die Ausgabe steuert.
Mit der Abhängigkeitseinspritzung gibt die Greeter
Klasse diese Verantwortlichkeiten zugunsten einer einzigen Verantwortung auf und schreibt eine Begrüßung.
Das Prinzip der Abhängigkeitsinversion legt nahe, dass Klassen von Abstraktionen (wie Schnittstellen) und nicht von anderen konkreten Klassen abhängen sollten. Direkte Abhängigkeiten (Kopplung) zwischen Klassen können die Wartung zunehmend erschweren. Abhängig von Abstraktionen kann diese Kopplung reduzieren.
Abhängigkeitsinjektion hilft uns, diese Abhängigkeitsinversion zu erreichen, weil sie dazu führt, dass Klassen geschrieben werden, die von Abstraktionen abhängen. Die Greeter
Klasse "kennt" die Implementierungsdetails von IGreetingProvider
und IGreetingWriter
. Es weiß nur, dass die eingefügten Abhängigkeiten diese Schnittstellen implementieren. Das heißt, Änderungen an den konkreten Klassen, die IGreetingProvider
und IGreetingWriter
implementieren, IGreetingWriter
sich nicht auf Greeter
. Sie werden auch nicht durch ganz andere Implementierungen ersetzt. Nur Änderungen an den Schnittstellen werden vorgenommen. Greeter
ist entkoppelt.
ControlFreakGreeter
nicht richtig durchführen. Wir möchten eine kleine Code-Einheit testen, stattdessen würde unser Test das Herstellen einer Verbindung zu SQL und das Ausführen einer gespeicherten Prozedur umfassen. Dazu gehört auch das Testen der Konsolenausgabe. Da ControlFreakGreeter so viel tut, ist es nicht möglich, es isoliert von anderen Klassen zu testen.
Greeter
sich leicht als Komponententest testen, da gemockte Implementierungen seiner Abhängigkeiten Greeter
können, die einfacher auszuführen und zu überprüfen sind, als eine gespeicherte Prozedur aufzurufen oder die Ausgabe der Konsole zu lesen. Es ist keine Verbindungszeichenfolge in app.config erforderlich.
Die konkreten Implementierungen von IGreetingProvider
und IGreetingWriter
möglicherweise komplexer. Sie könnten wiederum ihre eigenen Abhängigkeiten haben, die in sie hineingelegt werden. (Zum Beispiel würden wir die SQL-Verbindungszeichenfolge in SqlGreetingProvider
.) SqlGreetingProvider
Komplexität wird jedoch von anderen Klassen "verborgen", die nur von den Schnittstellen abhängen. Das macht es einfacher, eine Klasse zu ändern, ohne einen "Ripple-Effekt", der die entsprechenden Änderungen an anderen Klassen erfordert.
Abhängigkeitsinjektion - einfaches Beispiel
Diese Klasse heißt Greeter
. Ihre Aufgabe ist es, eine Begrüßung auszugeben. Es hat zwei Abhängigkeiten . Es braucht etwas, das ihm die Begrüßung zur Ausgabe gibt, und dann braucht es eine Möglichkeit, diese Begrüßung auszugeben. Diese Abhängigkeiten werden beide als Schnittstellen, IGreetingProvider
und IGreetingWriter
. In diesem Beispiel werden diese beiden Abhängigkeiten in Greeter
"injiziert". (Weitere Erläuterungen folgen dem Beispiel.)
public class Greeter
{
private readonly IGreetingProvider _greetingProvider;
private readonly IGreetingWriter _greetingWriter;
public Greeter(IGreetingProvider greetingProvider, IGreetingWriter greetingWriter)
{
_greetingProvider = greetingProvider;
_greetingWriter = greetingWriter;
}
public void Greet()
{
var greeting = _greetingProvider.GetGreeting();
_greetingWriter.WriteGreeting(greeting);
}
}
public interface IGreetingProvider
{
string GetGreeting();
}
public interface IGreetingWriter
{
void WriteGreeting(string greeting);
}
Die Greeting
Klasse hängt sowohl von IGreetingProvider
als auch von IGreetingWriter
, ist jedoch nicht für das Erstellen von Instanzen verantwortlich. Stattdessen benötigt man sie in seinem Konstruktor. Was auch immer eine Greeting
muss diese beiden Abhängigkeiten bieten. Wir können das "Injizieren" der Abhängigkeiten nennen.
Da der Klasse Abhängigkeiten in ihrem Konstruktor zur Verfügung gestellt werden, wird dies auch als "Konstruktorinjektion" bezeichnet.
Einige gängige Konventionen:
- Der Konstruktor speichert die Abhängigkeiten als
private
Felder. Sobald die Klasse instanziiert wird, sind diese Abhängigkeiten für alle anderen nicht statischen Methoden der Klasse verfügbar. - Die
private
Felder sindreadonly
. Sobald sie im Konstruktor festgelegt sind, können sie nicht mehr geändert werden. Dies zeigt an, dass diese Felder nicht außerhalb des Konstruktors geändert werden dürfen (und nicht können). Dies stellt außerdem sicher, dass diese Abhängigkeiten für die Lebensdauer der Klasse verfügbar sind. - Die Abhängigkeiten sind Schnittstellen. Dies ist nicht unbedingt erforderlich, wird jedoch häufig verwendet, da es einfacher ist, eine Implementierung der Abhängigkeit durch eine andere zu ersetzen. Es ermöglicht auch die Bereitstellung einer abgespeckten Version der Schnittstelle zum Testen von Einheiten.
Wie Abhängigkeitseinspritzung den Komponententest einfacher macht
Dies baut auf dem vorherigen Beispiel der Greeter
Klasse auf, die zwei Abhängigkeiten hat, IGreetingProvider
und IGreetingWriter
.
Die tatsächliche Implementierung von IGreetingProvider
kann eine Zeichenfolge von einem API-Aufruf oder einer Datenbank abrufen. Bei der Implementierung von IGreetingWriter
möglicherweise die Begrüßung in der Konsole angezeigt. Da Greeter
jedoch Abhängigkeiten in seinen Konstruktor Greeter
hat, ist es einfach, einen Komponententest zu schreiben, der verspottete Versionen dieser Schnittstellen enthält. In der Praxis könnten wir ein Framework wie Moq verwenden , aber in diesem Fall schreibe ich diese verspotteten Implementierungen.
public class TestGreetingProvider : IGreetingProvider
{
public const string TestGreeting = "Hello!";
public string GetGreeting()
{
return TestGreeting;
}
}
public class TestGreetingWriter : List<string>, IGreetingWriter
{
public void WriteGreeting(string greeting)
{
Add(greeting);
}
}
[TestClass]
public class GreeterTests
{
[TestMethod]
public void Greeter_WritesGreeting()
{
var greetingProvider = new TestGreetingProvider();
var greetingWriter = new TestGreetingWriter();
var greeter = new Greeter(greetingProvider, greetingWriter);
greeter.Greet();
Assert.AreEqual(greetingWriter[0], TestGreetingProvider.TestGreeting);
}
}
Das Verhalten von IGreetingProvider
und IGreetingWriter
ist für diesen Test nicht relevant. Wir möchten testen, dass Greeter
eine Begrüßung erhält und diese schreibt. Der Entwurf von Greeter
(mithilfe von Abhängigkeitseinspritzung) ermöglicht es uns, komplizierte Abhängigkeiten ohne komplizierte bewegliche Teile einzuspritzen. Alles, was wir testen, ist, dass Greeter
mit diesen Abhängigkeiten so interagiert, wie wir es erwarten.
Warum wir Abhängigkeitsinjektionscontainer (IoC-Container) verwenden
Abhängigkeitsinjektion bedeutet, Klassen so zu schreiben, dass sie ihre Abhängigkeiten nicht steuern - stattdessen werden ihnen ihre Abhängigkeiten zur Verfügung gestellt ("injiziert".)
Dies ist nicht das Gleiche wie das Verwenden eines Abhängigkeitsinjektions-Frameworks (oft als "DI-Container", "IoC-Container" oder nur "Container" bezeichnet) wie Castle Windsor, Autofac, SimpleInjector, Ninject, Unity oder andere.
Ein Container erleichtert die Abhängigkeitsinjektion. Nehmen Sie beispielsweise an, Sie schreiben eine Reihe von Klassen, die auf Abhängigkeiten angewiesen sind. Eine Klasse hängt von mehreren Schnittstellen ab, die Klassen, die diese Schnittstellen implementieren, hängen von anderen Schnittstellen ab und so weiter. Einige hängen von bestimmten Werten ab. Und nur zum Spaß implementieren einige dieser Klassen IDisposable
und müssen entsorgt werden.
Jede einzelne Klasse ist gut geschrieben und leicht zu testen. Nun gibt es jedoch ein anderes Problem: Das Erstellen einer Instanz einer Klasse ist viel komplizierter geworden. Angenommen, wir erstellen eine Instanz einer CustomerService
Klasse. Es hat Abhängigkeiten und seine Abhängigkeiten haben Abhängigkeiten. Das Konstruieren einer Instanz könnte ungefähr so aussehen:
public CustomerData GetCustomerData(string customerNumber)
{
var customerApiEndpoint = ConfigurationManager.AppSettings["customerApi:customerApiEndpoint"];
var logFilePath = ConfigurationManager.AppSettings["logwriter:logFilePath"];
var authConnectionString = ConfigurationManager.ConnectionStrings["authorization"].ConnectionString;
using(var logWriter = new LogWriter(logFilePath ))
{
using(var customerApiClient = new CustomerApiClient(customerApiEndpoint))
{
var customerService = new CustomerService(
new SqlAuthorizationRepository(authorizationConnectionString, logWriter),
new CustomerDataRepository(customerApiClient, logWriter),
logWriter
);
// All this just to create an instance of CustomerService!
return customerService.GetCustomerData(string customerNumber);
}
}
}
Sie fragen sich vielleicht, warum nicht die gesamte Riesenkonstruktion in eine separate Funktion gestellt wird, die nur CustomerService
zurückgibt. Ein Grund ist, dass eine Klasse nicht dafür verantwortlich ist, ob die Abhängigkeiten für jede Klasse in diese Klasse IDisposable
sind, und nicht dafür verantwortlich ist, ob diese Abhängigkeiten IDisposable
oder ob sie entsorgt werden sollen. Es benutzt sie nur. Wenn also eine GetCustomerService()
Funktion vorhanden war, die einen vollständig erstellten CustomerService
, enthält diese Klasse möglicherweise eine Reihe verfügbarer Ressourcen und keine Möglichkeit, auf sie zuzugreifen oder sie zu löschen.
Und abgesehen davon, IDisposable
entsorgen, wer möchte schon immer eine Reihe verschachtelter Konstruktoren nennen? Das ist ein kurzes Beispiel. Es könnte viel schlimmer werden. Das bedeutet auch nicht, dass wir die Klassen falsch geschrieben haben. Der Unterricht kann individuell perfekt sein. Die Herausforderung besteht darin, sie zusammenzusetzen.
Ein Abhängigkeitsinjektionsbehälter vereinfacht das. Damit können wir festlegen, welche Klasse oder welcher Wert verwendet werden soll, um jede Abhängigkeit zu erfüllen. Dieses etwas zu vereinfachte Beispiel verwendet Castle Windsor:
var container = new WindsorContainer()
container.Register(
Component.For<CustomerService>(),
Component.For<ILogWriter, LogWriter>()
.DependsOn(Dependency.OnAppSettingsValue("logFilePath", "logWriter:logFilePath")),
Component.For<IAuthorizationRepository, SqlAuthorizationRepository>()
.DependsOn(Dependency.OnValue(connectionString, ConfigurationManager.ConnectionStrings["authorization"].ConnectionString)),
Component.For<ICustomerDataProvider, CustomerApiClient>()
.DependsOn(Dependency.OnAppSettingsValue("apiEndpoint", "customerApi:customerApiEndpoint"))
);
Wir nennen dies "Abhängigkeiten registrieren" oder "Container konfigurieren". Übersetzt sagt dies unserem WindsorContainer
:
- Wenn eine Klasse
ILogWriter
erfordert, erstellen Sie eine Instanz vonLogWriter
.LogWriter
erfordert einen Dateipfad. Verwenden Sie diesen Wert ausAppSettings
. - Wenn eine Klasse
IAuthorizationRepository
erfordert, erstellen Sie eine Instanz vonSqlAuthorizationRepository
. Es erfordert eine Verbindungszeichenfolge. Verwenden Sie diesen Wert aus dem AbschnittConnectionStrings
. - Wenn eine Klasse
ICustomerDataProvider
erfordert, erstellen Sie einenCustomerApiClient
und geben Sie die erforderliche Zeichenfolge ausAppSettings
.
Wenn wir eine Abhängigkeit vom Container anfordern, nennen wir das "Auflösen" einer Abhängigkeit. Es ist eine schlechte Praxis, dies direkt mit dem Container zu tun, aber das ist eine andere Geschichte. Zu Demonstrationszwecken könnten wir jetzt Folgendes tun:
var customerService = container.Resolve<CustomerService>();
var data = customerService.GetCustomerData(customerNumber);
container.Release(customerService);
Der Container weiß, dass CustomerService
von IAuthorizationRepository
und ICustomerDataProvider
. Es weiß, welche Klassen erstellt werden müssen, um diese Anforderungen zu erfüllen. Diese Klassen haben wiederum mehr Abhängigkeiten, und der Container kann diese erfüllen. Es erstellt jede Klasse, die es benötigt, bis es eine Instanz von CustomerService
.
Wenn es zu einem Punkt kommt, an dem eine Klasse eine Abhängigkeit erfordert, die wir nicht registriert haben, wie IDoesSomethingElse
, wird beim Versuch, CustomerService
zu lösen, eine eindeutige Ausnahme IDoesSomethingElse
, die uns darüber IDoesSomethingElse
, dass wir nichts registriert haben, um diese Anforderung zu erfüllen.
Jedes DI-Framework verhält sich ein wenig anders, aber in der Regel können Sie steuern, wie bestimmte Klassen instanziiert werden. LogWriter
Sie beispielsweise, dass eine Instanz von LogWriter
und jeder Klasse ILogWriter
, die von ILogWriter
abhängt, oder möchten Sie, dass jedes Mal eine neue erstellt wird? Die meisten Container haben eine Möglichkeit, dies anzugeben.
Was ist mit Klassen, die IDisposable
implementieren? Deshalb nennen wir container.Release(customerService);
Am Ende. Die meisten Behälter (einschließlich Windsor) wird durch alle Abhängigkeiten Schritt zurück erstellt und Dispose
diejenigen , die Beseitigung benötigen. Wenn CustomerService
IDisposable
, wird auch dies entsorgt.
Das Registrieren von Abhängigkeiten, wie oben gezeigt, kann wie mehr zu schreibender Code aussehen. Aber wenn wir viele Klassen mit vielen Abhängigkeiten haben, lohnt es sich wirklich. Und wenn wir dieselben Klassen ohne Abhängigkeitseinspritzung schreiben müssten, wäre es schwierig, dieselbe Anwendung mit vielen Klassen zu warten und zu testen.
Dies verkratzt die Oberfläche, warum wir Abhängigkeitsinjektionsbehälter verwenden. Wie wir unsere Anwendung für die Verwendung einer Anwendung konfigurieren (und sie korrekt verwenden), ist nicht nur ein Thema, sondern eine Reihe von Themen, da die Anweisungen und Beispiele von Container zu Container unterschiedlich sind.