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 sind readonly . 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 von LogWriter . LogWriter erfordert einen Dateipfad. Verwenden Sie diesen Wert aus AppSettings .
  • Wenn eine Klasse IAuthorizationRepository erfordert, erstellen Sie eine Instanz von SqlAuthorizationRepository . Es erfordert eine Verbindungszeichenfolge. Verwenden Sie diesen Wert aus dem Abschnitt ConnectionStrings .
  • Wenn eine Klasse ICustomerDataProvider erfordert, erstellen Sie einen CustomerApiClient und geben Sie die erforderliche Zeichenfolge aus AppSettings .

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.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow