Facciamo un semplice esempio: forse stai iniettando un mezzo di registrazione.
Iniezione di una classe
class Worker: IWorker
{
ILogger _logger;
Worker(ILogger logger)
{
_logger = logger;
}
void SomeMethod()
{
_logger.Debug("This is a debug log statement.");
}
}
Penso che sia abbastanza chiaro cosa sta succedendo. Inoltre, se stai utilizzando un contenitore IoC, non hai nemmeno bisogno di iniettare qualcosa in modo esplicito, basta aggiungere alla tua radice di composizione:
container.RegisterType<ILogger, ConcreteLogger>();
container.RegisterType<IWorker, Worker>();
....
var worker = container.Resolve<IWorker>();
Quando esegui il debug di Worker
, uno sviluppatore deve solo consultare la root di composizione per determinare quale classe concreta viene utilizzata.
Se uno sviluppatore ha bisogno di una logica più complicata, ha l'intera interfaccia con cui lavorare:
void SomeMethod()
{
if (_logger.IsDebugEnabled) {
_logger.Debug("This is a debug log statement.");
}
}
Iniezione di un metodo
class Worker
{
Action<string> _methodThatLogs;
Worker(Action<string> methodThatLogs)
{
_methodThatLogs = methodThatLogs;
}
void SomeMethod()
{
_methodThatLogs("This is a logging statement");
}
}
Per prima cosa, nota che il parametro costruttore ha un nome più lungo ora, methodThatLogs
. Questo è necessario perché non puoi dire cosa si suppone debba fare un Action<string>
. Con l'interfaccia, era completamente chiaro, ma qui dobbiamo ricorrere al fare affidamento sulla denominazione dei parametri. Ciò sembra intrinsecamente meno affidabile e più difficile da applicare durante una compilazione.
Ora, come possiamo iniettare questo metodo? Bene, il contenitore IoC non lo farà per te. Pertanto, ti viene iniettato esplicitamente quando installi Worker
. Ciò solleva un paio di problemi:
- È più lavoro istanziare un
Worker
- Gli sviluppatori che tentano di eseguire il debug di
Worker
troveranno più difficile capire quale chiamata concreta venga chiamata. Non possono semplicemente consultare la radice della composizione; dovranno tracciare il codice.
Che ne dici se abbiamo bisogno di una logica più complicata? La tua tecnica espone solo un metodo. Ora suppongo che potresti preparare le cose complicate nella lambda:
var worker = new Worker((s) => { if (log.IsDebugEnabled) log.Debug(s) } );
ma quando scrivi i tuoi test unitari, come collaudi l'espressione lambda? È anonimo, quindi il tuo framework di test unitario non può istanziarlo direttamente. Forse puoi capire un modo intelligente per farlo, ma probabilmente sarà un PITA più grande di usare un'interfaccia.
Riepilogo delle differenze:
- L'iniezione di un solo metodo rende più difficile dedurre lo scopo, mentre un'interfaccia comunica chiaramente lo scopo.
- L'iniezione solo di un metodo espone meno funzionalità alla classe che riceve l'iniezione. Anche se non ne hai bisogno oggi, potresti averne bisogno domani.
- Non puoi iniettare automaticamente solo un metodo usando un contenitore IoC.
- Non si può sapere dalla radice della composizione quale classe concreta è al lavoro in una particolare istanza.
- È un problema testare l'espressione lambda stessa.
Se stai bene con tutto quanto sopra, allora è OK iniettare solo il metodo. Altrimenti ti suggerirei di rimanere fedele alla tradizione e iniettare un'interfaccia.