Qual è la differenza tra l'utilizzo dell'iniezione di dipendenza con un contenitore e l'uso di un localizzatore di servizi?


107

Comprendo che l'istanza diretta delle dipendenze all'interno di una classe è considerata una cattiva pratica. Questo ha senso perché fare così strettamente accoppia tutto ciò che a sua volta rende molto difficili i test.

Quasi tutti i framework che ho incontrato sembrano favorire l'iniezione di dipendenza con un container rispetto all'uso dei localizzatori di servizio. Entrambi sembrano raggiungere la stessa cosa consentendo al programmatore di specificare quale oggetto deve essere restituito quando una classe richiede una dipendenza.

Qual è la differenza tra i due? Perché dovrei scegliere l'uno rispetto all'altro?


3
Questo è stato chiesto (e risposto) su StackOverflow: stackoverflow.com/questions/8900710/…
Kyralessa il

2
A mio avviso, non è affatto raro che gli sviluppatori ingannino gli altri nel pensare che il loro localizzatore di servizi sia l'iniezione di dipendenza. Lo fanno perché si ritiene che l'iniezione di dipendenza sia più avanzata.
Gherman,


1
Sono sorpreso che nessuno abbia menzionato che con i Service Locator è più difficile sapere quando una risorsa transitoria può essere distrutta. Ho sempre pensato che fosse la ragione principale.
Buh Buh,

Risposte:


126

Quando l'oggetto stesso è responsabile della richiesta delle sue dipendenze, invece di accettarle attraverso un costruttore, nasconde alcune informazioni essenziali. È solo leggermente migliore del caso molto stretto dell'uso newdi un'istanza delle sue dipendenze. Riduce l'accoppiamento perché in realtà è possibile modificare le dipendenze che ottiene, ma ha ancora una dipendenza che non è possibile scuotere: il localizzatore di servizio. Questa diventa la cosa da cui tutto dipende.

Un contenitore che fornisce dipendenze tramite argomenti del costruttore offre la massima chiarezza. Vediamo subito che un oggetto ha bisogno sia di un AccountRepositoryche di un PasswordStrengthEvaluator. Quando si utilizza un localizzatore di servizi, tali informazioni sono meno immediatamente evidenti. Vedresti subito un caso in cui un oggetto ha, oh, 17 dipendenze, e dici a te stesso: "Hmm, sembra molto. Cosa sta succedendo lì?" Le chiamate a un localizzatore di servizi possono essere distribuite tra i vari metodi e nascondersi dietro la logica condizionale, e potresti non rendertene conto di aver creato una "classe di Dio", una che fa tutto. Forse quella classe potrebbe essere rifattorizzata in 3 classi più piccole che sono più focalizzate e quindi più verificabili.

Ora considera i test. Se un oggetto utilizza un localizzatore di servizi per ottenere le sue dipendenze, anche il framework di test avrà bisogno di un localizzatore di servizi. In un test, configurerai il localizzatore di servizi per fornire le dipendenze all'oggetto in prova, forse a FakeAccountRepositorye a VeryForgivingPasswordStrengthEvaluator, quindi eseguirai il test. Ma è più lavoro che specificare le dipendenze nel costruttore dell'oggetto. E anche il framework di test dipende dal localizzatore di servizi. È un'altra cosa che devi configurare in ogni test, il che rende i test di scrittura meno interessanti.

Cerca "Serivce Locator è un Anti-Pattern" per l'articolo di Mark Seeman al riguardo. Se sei nel mondo .Net, prendi il suo libro. È molto buono.


1
Leggendo la domanda ho pensato a Adaptive Code via C # di Gary McLean Hall, che si allinea abbastanza bene con questa risposta. Ha alcune buone analogie come il localizzatore di servizi che è la chiave per una cassaforte; quando viene consegnato a una classe, può creare dipendenze a piacimento che potrebbero non essere facili da individuare.
Dennis il

10
Una cosa che l'IMO deve essere aggiunta al constructor supplied dependenciesvs service locatorè che il primo può essere verificato in fase di compilazione, mentre il secondo può essere verificato solo in fase di esecuzione.
andras,

2
Inoltre, un localizzatore di servizi non scoraggia un componente dall'avere molte dipendenze. Se devi aggiungere 10 parametri al tuo costruttore, hai un buon senso che qualcosa non vada nel tuo codice. Tuttavia, se ti capita di effettuare 10 chiamate statiche a un localizzatore di servizi, potrebbe non essere evidente che hai qualche tipo di problema. Ho lavorato su un grande progetto con un localizzatore di servizi e questo è stato il problema più grande. Era troppo facile cortocircuitare un nuovo percorso piuttosto che sedersi, riflettere, riprogettare e rifrattore.
Pace

1
A proposito di test - But that's more work than specifying dependencies in the object's constructor.Vorrei obiettare. Con un localizzatore di servizi è sufficiente specificare le 3 dipendenze effettivamente necessarie per il test. Con un DI basato sul costruttore è necessario specificarne TUTTI 10, anche se 7 non sono utilizzati.
Vilx-

4
@ Vilx- Se hai più parametri del costruttore inutilizzati, è probabile che la tua classe stia violando il principio della responsabilità singola.
RB.

79

Immagina di essere un lavoratore in una fabbrica che produce scarpe .

Sei responsabile dell'assemblaggio delle scarpe e quindi avrai bisogno di molte cose per farlo.

  • Pelle
  • Nastro di misurazione
  • Colla
  • Chiodi
  • Martello
  • Forbici
  • Lacci delle scarpe

E così via.

Sei al lavoro in fabbrica e sei pronto per iniziare. Hai un elenco di istruzioni su come procedere, ma non hai ancora nessuno dei materiali o degli strumenti.

Un Service Locator è come un Foreman che può aiutarti a ottenere ciò di cui hai bisogno.

Chiedete al Service Locator ogni volta che avete bisogno di qualcosa e se ne vanno per trovarlo per voi. Il Service Locator è stato informato in anticipo su ciò che è probabile che tu chieda e su come trovarlo.

Faresti meglio a sperare di non chiedere qualcosa di inaspettato però. Se il Localizzatore non è stato informato in anticipo di un particolare strumento o materiale, non saranno in grado di ottenerlo per te e si limiteranno a scrollarti le spalle.

localizzatore di servizi

Un contenitore DIend Dependency Injection (DI) è come una grande scatola che si riempie di tutto ciò di cui tutti hanno bisogno all'inizio della giornata.

All'avvio della fabbrica, il Big Boss noto come Composition Root afferra il contenitore e distribuisce tutto ai Line Manager .

I Line Manager ora hanno ciò di cui hanno bisogno per svolgere i loro compiti per il giorno. Prendono ciò che hanno e trasmettono ciò che è necessario ai loro subordinati.

Questo processo continua, con dipendenze che scendono lungo la linea di produzione. Alla fine un container di materiali e strumenti si presenta per il tuo Foreman.

Foreman ora distribuisce esattamente ciò di cui hai bisogno a te e agli altri lavoratori, senza che tu nemmeno lo richieda.

Fondamentalmente, non appena ti presenti al lavoro, tutto ciò di cui hai bisogno è già lì in una scatola che ti aspetta. Non hai bisogno di sapere nulla su come ottenerli.

contenitore per iniezione di dipendenza


45
Questa è una descrizione eccellente di ciò che sono ciascuna di queste 2 cose, anche diagrammi super belli! Ma questo non risponde alla domanda "Perché scegliere l'uno rispetto all'altro"?
Matthieu M.

21
"Faresti meglio a sperare di non chiedere qualcosa di inaspettato, però. Se il Localizzatore non è stato informato in anticipo su un particolare strumento o materiale" Ma questo può valere anche per un contenitore DI.
Kenneth K.,

5
@FrankHopkins Se si omette di registrare un'interfaccia / classe con il proprio contenitore DI, il codice verrà comunque compilato e fallirà prontamente in fase di esecuzione.
Kenneth K.,

3
Entrambi hanno la stessa probabilità di fallire, a seconda di come sono impostati. Ma con un contenitore DI, è più probabile che si verifichi un problema prima, quando viene costruita la classe X, piuttosto che successivamente, quando la classe X ha effettivamente bisogno di quella dipendenza. È probabilmente migliore, perché è più facile imbattersi nel problema, trovarlo e risolverlo. Sono d'accordo con Kenneth, tuttavia, che la risposta suggerisce che questo problema esiste solo per i localizzatori di servizi, il che non è assolutamente vero.
GolezTrol,

3
Ottima risposta, ma penso che manchi un'altra cosa; vale a dire, se si chiede il capo in qualsiasi momento per un elefante, e lui non sa come ottenere uno, otterrà uno per voi senza fare asked- anche se non ne hai bisogno. E qualcuno che sta cercando di eseguire il debug di un problema sul pavimento (uno sviluppatore se lo desideri) si chiederà che un elefante sta facendo qui su questa scrivania, rendendo così più difficile per loro ragionare su cosa è effettivamente andato storto. Mentre con DI, tu, la classe operaia, dichiari in anticipo tutte le dipendenze di cui hai bisogno prima ancora di iniziare il lavoro, rendendo più facile ragionare.
Stephen Byrne,

10

Un paio di punti extra che ho trovato durante la ricerca del web:

  • L'iniezione di dipendenze nel costruttore semplifica la comprensione delle esigenze di una classe. Gli IDE moderni suggeriranno quali argomenti accetta il costruttore e i loro tipi. Se usi un localizzatore di servizi devi leggere la classe prima di sapere quali dipendenze sono richieste.
  • L'iniezione di dipendenza sembra aderire al principio "non chiedere" più dei locatori di servizio. Imponendo che una dipendenza sia di un tipo specifico si "dice" quali dipendenze sono richieste. È impossibile creare un'istanza della classe senza passare le dipendenze richieste. Con un localizzatore di servizi "chiedi" un servizio e se il localizzatore di servizi non è configurato correttamente, potresti non ottenere ciò che è richiesto.

4

Sto arrivando tardi a questa festa ma non posso resistere.

Qual è la differenza tra l'utilizzo dell'iniezione di dipendenza con un contenitore e l'uso di un localizzatore di servizi?

A volte nessuno. Ciò che fa la differenza è ciò che sa di cosa.

Sai che stai utilizzando un localizzatore di servizi quando il client che cerca la dipendenza conosce il contenitore. Un client che sa come trovare le sue dipendenze, anche quando passa attraverso un contenitore per ottenerle, è il modello di localizzazione del servizio.

Questo significa che se si desidera evitare l'individuazione del servizio non è possibile utilizzare un contenitore? No. Devi solo impedire ai clienti di conoscere il contenitore. La differenza fondamentale è dove si utilizza il contenitore.

Diciamo Clientbisogni Dependency. Il contenitore ha un Dependency.

class Client { 
    Client() { 
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        this.dependency = (Dependency) beanfactory.getBean("dependency");        
    }
    Dependency dependency;
}

Abbiamo appena seguito il modello di localizzazione del servizio perché Clientsa come trovarlo Dependency. Sicuro che usa un hard coded ClassPathXmlApplicationContextma anche se si inietta che hai ancora un localizzatore di servizi perché Clientchiama beanfactory.getBean().

Per evitare l'individuazione del servizio non è necessario abbandonare questo contenitore. Devi solo spostarlo da Clientcosì Clientnon lo so.

class EntryPoint { 
    public static void main(String[] args) {
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        Client client = (Client) beanfactory.getBean("client");

        client.start();
    }
}

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dependency" class="Dependency">
    </bean>

    <bean id="client" class="Client">
        <constructor-arg value="dependency" />        
    </bean>
</beans>

Nota come Clientora non ha idea che esista il contenitore:

class Client { 
    Client(Dependency dependency) { 

        this.dependency = dependency;        
    }
    Dependency dependency;
}

Sposta il contenitore fuori da tutti i client e incollalo nella parte principale dove può costruire un grafico a oggetti di tutti i tuoi oggetti longevi. Scegli uno di quegli oggetti da estrarre e chiama un metodo su di esso e inizi a spuntare l'intero grafico.

Ciò sposta tutta la costruzione statica nei contenitori XML ma mantiene tutti i vostri clienti beati ignoranti su come trovare le loro dipendenze.

Ma principale sa ancora come individuare le dipendenze! Sì lo fa. Ma non diffondendo tale conoscenza intorno hai evitato il problema principale del localizzatore di servizi. La decisione di utilizzare un contenitore è ora presa in un unico posto e potrebbe essere modificata senza riscrivere centinaia di client.


1

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 .


Service Locators (SL) e Dependency Injection (DI) risolvono entrambi lo stesso problema in modi simili. L'unica differenza se si sta "dicendo" DI o "chiedendo" SL per dipendenze.
Matthew Whited,

@MatthewWhited La differenza principale è il numero di dipendenze implicite che si prendono con un localizzatore di servizi. E questo fa una grande differenza per la manutenibilità e la stabilità a lungo termine del codice.
Stephen,

Non lo è davvero. Hai lo stesso numero di dipendenze in entrambi i modi.
Matthew Whited,

Inizialmente sì, ma puoi prendere dipendenze con un localizzatore di servizi senza accorgerti che stai assumendo dipendenze. Che sconfigge gran parte del motivo per cui facciamo l'inversione di dipendenza in primo luogo. Una classe dipende dalle proprietà A e B, un'altra dipende da B e C. Improvvisamente il Service Locator diventa una classe divina. Il contenitore DI no e le due classi dipendono correttamente solo rispettivamente da A, B, B e C. Questo rende il refactoring cento volte più semplice. I Service Locator sono insidiosi in quanto appaiono come DI ma non lo sono.
Stephen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.