Ricerca…


Osservazioni

Problemi risolti dall'iniezione delle dipendenze

Se non si utilizza l'iniezione di dipendenza, la classe Greeter potrebbe essere più simile a questa:

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

È un "maniaco del controllo" perché controlla la creazione della classe che fornisce il saluto, controlla da dove proviene la stringa di connessione SQL e controlla l'output.

Usando l'iniezione di dipendenza, la classe Greeter rinuncia a quelle responsabilità a favore di una singola responsabilità, scrivendo un saluto fornito ad essa.

Il principio di inversione di dipendenza suggerisce che le classi dovrebbero dipendere dalle astrazioni (come le interfacce) piuttosto che da altre classi concrete. Le dipendenze dirette (accoppiamento) tra classi possono rendere la manutenzione progressivamente difficile. A seconda delle astrazioni è possibile ridurre tale accoppiamento.

L'iniezione di dipendenza ci aiuta a raggiungere tale inversione di dipendenza perché conduce a scrivere classi che dipendono dalle astrazioni. La classe Greeter "non sa" nulla dei dettagli di implementazione di IGreetingProvider e IGreetingWriter . Sa solo che le dipendenze iniettate implementano quelle interfacce. Ciò significa che le modifiche alle classi concrete che implementano IGreetingProvider e IGreetingWriter non influiranno su Greeter . Né li sostituiranno con implementazioni completamente diverse. Verranno modificate solo le interfacce. Greeter è disaccoppiato.

ControlFreakGreeter è impossibile eseguire correttamente il test dell'unità. Vogliamo testare una piccola unità di codice, ma il nostro test includerebbe la connessione a SQL e l'esecuzione di una stored procedure. Includerebbe anche il test dell'output della console. Poiché ControlFreakGreeter fa così tanto è impossibile testare separatamente dalle altre classi.

Greeter è facile da testare in quanto è possibile iniettare implementazioni derise delle sue dipendenze che sono più facili da eseguire e verificare rispetto alla chiamata di una stored procedure o alla lettura dell'output della console. Non richiede una stringa di connessione in app.config.

Le implementazioni concrete di IGreetingProvider e IGreetingWriter potrebbero diventare più complesse. A loro volta, potrebbero avere le loro dipendenze che vengono iniettate in loro. (Ad esempio, inseriremmo la stringa di connessione SQL in SqlGreetingProvider .) Ma quella complessità è "nascosta" da altre classi che dipendono solo dalle interfacce. Ciò rende più semplice modificare una classe senza un "effetto a catena" che richiede di apportare modifiche corrispondenti ad altre classi.

Iniezione delle dipendenze - Semplice esempio

Questa classe si chiama Greeter . La sua responsabilità è di produrre un saluto. Ha due dipendenze . Ha bisogno di qualcosa che gli dia il benvenuto per l'output, e quindi ha bisogno di un modo per produrre quel saluto. Queste dipendenze sono entrambe descritte come interfacce, IGreetingProvider e IGreetingWriter . In questo esempio, queste due dipendenze vengono "iniettate" in Greeter . (Ulteriori spiegazioni sull'esempio.)

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

La classe di Greeting dipende da IGreetingProvider e IGreetingWriter , ma non è responsabile della creazione di istanze di entrambi. Invece li richiede nel suo costruttore. Qualsiasi cosa crei un'istanza di Greeting deve fornire queste due dipendenze. Possiamo chiamarlo "iniettare" le dipendenze.

Poiché le dipendenze sono fornite alla classe nel suo costruttore, questa viene anche chiamata "iniezione costruttore".

Alcune convenzioni comuni:

  • Il costruttore salva le dipendenze come campi private . Non appena la classe viene istanziata, tali dipendenze sono disponibili per tutti gli altri metodi non statici della classe.
  • I campi private sono di readonly . Una volta impostati nel costruttore, non possono essere modificati. Questo indica che quei campi non dovrebbero (e non possono) essere modificati al di fuori del costruttore. Ciò garantisce inoltre che tali dipendenze siano disponibili per tutta la vita della classe.
  • Le dipendenze sono interfacce. Questo non è strettamente necessario, ma è comune perché rende più facile sostituire un'implementazione della dipendenza con un'altra. Consente inoltre di fornire una versione mocked dell'interfaccia per scopi di test unitari.

In che modo l'iniezione delle dipendenze rende più semplice il test unitario

Questo si basa sul precedente esempio della classe Greeter che ha due dipendenze, IGreetingProvider e IGreetingWriter .

L'effettiva implementazione di IGreetingProvider potrebbe recuperare una stringa da una chiamata API o da un database. L'implementazione di IGreetingWriter potrebbe visualizzare il saluto nella console. Ma poiché Greeter ha le sue dipendenze iniettate nel suo costruttore, è facile scrivere un test unitario che inietta versioni derise di quelle interfacce. Nella vita reale potremmo usare un framework come Moq , ma in questo caso scriverò quelle implementazioni derise.

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

Il comportamento di IGreetingProvider e IGreetingWriter non sono rilevanti per questo test. Vogliamo testare che Greeter riceve un saluto e lo scrive. Il design di Greeter (usando l'iniezione di dipendenza) ci permette di iniettare dipendenze derise senza parti mobili complicate. Tutto quello che stiamo testando è che Greeter interagisce con quelle dipendenze come ci aspettiamo.

Perché utilizziamo i contenitori di iniezione delle dipendenze (contenitori IoC)

Iniezione di dipendenza significa scrivere classi in modo che non controllino le loro dipendenze - invece, le loro dipendenze sono fornite a loro ("iniettato").

Questa non è la stessa cosa dell'utilizzo di un'infrastruttura per le dipendenze (spesso chiamata "contenitore DI", "contenitore IoC" o semplicemente "contenitore") come Castle Windsor, Autofac, SimpleInjector, Ninject, Unity o altri.

Un contenitore semplifica l'iniezione della dipendenza. Ad esempio, supponiamo di scrivere un numero di classi che si basano sull'integrazione delle dipendenze. Una classe dipende da diverse interfacce, le classi che implementano tali interfacce dipendono da altre interfacce e così via. Alcuni dipendono da valori specifici. E solo per divertimento, alcune di queste classi implementano IDisposable e devono essere smaltite.

Ogni singola classe è ben scritta e facile da testare. Ma ora c'è un altro problema: la creazione di un'istanza di una classe è diventata molto più complicata. Supponiamo di creare un'istanza di una classe CustomerService . Ha dipendenze e le sue dipendenze hanno dipendenze. La costruzione di un'istanza potrebbe essere simile a questa:

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

Potresti chiedertelo, perché non inserire l'intera costruzione gigante in una funzione separata che restituisce solo CustomerService ? Una ragione è che poiché le dipendenze di ogni classe vengono iniettate in essa, una classe non è responsabile per sapere se quelle dipendenze sono IDisposable o che le eliminano. Li usa solo. Quindi, se avessimo una funzione GetCustomerService() che restituiva un CustomerService completamente costruito, quella classe potrebbe contenere un numero di risorse disponibili e nessun modo per accedervi o disporne.

E a parte il disporre di IDisposable , chi vuole chiamare una serie di costruttori annidati in questo modo, mai? Questo è un breve esempio. Potrebbe ottenere molto, molto peggio. Di nuovo, ciò non significa che abbiamo scritto le classi nel modo sbagliato. Le classi potrebbero essere individualmente perfette. La sfida è comporle insieme.

Un contenitore per l'iniezione di dipendenza lo semplifica. Ci permette di specificare quale classe o valore dovrebbe essere usato per soddisfare ogni dipendenza. Questo esempio un po 'semplicistico utilizza 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"))   
);

Chiamiamo questo "registrazione delle dipendenze" o "configurazione del contenitore". Tradotto, questo dice al nostro WindsorContainer :

  • Se una classe richiede ILogWriter , creare un'istanza di LogWriter . LogWriter richiede un percorso file. Utilizzare questo valore da AppSettings .
  • Se una classe richiede IAuthorizationRepository , creare un'istanza di SqlAuthorizationRepository . Richiede una stringa di connessione. Utilizzare questo valore dalla sezione ConnectionStrings .
  • Se una classe richiede ICustomerDataProvider , creare un CustomerApiClient e fornire la stringa necessaria da AppSettings .

Quando chiediamo una dipendenza dal contenitore, chiamiamo "risolvere" una dipendenza. È una cattiva pratica farlo direttamente utilizzando il contenitore, ma questa è una storia diversa. A scopo dimostrativo, ora potremmo fare questo:

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

Il contenitore sa che CustomerService dipende da IAuthorizationRepository e ICustomerDataProvider . Sa quali classi deve creare per soddisfare tali requisiti. Queste classi, a loro volta, hanno più dipendenze e il contenitore sa come soddisfarle. Creerà ogni classe di cui ha bisogno fino a quando non potrà restituire un'istanza di CustomerService .

Se arriva a un punto in cui una classe richiede una dipendenza che non abbiamo registrato, come IDoesSomethingElse , quando proviamo a risolvere CustomerService genererà un'eccezione chiara che ci dice che non abbiamo registrato nulla per soddisfare tale requisito.

Ogni framework DI si comporta in modo leggermente diverso, ma in genere ci dà il controllo sul modo in cui determinate classi vengono istanziate. Ad esempio, vogliamo creare un'istanza di LogWriter e fornirla ad ogni classe che dipende da ILogWriter , oppure vogliamo che ne crei una nuova ogni volta? La maggior parte dei contenitori ha un modo per specificarlo.

Che dire delle classi che implementano IDisposable ? Ecco perché chiamiamo container.Release(customerService); alla fine. La maggior parte dei contenitori (incluso Windsor) eseguirà un passo indietro attraverso tutte le dipendenze create e Dispose quelle che devono essere eliminate. Se CustomerService è IDisposable lo IDisposable anche.

La registrazione delle dipendenze come visto sopra potrebbe sembrare un po 'più codice da scrivere. Ma quando abbiamo un sacco di classi con un sacco di dipendenze, allora paga davvero. E se dovessimo scrivere quelle stesse classi senza usare l'injection dependency, quella stessa applicazione con molte classi diventerebbe difficile da mantenere e testare.

Questo graffia la superficie del motivo per cui utilizziamo i contenitori di iniezione di dipendenza. Il modo in cui configuriamo la nostra applicazione per usarne uno (e usarlo correttamente) non è solo un argomento: è un numero di argomenti, in quanto le istruzioni e gli esempi variano da un contenitore all'altro.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow