In primo luogo, voglio spiegare un presupposto che ho fatto per questa risposta. Non è sempre vero, ma abbastanza spesso:
Le interfacce sono aggettivi; le classi sono sostantivi.
(In realtà, ci sono anche interfacce che sono sostantivi, ma voglio generalizzare qui.)
Ad esempio, un'interfaccia può essere qualcosa come IDisposable
, IEnumerable
o IPrintable
. Una classe è un'implementazione effettiva di una o più di queste interfacce: List
o Map
entrambe possono essere implementazioni di IEnumerable
.
Per capire il punto: spesso le tue lezioni dipendono l'una dall'altra. Ad esempio potresti avere una Database
classe che accede al tuo database (ah, sorpresa! ;-)), ma vuoi anche che questa classe effettui il log per accedere al database. Supponi di avere un'altra classe Logger
, quindi Database
abbia una dipendenza Logger
.
Fin qui tutto bene.
Puoi modellare questa dipendenza all'interno della tua Database
classe con la seguente riga:
var logger = new Logger();
e tutto va bene. Va bene fino al giorno in cui ti rendi conto che hai bisogno di un sacco di logger: a volte vuoi accedere alla console, a volte al file system, a volte utilizzando TCP / IP e un server di log remoto, e così via ...
E ovviamente NON vuoi cambiare tutto il tuo codice (nel frattempo ne hai milioni) e sostituire tutte le righe
var logger = new Logger();
di:
var logger = new TcpLogger();
Innanzitutto, non è divertente. In secondo luogo, questo è soggetto a errori. Terzo, questo è un lavoro stupido e ripetitivo per una scimmia addestrata. Allora cosa fai?
Ovviamente è una buona idea introdurre un'interfaccia ICanLog
(o simile) implementata da tutti i vari logger. Quindi il passaggio 1 nel tuo codice è che lo fai:
ICanLog logger = new Logger();
Ora l'inferenza del tipo non cambia più tipo, hai sempre un'unica interfaccia su cui sviluppare. Il prossimo passo è che non vuoi avere new Logger()
più e più volte. Quindi metti l'affidabilità per creare nuove istanze in una singola classe di fabbrica centrale e ottieni codice come:
ICanLog logger = LoggerFactory.Create();
La fabbrica stessa decide quale tipo di logger creare. Al tuo codice non importa più, e se vuoi cambiare il tipo di logger in uso, lo cambi una volta : All'interno della fabbrica.
Ora, ovviamente, puoi generalizzare questa fabbrica e farla funzionare per qualsiasi tipo:
ICanLog logger = TypeFactory.Create<ICanLog>();
Da qualche parte questo TypeFactory necessita di dati di configurazione su quale classe effettiva creare un'istanza quando viene richiesto un tipo di interfaccia specifico, quindi è necessario un mapping. Ovviamente puoi fare questa mappatura all'interno del tuo codice, ma poi un cambio di tipo significa ricompilare. Ma potresti anche inserire questa mappatura in un file XML, ad es. Questo ti permette di cambiare la classe effettivamente usata anche dopo il tempo di compilazione (!), Ciò significa dinamicamente, senza ricompilare!
Per darti un utile esempio per questo: pensa a un software che non si registra normalmente, ma quando il tuo cliente chiama e chiede aiuto perché ha un problema, tutto ciò che gli invii è un file di configurazione XML aggiornato, e ora ha registrazione abilitata e il tuo supporto può utilizzare i file di registro per aiutare il tuo cliente.
E ora, quando sostituisci un po 'i nomi, finisci con una semplice implementazione di un Service Locator , che è uno dei due schemi per Inversion of Control (poiché inverti il controllo su chi decide quale classe esatta istanziare).
Tutto sommato ciò riduce le dipendenze nel codice, ma ora tutto il codice ha una dipendenza dal localizzatore di servizi singolo centrale.
L'iniezione di dipendenze è ora il passo successivo in questa linea: basta sbarazzarsi di questa singola dipendenza dal localizzatore di servizi: invece di varie classi che chiedono al localizzatore di servizi un'implementazione per una specifica interfaccia, tu - ancora una volta - ripristini il controllo su chi crea un'istanza di cosa .
Con l'iniezione di dipendenza, la tua Database
classe ora ha un costruttore che richiede un parametro di tipo ICanLog
:
public Database(ICanLog logger) { ... }
Ora il tuo database ha sempre un logger da usare, ma non sa più da dove provenga questo logger.
Ed è qui che entra in gioco un framework DI: configuri di nuovo i tuoi mapping e poi chiedi al tuo DI framework di creare un'istanza per te. Poiché la Application
classe richiede ICanPersistData
un'implementazione, Database
viene iniettata un'istanza di - ma per questo deve prima creare un'istanza del tipo di logger per cui è configurata ICanLog
. E così via ...
Quindi, per farla breve: l'iniezione di dipendenza è uno dei due modi per rimuovere le dipendenze dal codice. È molto utile per le modifiche di configurazione dopo il tempo di compilazione ed è un'ottima cosa per i test unitari (in quanto semplifica l'iniezione di stub e / o mock).
In pratica, ci sono cose che non puoi fare senza un localizzatore di servizi (ad esempio, se non sai in anticipo quante istanze hai bisogno di un'interfaccia specifica: un framework DI inietta sempre solo un'istanza per parametro, ma puoi chiamare un localizzatore di servizi all'interno di un loop, ovviamente), quindi molto spesso ogni framework DI fornisce anche un localizzatore di servizi.
Ma sostanzialmente, tutto qui.
PS: Quello che ho descritto qui è una tecnica chiamata iniezione del costruttore , c'è anche l' iniezione di proprietà in cui non sono i parametri del costruttore, ma le proprietà vengono utilizzate per definire e risolvere le dipendenze. Pensa all'iniezione di proprietà come una dipendenza opzionale e all'iniezione del costruttore come dipendenze obbligatorie. Ma la discussione su questo va oltre lo scopo di questa domanda.