Penso che il modo più semplice per capire la differenza tra i due e perché un contenitore DI sia molto meglio di un localizzatore di servizi sia in primo luogo pensare al motivo per cui facciamo l'inversione di dipendenza.
Facciamo inversione di dipendenza in modo che ogni classe indichi esplicitamente da cosa dipende per l'operazione. Lo facciamo perché questo crea l'accoppiamento più lento che possiamo ottenere. Più l'accoppiamento è allentato, più è facile eseguire il test e il refactoring (e in genere richiede il meno refactoring in futuro perché il codice è più pulito).
Diamo un'occhiata alla seguente classe:
public class MySpecialStringWriter
{
private readonly IOutputProvider outputProvider;
public MySpecialFormatter(IOutputProvider outputProvider)
{
this.outputProvider = outputProvider;
}
public void OutputString(string source)
{
this.outputProvider.Output("This is the string that was passed: " + source);
}
}
In questa classe, stiamo dichiarando esplicitamente che abbiamo bisogno di un IOutputProvider e nient'altro per far funzionare questa classe. Questo è completamente testabile e ha una dipendenza da una singola interfaccia. Posso spostare questa classe ovunque nella mia applicazione, incluso un progetto diverso e tutto ciò di cui ha bisogno è l'accesso all'interfaccia IOutputProvider. Se altri sviluppatori vogliono aggiungere qualcosa di nuovo a questa classe, che richiede una seconda dipendenza, devono essere espliciti su ciò di cui hanno bisogno nel costruttore.
Dai un'occhiata alla stessa classe con un localizzatore di servizi:
public class MySpecialStringWriter
{
private readonly ServiceLocator serviceLocator;
public MySpecialFormatter(ServiceLocator serviceLocator)
{
this.serviceLocator = serviceLocator;
}
public void OutputString(string source)
{
this.serviceLocator.OutputProvider.Output("This is the string that was passed: " + source);
}
}
Ora ho aggiunto il localizzatore di servizi come dipendenza. Ecco i problemi che sono immediatamente evidenti:
- Il primo problema con questo è che ci vuole più codice per ottenere lo stesso risultato. Più codice è male. Non è molto più codice ma è ancora di più.
- Il secondo problema è che la mia dipendenza non è più esplicita . Devo ancora iniettare qualcosa nella classe. Tranne ora ciò che voglio non è esplicito. È nascosto in una proprietà della cosa che ho richiesto. Ora ho bisogno di accedere sia a ServiceLocator che a IOutputProvider se voglio spostare la classe in un assembly diverso.
- Il terzo problema è che un'ulteriore dipendenza può essere presa da un altro sviluppatore che non si rende nemmeno conto di prenderla quando aggiunge codice alla classe.
- Infine, questo codice è più difficile da testare (anche se ServiceLocator è un'interfaccia) perché dobbiamo deridere ServiceLocator e IOutputProvider anziché solo IOutputProvider
Quindi perché non rendere il servizio di localizzazione una classe statica? Diamo un'occhiata:
public class MySpecialStringWriter
{
public void OutputString(string source)
{
ServiceLocator.OutputProvider.Output("This is the string that was passed: " + source);
}
}
È molto più semplice, vero?
Sbagliato.
Diciamo che IOutputProvider è implementato da un servizio web molto lungo che scrive la stringa in quindici diversi database in tutto il mondo e richiede molto tempo per il completamento.
Proviamo a testare questa classe. Abbiamo bisogno di un'implementazione diversa di IOutputProvider per il test. Come scriviamo il test?
Per fare ciò, dobbiamo fare una configurazione di fantasia nella classe statica ServiceLocator per usare un'implementazione diversa di IOutputProvider quando viene chiamato dal test. Anche scrivere quella frase è stato doloroso. L'attuazione sarebbe tortuosa e sarebbe un incubo per la manutenzione . Non dovremmo mai aver bisogno di modificare una classe specificamente per i test, specialmente se quella classe non è la classe che stiamo effettivamente provando a testare.
Quindi ora ti resta con a) un test che sta causando cambiamenti di codice invadenti nella classe ServiceLocator non correlata; oppure b) nessuna prova. E ti rimane anche una soluzione meno flessibile.
Quindi la classe del localizzatore di servizio deve essere iniettata nel costruttore. Ciò significa che ci rimangono i problemi specifici menzionati in precedenza. Il localizzatore di servizi richiede più codice, dice ad altri sviluppatori che ha bisogno di cose che non lo fanno, incoraggia gli altri sviluppatori a scrivere codice peggiore e ci dà meno flessibilità andando avanti.
In poche parole, i localizzatori di servizi aumentano l'accoppiamento in un'applicazione e incoraggiano altri sviluppatori a scrivere codice altamente accoppiato .