Che cos'è Dependency Injection (DI)?
Come altri hanno già detto, Dependency Injection (DI) rimuove la responsabilità della creazione diretta e della gestione della durata della vita, di altri casi di oggetto da cui dipende la nostra classe di interesse (classe di consumo) (nel senso UML ). Queste istanze vengono invece passate alla nostra classe di consumatori, in genere come parametri del costruttore o tramite setter di proprietà (la gestione dell'oggetto di dipendenza che istanza e passa alla classe di consumatore viene generalmente eseguita da un contenitore Inversion of Control (IoC) , ma questo è un altro argomento) .
DI, DIP e SOLID
Nello specifico, nel paradigma dei principi solidi di Robert C Martin di Object Oriented Design , DI
è una delle possibili implementazioni del principio di inversione di dipendenza (DIP) . Il DIP è D
del SOLID
mantra - altre implementazioni DIP includono il Locator Service e modelli Plugin.
L'obiettivo del DIP è disaccoppiare dipendenze strette e concrete tra le classi e, invece, allentare l'accoppiamento mediante un'astrazione, che può essere ottenuta tramite un interface
, abstract class
o pure virtual class
, a seconda della lingua e dell'approccio utilizzati.
Senza il DIP, il nostro codice (che ho chiamato questa "classe di consumo") è direttamente associato a una dipendenza concreta ed è spesso gravato dalla responsabilità di sapere come ottenere e gestire un'istanza di questa dipendenza, cioè concettualmente:
"I need to create/use a Foo and invoke method `GetBar()`"
Considerando che dopo l'applicazione del DIP, il requisito è allentato e la preoccupazione di ottenere e gestire la durata della Foo
dipendenza è stata rimossa:
"I need to invoke something which offers `GetBar()`"
Perché usare DIP (e DI)?
Il disaccoppiamento delle dipendenze tra le classi in questo modo consente una facile sostituzione di queste classi di dipendenza con altre implementazioni che soddisfano anche i prerequisiti dell'astrazione (ad es. La dipendenza può essere commutata con un'altra implementazione della stessa interfaccia). Inoltre, come altri hanno già detto, forse il motivo più comune per disaccoppiare le classi tramite il DIP è quello di consentire a una classe consumatrice di essere testata isolatamente, poiché queste stesse dipendenze possono ora essere mozzate e / o derise.
Una conseguenza di DI è che la gestione della durata delle istanze di oggetti dipendenza non è più controllata da una classe consumante, poiché l'oggetto dipendenza viene ora passato alla classe consumante (tramite l'iniezione del costruttore o del setter).
Questo può essere visualizzato in diversi modi:
- Se il controllo della durata della vita delle dipendenze da parte della classe consumatrice deve essere mantenuto, il controllo può essere ristabilito iniettando una factory (astratta) per creare le istanze della classe di dipendenza, nella classe consumer. Il consumatore sarà in grado di ottenere istanze tramite una
Create
in fabbrica in base alle esigenze e smaltire tali istanze una volta completato.
- Oppure, il controllo della durata della vita delle istanze di dipendenza può essere abbandonato a un contenitore IoC (maggiori informazioni al riguardo di seguito).
Quando utilizzare DI?
- Laddove sarà probabilmente necessario sostituire una dipendenza con un'implementazione equivalente,
- Ogni volta che dovrai testare l'unità dei metodi di una classe in modo isolato dalle sue dipendenze,
- Laddove l'incertezza della durata di una dipendenza può giustificare la sperimentazione (ad es. Hey,
MyDepClass
è sicuro il thread - e se lo rendessimo un singleton e iniettassimo la stessa istanza in tutti i consumatori?)
Esempio
Ecco una semplice implementazione in C #. Data la seguente classe di consumo:
public class MyLogger
{
public void LogRecord(string somethingToLog)
{
Console.WriteLine("{0:HH:mm:ss} - {1}", DateTime.Now, somethingToLog);
}
}
Sebbene apparentemente innocuo, ha due static
dipendenze da altre due classi System.DateTime
e System.Console
, che non solo limitano le opzioni di output della registrazione (accedere alla console sarà inutile se nessuno sta guardando), ma peggio, è difficile testare automaticamente data la dipendenza da un orologio di sistema non deterministico.
Possiamo tuttavia applicare DIP
a questa classe, sottrando la preoccupazione del timestamp come dipendenza e accoppiandolo MyLogger
solo a una semplice interfaccia:
public interface IClock
{
DateTime Now { get; }
}
Possiamo anche allentare la dipendenza Console
da un'astrazione, come a TextWriter
. L'iniezione di dipendenza viene in genere implementata come constructor
iniezione (passando un'astrazione a una dipendenza come parametro al costruttore di una classe consumante) o Setter Injection
(passando la dipendenza tramite un setXyz()
setter o una proprietà .Net con {set;}
definita). È preferibile l'iniezione del costruttore, poiché ciò garantisce che la classe sarà nello stato corretto dopo la costruzione e consente ai campi di dipendenza interna di essere contrassegnati come readonly
(C #) o final
(Java). Quindi, usando l'iniezione del costruttore nell'esempio sopra, questo ci lascia con:
public class MyLogger : ILogger // Others will depend on our logger.
{
private readonly TextWriter _output;
private readonly IClock _clock;
// Dependencies are injected through the constructor
public MyLogger(TextWriter stream, IClock clock)
{
_output = stream;
_clock = clock;
}
public void LogRecord(string somethingToLog)
{
// We can now use our dependencies through the abstraction
// and without knowledge of the lifespans of the dependencies
_output.Write("{0:yyyy-MM-dd HH:mm:ss} - {1}", _clock.Now, somethingToLog);
}
}
(È Clock
necessario fornire un concreto , che ovviamente potrebbe essere ripristinato DateTime.Now
, e le due dipendenze devono essere fornite da un contenitore IoC tramite iniezione del costruttore)
È possibile creare un Unit Test automatizzato, il che dimostra definitivamente che il nostro logger funziona correttamente, poiché ora abbiamo il controllo sulle dipendenze - il tempo e possiamo spiare l'output scritto:
[Test]
public void LoggingMustRecordAllInformationAndStampTheTime()
{
// Arrange
var mockClock = new Mock<IClock>();
mockClock.Setup(c => c.Now).Returns(new DateTime(2015, 4, 11, 12, 31, 45));
var fakeConsole = new StringWriter();
// Act
new MyLogger(fakeConsole, mockClock.Object)
.LogRecord("Foo");
// Assert
Assert.AreEqual("2015-04-11 12:31:45 - Foo", fakeConsole.ToString());
}
Prossimi passi
L'iniezione di dipendenza è invariabilmente associata a un contenitore Inversion of Control (IoC) , per iniettare (fornire) le istanze di dipendenza concrete e gestire istanze di durata della vita. Durante il processo di configurazione / bootstrap, i IoC
contenitori consentono di definire quanto segue:
- mappatura tra ciascuna astrazione e l'implementazione concreta configurata (ad esempio "ogni volta che un consumatore richiede un
IBar
, restituisce ConcreteBar
un'istanza" )
- le politiche possono essere impostate per la gestione della durata di ogni dipendenza, ad esempio per creare un nuovo oggetto per ogni istanza del consumatore, condividere un'istanza di dipendenza singleton tra tutti i consumatori, condividere la stessa istanza di dipendenza solo attraverso lo stesso thread, ecc.
- In .Net, i contenitori IoC sono a conoscenza di protocolli come
IDisposable
e assumeranno la responsabilità delle Disposing
dipendenze in linea con la gestione della durata della vita configurata.
In genere, una volta che i contenitori IoC sono stati configurati / avviati, funzionano senza soluzione di continuità in background consentendo al programmatore di concentrarsi sul codice a portata di mano piuttosto che preoccuparsi delle dipendenze.
La chiave del codice DI-friendly è evitare l'accoppiamento statico di classi e non usare new () per la creazione di dipendenze
Come nell'esempio precedente, il disaccoppiamento delle dipendenze richiede un certo sforzo di progettazione e, per lo sviluppatore, è necessario un cambio di paradigma per rompere l'abitudine di new
gestire direttamente le dipendenze e affidarsi invece al contenitore per gestire le dipendenze.
Ma i vantaggi sono molti, soprattutto nella capacità di testare a fondo la tua classe di interesse.
Nota : la creazione / mappatura / proiezione (via new ..()
) di POCO / POJO / DTO di serializzazione / Grafici di entità / Proiezioni JSON anonime e altri - ovvero classi o record "Solo dati" - utilizzati o restituiti da metodi non sono considerati dipendenze UML) e non soggetto a DI. Usare new
per proiettare questi va bene.