Sök…


Anmärkningar

Problem som löses genom beroendeinsprutning

Om vi inte använde beroendeinjektion kan Greeter klassen se mer ut så här:

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

Det är en "kontrollfreak" eftersom den kontrollerar att skapa klassen som ger hälsningen, den styr var SQL-anslutningssträngen kommer från, och den styr utdata.

Med användning av beroendeinjektion avstår Greeter klassen det ansvaret till förmån för ett enda ansvar och skriver en hälsning som den tillhandahåller.

Dependency Inversion Principen föreslår att klasser bör bero på abstraktioner (som gränssnitt) snarare än på andra konkreta klasser. Direkt beroende (koppling) mellan klasser kan göra underhållet svårt. Beroende på abstraktioner kan minska den kopplingen.

Beroende injektion hjälper oss att uppnå den beroende inversion eftersom det leder till att skriva klasser som är beroende av abstraktioner. Greeter klassen "vet" ingenting alls om implementeringsdetaljerna för IGreetingProvider och IGreetingWriter . Den vet bara att de injicerade beroenden implementerar dessa gränssnitt. Det betyder att förändringar i de konkreta klasserna som implementerar IGreetingProvider och IGreetingWriter kommer att påverka Greeter . Inte heller kommer att ersätta dem med helt olika implementationer. Endast ändringar i gränssnitten kommer. Greeter är frånkopplad.

ControlFreakGreeter är omöjligt att korrekt testa enhet. Vi vill testa en liten kodenhet, men istället skulle vårt test inkludera anslutning till SQL och utföra en lagrad procedur. Det skulle också inkludera att testa konsolutgången. Eftersom ControlFreakGreeter gör så mycket är det omöjligt att testa isolerat från andra klasser.

Greeter är lätt att enhetstest eftersom vi kan injicera håliga implementationer av dess beroenden som är lättare att utföra och verifiera än att ringa en lagrad procedur eller läsa utgången från konsolen. Det kräver ingen anslutningssträng i app.config.

De konkreta implementeringarna av IGreetingProvider och IGreetingWriter kan bli mer komplexa. De kan i sin tur ha sina egna beroenden som sprutas in i dem. (Till exempel injicerar vi SQL-anslutningssträngen i SqlGreetingProvider .) Men den komplexiteten är "dold" från andra klasser som bara beror på gränssnitten. Det gör det lättare att ändra en klass utan en "rippeleffekt" som kräver att vi gör motsvarande ändringar av andra klasser.

Beroende på injektion - Enkelt exempel

Denna klass kallas Greeter . Dess ansvar är att ge en hälsning. Det har två beroenden . Den behöver något som ger den hälsningen att matas ut, och sedan behöver den ett sätt att mata ut den hälsningen. Dessa beroenden beskrivs båda som gränssnitt, IGreetingProvider och IGreetingWriter . I det här exemplet "injiceras" dessa två beroenden i Greeter . (Ytterligare förklaring enligt exemplet.)

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

Greeting beror på både IGreetingProvider och IGreetingWriter , men det är inte ansvarigt för att skapa förekomster av endera. Istället kräver det dem i sin konstruktör. Oavsett vad som skapar en instans av Greeting måste ge dessa två beroenden. Vi kan kalla det att "injicera" beroenden.

Eftersom beroenden tillhandahålls klassen i dess konstruktör, kallas detta också "konstruktörsinjektion."

Några vanliga konventioner:

  • Konstruktören sparar beroenden som private fält. Så snart klassen är instanserad är dessa beroenden tillgängliga för alla andra icke-statiska metoder i klassen.
  • De private fälten är readonly . När de är inställda i konstruktören kan de inte ändras. Detta indikerar att dessa fält inte bör (och inte kan) ändras utanför konstruktören. Det säkerställer vidare att dessa beroenden är tillgängliga under klassens livstid.
  • Beroenden är gränssnitt. Detta är inte strikt nödvändigt, men är vanligt eftersom det gör det lättare att ersätta en implementering av beroendet med en annan. Det tillåter också att tillhandahålla en hånad version av gränssnittet för enhetsteständamål.

Hur beroende injektion gör enhetstest enklare

Detta bygger på det föregående exemplet på Greeter klassen som har två beroenden, IGreetingProvider och IGreetingWriter .

Den faktiska implementeringen av IGreetingProvider kan hämta en sträng från ett API-samtal eller en databas. Implementeringen av IGreetingWriter kan visa hälsningen i konsolen. Men eftersom Greeter har sina beroenden injicerat i sin konstruktör, är det lätt att skriva ett enhetstest som injicerar håliga versioner av dessa gränssnitt. I det verkliga livet kanske vi använder ett ramverk som Moq , men i det här fallet kommer jag att skriva de förlöjliga implementeringarna.

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

IGreetingProvider och IGreetingWriter är inte relevanta för detta test. Vi vill testa att Greeter får en hälsning och skriver den. Utformningen av Greeter (med beroendeinjektion) gör att vi kan injicera håliga beroenden utan komplicerade rörliga delar. Allt vi testar är att Greeter interagerar med de beroenden som vi förväntar oss att göra.

Varför vi använder behållare för beroende av injektion (IoC-behållare)

Beroende injektion betyder att skriva klasser så att de inte kontrollerar deras beroenden - istället tillhandahålls deras beroenden till dem ("injiceras.")

Det här är inte samma sak som att använda ett ramar för injektion av beroende (ofta kallat en "DI-behållare", "IoC-behållare" eller bara "behållare") som Castle Windsor, Autofac, SimpleInjector, Ninject, Unity eller andra.

En behållare underlättar bara injektionsinjektion. Anta till exempel att du skriver ett antal klasser som förlitar sig på beroendeinjektion. En klass beror på flera gränssnitt, klasserna som implementerar dessa gränssnitt beror på andra gränssnitt och så vidare. Vissa beror på specifika värden. Och bara för skojs skull, implementerar några av dessa klasser IDisposable och måste kasseras.

Varje enskild klass är välskrivet och lätt att testa. Men nu finns det ett annat problem: Att skapa en instans av en klass har blivit mycket mer komplicerad. Anta att vi skapar en instans av en CustomerService klass. Det har beroenden och dess beroenden har beroenden. Att bygga en instans kan se ut så här:

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

Du kanske undrar, varför inte sätta hela jättekonstruktionen i en separat funktion som bara returnerar CustomerService ? En orsak är att eftersom beroenden för varje klass injiceras i den, är en klass inte ansvarig för att veta om dessa beroenden är IDisposable eller bortskaffa dem. Den använder dem bara. Så om a vi hade en GetCustomerService() -funktion som returnerade en helt konstruerad CustomerService , kan den klassen innehålla ett antal engångsresurser och inget sätt att komma åt eller bortskaffa dem.

Och bortsett från att bortskaffa IDisposable , vem vill ringa en serie kapslade konstruktörer som det någonsin? Det är ett kort exempel. Det kan bli mycket, mycket värre. Återigen betyder det inte att vi skrev klasserna på fel sätt. Lektionerna kan vara individuellt perfekta. Utmaningen är att komponera dem tillsammans.

En behållare för injektionsinjektion förenklar det. Det tillåter oss att specificera vilken klass eller värde som ska användas för att uppfylla varje beroende. Detta något förenklade exempel använder 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"))   
);

Vi kallar detta "att registrera beroenden" eller "konfigurera behållaren." Översatt, detta berättar vår WindsorContainer :

  • Om en klass kräver ILogWriter , skapa en instans av LogWriter . LogWriter kräver en LogWriter . Använd detta värde från AppSettings .
  • Om en klass kräver IAuthorizationRepository , skapa en instans av SqlAuthorizationRepository . Det kräver en anslutningssträng. Använd detta värde från avsnittet ConnectionStrings .
  • Om en klass kräver ICustomerDataProvider , skapar du en CustomerApiClient och tillhandahåller den sträng som den behöver från AppSettings .

När vi begär ett beroende från behållaren kallar vi att "lösa" ett beroende. Det är dålig praxis att göra det direkt med behållaren, men det är en annan historia. För demonstrationsändamål kan vi nu göra detta:

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

Containern vet att CustomerService beror på IAuthorizationRepository och ICustomerDataProvider . Den vet vilka klasser den behöver skapa för att uppfylla dessa krav. Dessa klasser har i sin tur fler beroenden, och behållaren vet hur man kan uppfylla dessa. Det skapar varje klass som den behöver tills den kan returnera en instans av CustomerService .

Om det kommer till en punkt där en klass kräver ett beroende som vi inte har registrerat, som IDoesSomethingElse , så när vi försöker lösa CustomerService kommer det att kasta ett tydligt undantag som säger att vi inte har registrerat något för att uppfylla detta krav.

Varje DI-ramverk uppför sig lite annorlunda, men vanligtvis ger de oss viss kontroll över hur vissa klasser instanseras. Till exempel vill vi att den ska skapa en instans av LogWriter och tillhandahålla den till varje klass som beror på ILogWriter , eller vill vi att den ska skapa en ny varje gång? De flesta containrar har ett sätt att specificera det.

Vad IDisposable om klasser som implementerar IDisposable ? Det är därför vi kallar container.Release(customerService); i slutet. De flesta containrar (inklusive Windsor) kommer att gå tillbaka genom alla beroenden som skapats och Dispose de som behöver kasseras. Om CustomerService är IDisposable kommer den att bortskaffa det också.

Att registrera beroenden som ses ovan kanske bara ser ut som mer kod att skriva. Men när vi har massor av klasser med massor av beroenden, så lönar det sig verkligen. Och om vi var tvungna att skriva samma klasser utan att använda beroendeinjektion, skulle samma applikation med massor av klasser bli svårt att underhålla och testa.

Detta repar ytan till varför vi använder behållare för beroendeinsprutning. Hur vi konfigurerar vår applikation för att använda ett (och använda det på rätt sätt) är inte bara ett ämne - det är ett antal ämnen, eftersom instruktionerna och exemplen varierar från en behållare till nästa.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow