Zoeken…


Opmerkingen

Problemen opgelost door afhankelijkheid Injectie

Als we geen afhankelijkheidsinjectie hadden gebruikt, zou de klasse Greeter er meer als volgt uit kunnen zien:

public class ControlFreakGreeter
{
    public void Greet()
    {
        var greetingProvider = new SqlGreetingProvider(
            ConfigurationManager.ConnectionStrings["myConnectionString"].ConnectionString);
        var greeting = greetingProvider.GetGreeting();
        Console.WriteLine(greeting);
    }
}

Het is een "control freak" omdat het de klasse bepaalt die de begroeting verzorgt, het bepaalt waar de SQL-verbindingsreeks vandaan komt en het de uitvoer regelt.

Met behulp van afhankelijkheidsinjectie geeft de klasse Greeter afstand van die verantwoordelijkheden ten gunste van een enkele verantwoordelijkheid, door een begroeting te schrijven.

Het Dependency Inversion Principle suggereert dat klassen afhankelijk moeten zijn van abstracties (zoals interfaces) en niet van andere concrete klassen. Directe afhankelijkheden (koppeling) tussen klassen kunnen onderhoud steeds moeilijker maken. Afhankelijk van abstracties kan die koppeling verminderen.

Afhankelijkheidsinjectie helpt ons om die afhankelijkheidsinversie te bereiken omdat het leidt tot schrijfklassen die afhankelijk zijn van abstracties. De klasse Greeter "weet" helemaal niets van de implementatiedetails van IGreetingProvider en IGreetingWriter . Het weet alleen dat de geïnjecteerde afhankelijkheden die interfaces implementeren. Dat betekent dat wijzigingen in de concrete klassen die IGreetingProvider en IGreetingWriter implementeren, Greeter niet beïnvloeden. Ze zullen ook niet door geheel verschillende implementaties worden vervangen. Alleen wijzigingen in de interfaces zullen. Greeter is ontkoppeld.

ControlFreakGreeter kan de unit niet goed testen. We willen een kleine eenheid code testen, maar in plaats daarvan zou onze test een verbinding met SQL omvatten en een opgeslagen procedure uitvoeren. Het zou ook het testen van de console-uitvoer omvatten. Omdat ControlFreakGreeter zoveel doet, is het onmogelijk om los van andere klassen te testen.

Greeter is eenvoudig te testen omdat we bespotte implementaties van zijn afhankelijkheden kunnen injecteren die gemakkelijker zijn uit te voeren en te verifiëren dan een opgeslagen procedure aan te roepen of de uitvoer van de console te lezen. Het vereist geen verbindingsreeks in app.config.

De concrete implementaties van IGreetingProvider en IGreetingWriter kunnen complexer worden. Zij kunnen op hun beurt hun eigen afhankelijkheden hebben die in hen worden geïnjecteerd. (We zouden bijvoorbeeld de SQL-verbindingsreeks in SqlGreetingProvider .) Maar die complexiteit is "verborgen" voor andere klassen die alleen afhankelijk zijn van de interfaces. Dat maakt het eenvoudiger om één klasse te wijzigen zonder een "rimpeleffect" waarvoor we overeenkomstige wijzigingen in andere klassen moeten aanbrengen.

Afhankelijkheidsinjectie - eenvoudig voorbeeld

Deze klasse wordt Greeter . Het is zijn verantwoordelijkheid om een begroeting uit te voeren. Het heeft twee afhankelijkheden . Het heeft iets nodig dat het de begroeting geeft om uit te voeren, en dan heeft het een manier nodig om die begroeting uit te voeren. Deze afhankelijkheden worden beide beschreven als interfaces, IGreetingProvider en IGreetingWriter . In dit voorbeeld worden die twee afhankelijkheden "ingespoten" in Greeter . (Verdere uitleg volgens het voorbeeld.)

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);
}

De klasse Greeting afhankelijk van zowel IGreetingProvider als IGreetingWriter , maar is niet verantwoordelijk voor het maken van instanties van beide. In plaats daarvan vereist het ze in zijn constructor. Wat een instantie van Greeting moet die twee afhankelijkheden bieden. We kunnen dat de "afhankelijkheden" noemen.

Omdat afhankelijkheden worden verstrekt aan de klasse in de constructor, wordt dit ook wel "constructorinjectie" genoemd.

Enkele veel voorkomende conventies:

  • De constructeur slaat de afhankelijkheden als private velden. Zodra de klasse wordt geïnstantieerd, zijn die afhankelijkheden beschikbaar voor alle andere niet-statische methoden van de klasse.
  • De private velden zijn readonly . Als ze eenmaal in de constructor zijn ingesteld, kunnen ze niet meer worden gewijzigd. Dit geeft aan dat die velden niet (en niet) buiten de constructor mogen worden gewijzigd. Dat zorgt er verder voor dat die afhankelijkheden beschikbaar blijven voor de levensduur van de klas.
  • De afhankelijkheden zijn interfaces. Dit is niet strikt noodzakelijk, maar is gebruikelijk omdat het gemakkelijker is om de ene implementatie van de afhankelijkheid te vervangen door een andere. Het biedt ook de mogelijkheid om een bespotte versie van de interface te leveren voor testdoeleinden.

Hoe afhankelijkheidsinjectie het testen van eenheden eenvoudiger maakt

Dit bouwt voort op het vorige voorbeeld van de klasse Greeter , die twee afhankelijkheden heeft, IGreetingProvider en IGreetingWriter .

De daadwerkelijke implementatie van IGreetingProvider kan een tekenreeks ophalen uit een API-aanroep of een database. De implementatie van IGreetingWriter kan de begroeting in de console weergeven. Maar omdat Greeter afhankelijkheden in zijn constructor heeft geïnjecteerd, is het eenvoudig om een eenheidstest te schrijven die gespotte versies van die interfaces injecteert. In het echte leven gebruiken we misschien een framework zoals Moq , maar in dit geval zal ik die bespotte implementaties schrijven.

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);
    }
}

Het gedrag van IGreetingProvider en IGreetingWriter is niet relevant voor deze test. We willen testen dat Greeter een groet krijgt en deze schrijft. Het ontwerp van Greeter (met behulp van afhankelijkheidsinjectie) stelt ons in staat om bespotte afhankelijkheden te injecteren zonder ingewikkelde bewegende delen. Het enige dat we testen is dat Greeter met die afhankelijkheden werkt zoals we verwachten.

Waarom we afhankelijkheidsinjectiecontainers (IoC-containers) gebruiken

Afhankelijkheidsinjectie betekent klassen schrijven zodat ze hun afhankelijkheden niet beheersen - in plaats daarvan worden hun afhankelijkheden aan hen verstrekt ('geïnjecteerd').

Dit is niet hetzelfde als het gebruik van een framework voor afhankelijkheid van injectie (vaak een "DI-container", "IoC-container" of gewoon "container" genoemd) zoals Castle Windsor, Autofac, SimpleInjector, Ninject, Unity of andere.

Een container maakt afhankelijkheidsinjectie eenvoudig. Stel bijvoorbeeld dat u een aantal klassen schrijft die afhankelijk zijn van afhankelijkheidsinjectie. Eén klasse is afhankelijk van meerdere interfaces, de klassen die deze interfaces implementeren, zijn afhankelijk van andere interfaces, enzovoort. Sommige zijn afhankelijk van specifieke waarden. En gewoon voor de lol, sommige van die klassen implementeren IDisposable en moeten worden weggegooid.

Elke individuele les is goed geschreven en gemakkelijk te testen. Maar nu is er een ander probleem: het maken van een instantie van een klasse is veel gecompliceerder geworden. Stel dat we een instantie van een CustomerService klasse maken. Het heeft afhankelijkheden en zijn afhankelijkheden hebben afhankelijkheden. Het construeren van een instantie kan er ongeveer zo uitzien:

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);
        }
    }
}

Je vraagt je misschien af, waarom zet je de hele gigantische constructie niet in een aparte functie die alleen CustomerService retourneert? Een reden is dat omdat de afhankelijkheden voor elke klasse erin worden geïnjecteerd, een klasse niet verantwoordelijk is om te weten of die afhankelijkheden IDisposable of ze weg te doen. Het gebruikt ze gewoon. Dus als we een GetCustomerService() -functie hadden die een volledig geconstrueerde CustomerService retourneerde, zou die klasse een aantal wegwerpresources kunnen bevatten en geen manier om ze te openen of te verwijderen.

En afgezien van het weggooien van IDisposable , wie wil ooit een reeks geneste constructeurs noemen? Dat is een kort voorbeeld. Het kan veel, veel erger worden. Nogmaals, dat betekent niet dat we de lessen op de verkeerde manier hebben geschreven. De lessen kunnen individueel perfect zijn. De uitdaging is om ze samen te stellen.

Een afhankelijkheid injectie container vereenvoudigt dat. Hiermee kunnen we specificeren welke klasse of waarde moet worden gebruikt om aan elke afhankelijkheid te voldoen. Dit ietwat vereenvoudigde voorbeeld gebruikt 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"))   
);

We noemen dit "het registreren van afhankelijkheden" of "het configureren van de container." Vertaald, vertelt dit onze WindsorContainer :

  • Als een klasse ILogWriter vereist, maakt u een exemplaar van LogWriter . LogWriter vereist een bestandspad. Gebruik deze waarde van AppSettings .
  • Als een klasse IAuthorizationRepository vereist, maakt u een instantie van SqlAuthorizationRepository . Het vereist een verbindingsreeks. Gebruik deze waarde in het gedeelte ConnectionStrings .
  • Als een klasse ICustomerDataProvider vereist, maakt u een CustomerApiClient en geeft u de tekenreeks die nodig is vanuit AppSettings .

Wanneer we een afhankelijkheid van de container aanvragen, noemen we dat een afhankelijkheid "oplossen". Het is een slechte gewoonte om dat rechtstreeks met de container te doen, maar dat is een ander verhaal. Voor demonstratiedoeleinden zouden we dit nu kunnen doen:

var customerService = container.Resolve<CustomerService>();
var data = customerService.GetCustomerData(customerNumber);
container.Release(customerService);

De container weet dat de CustomerService afhankelijk is van IAuthorizationRepository en ICustomerDataProvider . Het weet welke klassen het moet maken om aan die vereisten te voldoen. Die klassen hebben op hun beurt meer afhankelijkheden en de container weet hoe hij die moet vervullen. Het maakt elke klasse die het nodig heeft totdat het een exemplaar van CustomerService kan retourneren.

Als het op een punt komt dat een klasse een afhankelijkheid vereist die we niet hebben geregistreerd, zoals IDoesSomethingElse , dan zal wanneer we proberen CustomerService te lossen, een duidelijke uitzondering worden weergegeven die IDoesSomethingElse dat we niets hebben geregistreerd om aan die vereiste te voldoen.

Elk DI-framework gedraagt zich een beetje anders, maar meestal geven ze ons enige controle over hoe bepaalde klassen worden geïnstantieerd. LogWriter we bijvoorbeeld dat het één exemplaar van LogWriter en het aan elke klasse levert die van ILogWriter afhankelijk is, of willen we dat het elke keer een nieuw exemplaar maakt? De meeste containers hebben een manier om dat aan te geven.

Hoe zit het met klassen die IDisposable ? Daarom noemen we container.Release(customerService); aan het einde. De meeste containers (inclusief Windsor) zullen een stap terug doen door alle gecreëerde afhankelijkheden en de containers Dispose die moeten worden verwijderd. Als CustomerService IDisposable is, zal dit ook worden verwijderd.

Het registreren van afhankelijkheden zoals hierboven weergegeven, lijkt misschien meer code om te schrijven. Maar als we veel klassen hebben met veel afhankelijkheden, dan loont het echt. En als we dezelfde klassen moesten schrijven zonder afhankelijkheidsinjectie te gebruiken, zou diezelfde applicatie met veel klassen moeilijk te onderhouden en te testen worden.

Dit krast het oppervlak van waarom we afhankelijkheid injectiecontainers gebruiken. Hoe we onze applicatie configureren om er één te gebruiken (en correct te gebruiken) is niet slechts één onderwerp - het is een aantal onderwerpen, omdat de instructies en voorbeelden van container tot container verschillen.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow