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 si utilizza un contenitore IoC, non è nemmeno necessario iniettare nulla in modo esplicito, è sufficiente aggiungere alla radice della composizione:
container.RegisterType<ILogger, ConcreteLogger>();
container.RegisterType<IWorker, Worker>();
....
var worker = container.Resolve<IWorker>();
Durante il debug Worker
, uno sviluppatore deve solo consultare la radice della 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");
}
}
In primo luogo, si noti che il parametro del costruttore ha un nome più lungo ora, methodThatLogs
. Questo è necessario perché non puoi dire cosa Action<string>
dovrebbe fare. Con l'interfaccia, era completamente chiaro, ma qui dobbiamo ricorrere a fare affidamento sulla denominazione dei parametri. Questo sembra intrinsecamente meno affidabile e più difficile da applicare durante una build.
Ora, come iniettiamo questo metodo? Bene, il contenitore IoC non lo farà per te. Quindi ti viene iniettato esplicitamente quando crei un'istanza Worker
. Ciò solleva un paio di problemi:
- È più lavoro istanziare a
Worker
- Gli sviluppatori che tentano di eseguire il debug
Worker
troveranno più difficile capire quale istanza concreta viene 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 cuocere le cose complicate nella lambda:
var worker = new Worker((s) => { if (log.IsDebugEnabled) log.Debug(s) } );
ma quando scrivi i tuoi test unitari, come testate quell'espressione lambda? È anonimo, quindi il framework di unit test non può istanziarlo direttamente. Forse puoi trovare un modo intelligente per farlo, ma probabilmente sarà un PITA più grande rispetto all'uso di un'interfaccia.
Riepilogo delle differenze:
- Iniettare solo un metodo rende più difficile inferire lo scopo, mentre un'interfaccia comunica chiaramente lo scopo.
- L'iniezione di un solo metodo espone meno funzionalità alla classe che riceve l'iniezione. Anche se non ne hai bisogno oggi, potresti averne bisogno domani.
- Non è possibile iniettare automaticamente solo un metodo utilizzando un contenitore IoC.
- Dalla radice della composizione non è possibile sapere quale classe concreta è al lavoro in una particolare istanza.
- È un problema test unitario dell'espressione lambda stessa.
Se stai bene con tutto quanto sopra, allora va bene iniettare solo il metodo. Altrimenti ti suggerirei di restare fedele alla tradizione e di iniettare un'interfaccia.