ServiceLocator è un anti-pattern?


137

Di recente ho letto l'articolo di Mark Seemann sull'anti-schema di Service Locator.

L'autore sottolinea due motivi principali per cui ServiceLocator è un anti-schema:

  1. Problema di utilizzo dell'API (che sto perfettamente bene)
    Quando la classe impiega un localizzatore di servizi, è molto difficile vedere le sue dipendenze poiché, nella maggior parte dei casi, la classe ha un solo costruttore PARAMETERLESS. Contrariamente a ServiceLocator, l'approccio DI espone esplicitamente le dipendenze tramite i parametri del costruttore in modo che le dipendenze siano facilmente visibili in IntelliSense.

  2. Problema di manutenzione (che mi confonde)
    Considera l'esempio seguente

Abbiamo una classe "MyType" che utilizza un approccio di localizzazione dei servizi:

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();
    }
}

Ora vogliamo aggiungere un'altra dipendenza alla classe "MyType"

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

Ed ecco dove inizia il mio malinteso. L'autore dice:

Diventa molto più difficile dire se stai introducendo un cambiamento di rottura o meno. È necessario comprendere l'intera applicazione in cui viene utilizzato Service Locator e il compilatore non sarà di aiuto.

Ma aspetta un secondo, se usassimo l'approccio DI, introdurremmo una dipendenza con un altro parametro nel costruttore (in caso di iniezione del costruttore). E il problema sarà ancora lì. Se potremmo dimenticare di installare ServiceLocator, potremmo dimenticare di aggiungere un nuovo mapping nel nostro contenitore IoC e l'approccio DI avrebbe lo stesso problema di runtime.

Inoltre, l'autore ha menzionato le difficoltà del test unitario. Ma non avremo problemi con l'approccio DI? Non avremo bisogno di aggiornare tutti i test che stavano istanziando quella classe? Li aggiorneremo per passare una nuova dipendenza derisa solo per rendere compilabile il nostro test. E non vedo alcun beneficio da quell'aggiornamento e dal tempo speso.

Non sto cercando di difendere l'approccio Service Locator. Ma questo malinteso mi fa pensare che sto perdendo qualcosa di molto importante. Qualcuno potrebbe dissipare i miei dubbi?

AGGIORNAMENTO (SINTESI):

La risposta alla mia domanda "Is Service Locator è un anti-schema" dipende dalle circostanze. E sicuramente non consiglierei di cancellarlo dal tuo elenco di strumenti. Potrebbe essere molto utile quando inizi a gestire il codice legacy. Se sei abbastanza fortunato da essere all'inizio del tuo progetto, l'approccio DI potrebbe essere una scelta migliore in quanto presenta alcuni vantaggi rispetto a Service Locator.

E qui ci sono le principali differenze che mi hanno convinto a non utilizzare Service Locator per i miei nuovi progetti:

  • Più ovvio e importante: Service Locator nasconde le dipendenze di classe
  • Se stai utilizzando un contenitore IoC, probabilmente eseguirà la scansione di tutti i costruttori all'avvio per convalidare tutte le dipendenze e fornirti un feedback immediato sui mapping mancanti (o sulla configurazione errata); ciò non è possibile se si utilizza il contenitore IoC come Service Locator

Per i dettagli leggi le risposte eccellenti fornite di seguito.


"Non avremo bisogno di aggiornare tutti i test che stavano istanziando quella classe?" Non necessariamente vero se si utilizza un builder nei test. In questo caso dovresti solo aggiornare il builder.
Peter Karlsson,

Hai ragione, dipende. Nelle grandi app Android, ad esempio, finora le persone sono state molto riluttanti a utilizzare DI a causa di problemi di prestazioni su dispositivi mobili a bassa specifica. In questi casi, devi trovare un'alternativa per scrivere ancora codice verificabile, e direi che Service Locator è un sostituto abbastanza buono in quel caso. (Nota: le cose potrebbero cambiare per Android quando il nuovo framework Dagger 2.0 DI è abbastanza maturo.)
G. Lombard,

1
Si noti che, dal momento che questa domanda è stata pubblicata, c'è un aggiornamento sul Service Locator di Mark Seemann è un post Anti-Pattern che descrive come il localizzatore del servizio viola OOP rompendo l'incapsulamento, che è il suo argomento migliore (e il motivo alla base di tutti i sintomi che ha usato in tutti gli argomenti precedenti). Aggiornamento 2015-10-26: Il problema fondamentale con Service Locator è che viola l'incapsulamento .
NightOwl888

Risposte:


125

Se definisci pattern come anti-pattern solo perché ci sono alcune situazioni in cui non si adatta, allora SÌ è un anti-pattern. Ma con questo ragionamento tutti gli schemi sarebbero anche anti schemi.

Dobbiamo invece verificare se ci sono usi validi dei modelli e per Service Locator ci sono diversi casi d'uso. Ma cominciamo guardando gli esempi che hai dato.

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

L'incubo della manutenzione con quella classe è che le dipendenze sono nascoste. Se crei e usi quella classe:

var myType = new MyType();
myType.MyMethod();

Non si capisce che ha dipendenze se sono nascoste utilizzando la posizione del servizio. Ora, se invece utilizziamo l'iniezione di dipendenza:

public class MyType
{
    public MyType(IDep1 dep1, IDep2 dep2)
    {
    }

    public void MyMethod()
    {
        dep1.DoSomething();

        // new dependency
        dep2.DoSomething();
    }
}

È possibile individuare direttamente le dipendenze e non è possibile utilizzare le classi prima di soddisfarle.

In una tipica linea di applicazione aziendale è necessario evitare l'uso dell'ubicazione del servizio proprio per questo motivo. Dovrebbe essere il modello da usare quando non ci sono altre opzioni.

Il modello è un anti-modello?

No.

Ad esempio, l'inversione dei contenitori di controllo non funzionerebbe senza l'ubicazione del servizio. È come risolvono i servizi internamente.

Ma un esempio molto migliore è ASP.NET MVC e WebApi. Cosa pensi rende possibile l'iniezione di dipendenza nei controller? Proprio così - posizione del servizio.

Le tue domande

Ma aspetta un secondo, se usassimo l'approccio DI, introdurremmo una dipendenza con un altro parametro nel costruttore (in caso di iniezione del costruttore). E il problema sarà ancora lì.

Ci sono altri due problemi seri:

  1. Con l'ubicazione del servizio si aggiunge anche un'altra dipendenza: l'individuazione del servizio.
  2. Come si fa a sapere quale durata dovrebbero avere le dipendenze e come / quando devono essere ripulite?

Con l'iniezione del costruttore che utilizza un contenitore, lo si ottiene gratuitamente.

Se potremmo dimenticare di installare ServiceLocator, potremmo dimenticare di aggiungere un nuovo mapping nel nostro contenitore IoC e l'approccio DI avrebbe lo stesso problema di runtime.

È vero. Ma con l'iniezione del costruttore non è necessario scansionare l'intera classe per capire quali dipendenze mancano.

E alcuni contenitori migliori convalidano anche tutte le dipendenze all'avvio (analizzando tutti i costruttori). Quindi con quei contenitori si ottiene direttamente l'errore di runtime e non in un momento temporale successivo.

Inoltre, l'autore ha menzionato le difficoltà del test unitario. Ma non avremo problemi con l'approccio DI?

No. Dato che non si ha una dipendenza da un localizzatore di servizi statici. Hai provato a far funzionare test paralleli con dipendenze statiche? Non è divertente.


2
Jgauffin, grazie per la tua risposta. Hai indicato una cosa importante sul controllo automatico all'avvio. Non ci ho pensato e ora vedo un altro vantaggio di DI. Hai anche fornito un esempio: "var myType = new MyType ()". Ma non posso considerarlo valido perché non istanziamo mai dipendenze in un'app reale (IoC Container lo fa sempre per noi). Vale a dire: nell'app MVC abbiamo un controller, che dipende da IMyService e MyServiceImpl dipende da IMyRepository. Non istanzeremo mai MyRepository e MyService. Otteniamo istanze dai parametri Ctor (come da ServiceLocator) e le usiamo. No?
davidoff,

33
L'unico argomento per cui Service Locator non è un anti-pattern è: "l'inversione dei contenitori di controllo non funzionerebbe senza l'ubicazione del servizio". Questo argomento tuttavia non è valido, poiché Service Locator riguarda le intenzioni e non la meccanica, come spiegato chiaramente qui da Mark Seemann: "Un contenitore DI incapsulato in un Root di composizione non è un Service Locator - è un componente di infrastruttura".
Steven

4
L'API Web @jgauffin non utilizza la posizione del servizio per DI nei controller. Non fa affatto DI. Quello che fa è: ti dà la possibilità di creare il tuo ControllerActivator per passare ai Servizi di configurazione. Da lì, puoi creare una radice di composizione, che sia Pure DI o Containers. Inoltre, stai mescolando l'uso di Posizione di servizio, come componente del tuo modello, con la definizione di "Modello di localizzazione del servizio". Con tale definizione, la composizione radice DI potrebbe essere considerata un "modello di localizzazione di servizio". Quindi l'intero punto di tale definizione è discutibile.
Suamere,

1
Voglio sottolineare che questa è una risposta generalmente buona, ho appena commentato un punto fuorviante che hai sollevato.
Suamere,

2
@jgauffin DI e SL sono entrambe le versioni della versione di IoC. SL è solo il modo sbagliato di farlo. Un contenitore IoC potrebbe essere SL o potrebbe utilizzare DI. Dipende da come è cablato. Ma SL è cattiva, cattiva. È un modo per nascondere il fatto che stai accoppiando strettamente tutto insieme.
MirroredFate

37

Vorrei anche sottolineare che SE stai rifattorizzando il codice legacy che il modello di Service Locator non è solo non un modello, ma è anche una necessità pratica. Nessuno potrà mai agitare una bacchetta magica su milioni di righe di codice e improvvisamente tutto quel codice sarà pronto per la DI. Quindi, se si desidera iniziare a introdurre DI in una base di codice esistente, spesso accade che cambierete le cose per diventare lentamente servizi DI, e il codice che fa riferimento a questi servizi NON sarà spesso DI servizi. Pertanto, tali servizi dovranno utilizzare il Service Locator per ottenere istanze di quei servizi che sono stati convertiti per utilizzare DI.

Pertanto, quando si esegue il refactoring di applicazioni legacy di grandi dimensioni per iniziare a utilizzare i concetti DI, direi che non solo Service Locator NON è un anti-pattern, ma che è l'unico modo per applicare gradualmente concetti DI alla base di codice.


12
Quando hai a che fare con il codice legacy, tutto è giustificato per farti uscire da quel casino, anche se ciò significa fare passi intermedi (e imperfetti). Il Service Locator è tale passaggio intermedio. Ti permette di uscire da quell'inferno un passo alla volta, purché ti ricordi che esiste una buona soluzione alternativa che è documentata, ripetibile e dimostrata efficace . Questa soluzione alternativa è l'iniezione di dipendenza ed è proprio per questo che il Service Locator è ancora un anti-schema; un software progettato correttamente non lo utilizza.
Steven,

RE: "Quando hai a che fare con il codice legacy, tutto è giustificato per farti uscire da quel pasticcio" A volte mi chiedo se ci sia solo un po 'di codice legacy che sia mai esistito, ma perché possiamo giustificare qualsiasi cosa per risolverlo noi in qualche modo non è mai riuscito a farlo.
Ha disegnato Delano l'

8

Dal punto di vista del test, Service Locator è errato. Vedi la bella spiegazione di Google Tech Talk di Misko Hevery con esempi di codice http://youtu.be/RlfLCWKxHJ0 a partire dal minuto 8:45. Mi è piaciuta la sua analogia: se hai bisogno di $ 25, chiedi direttamente denaro invece di dare il tuo portafoglio da dove verranno prelevati i soldi. Confronta inoltre Service Locator con un pagliaio che ha l'ago di cui hai bisogno e sa come recuperarlo. Le classi che utilizzano Service Locator sono difficili da riutilizzare a causa di ciò.


10
Questa è un'opinione riciclata e sarebbe stata migliore come commento. Inoltre, penso che la tua (sua?) Analogia serva a dimostrare che alcuni schemi sono più appropriati per alcuni problemi rispetto ad altri.
8bitjunkie,

6

Problema di manutenzione (che mi confonde)

Ci sono 2 diversi motivi per cui l'uso del localizzatore di servizi è negativo in questo senso.

  1. Nel tuo esempio, stai codificando un riferimento statico al localizzatore di servizi nella tua classe. Questo accoppia strettamente la tua classe direttamente al localizzatore di servizio, il che a sua volta significa che non funzionerà senza il localizzatore di servizio . Inoltre, anche i test unitari (e chiunque altro utilizzi la classe) dipendono implicitamente dal localizzatore di servizio. Una cosa che è sembrata passare inosservata qui è che quando si utilizza l'iniezione del costruttore non è necessario un contenitore DI durante i test delle unità , il che semplifica notevolmente i test delle unità (e la capacità degli sviluppatori di comprenderli). Questo è il vantaggio del test unitario realizzato che si ottiene dall'uso dell'iniezione del costruttore.
  2. Per quanto riguarda il motivo per cui il costruttore Intellisense è importante, le persone qui sembrano aver perso del tutto il punto. Una classe viene scritta una volta, ma può essere utilizzata in diverse applicazioni (ovvero diverse configurazioni DI) . Nel tempo, paga dividendi se puoi guardare la definizione del costruttore per capire le dipendenze di una classe, piuttosto che guardare la documentazione (si spera aggiornata) o, in mancanza di ciò, tornare al codice sorgente originale (che potrebbe non sii utile) per determinare quali sono le dipendenze di una classe. La classe con l'individuazione del servizio è generalmente più facile da scrivere , ma tu paghi più che altro il costo di questa convenienza nella manutenzione continua del progetto.

Chiaro e semplice: una classe con un servizio di localizzazione è più difficile da riutilizzare di una che accetta le sue dipendenze attraverso il suo costruttore.

Considera il caso in cui devi utilizzare un servizio LibraryAche il suo autore ha deciso di utilizzare ServiceLocatorAe un servizio del LibraryBcui autore ha deciso di utilizzare ServiceLocatorB. Non abbiamo altra scelta che utilizzare 2 diversi localizzatori di servizi nel nostro progetto. Quante dipendenze devono essere configurate è un gioco d'ipotesi se non abbiamo una buona documentazione, codice sorgente o l'autore sulla composizione rapida. In mancanza di queste opzioni, potrebbe essere necessario utilizzare solo un decompilatoreper capire quali sono le dipendenze. Potrebbe essere necessario configurare 2 API di localizzazione dei servizi completamente diverse e, a seconda del progetto, potrebbe non essere possibile avvolgere semplicemente il contenitore DI esistente. Potrebbe non essere affatto possibile condividere un'istanza di una dipendenza tra le due librerie. La complessità del progetto potrebbe anche essere ulteriormente aggravata se i localizzatori di servizi non risiedono effettivamente nelle stesse librerie dei servizi di cui abbiamo bisogno: stiamo implicitamente trascinando riferimenti di libreria aggiuntivi nel nostro progetto.

Consideriamo ora gli stessi due servizi realizzati con l'iniezione del costruttore. Aggiungi un riferimento a LibraryA. Aggiungi un riferimento a LibraryB. Fornire le dipendenze nella configurazione DI (analizzando ciò che è necessario tramite Intellisense). Fatto.

Mark Seemann ha una risposta StackOverflow che illustra chiaramente questo vantaggio in forma grafica , che si applica non solo quando si utilizza un localizzatore di servizi da un'altra libreria, ma anche quando si utilizzano impostazioni predefinite esterne nei servizi.


1

La mia conoscenza non è abbastanza buona per giudicare questo, ma in generale, penso che se qualcosa ha un uso in una situazione particolare, ciò non significa necessariamente che non possa essere un anti-schema. Soprattutto, quando hai a che fare con librerie di terze parti, non hai il pieno controllo su tutti gli aspetti e potresti finire per usare la soluzione non molto migliore.

Ecco un paragrafo dal Codice adattivo via C # :

"Sfortunatamente, il localizzatore di servizi è talvolta un inevitabile anti-pattern. In alcuni tipi di applicazioni - in particolare Windows Workflow Foundation - l'infrastruttura non si presta all'iniezione del costruttore. In questi casi, l'unica alternativa è usare un localizzatore di servizi. meglio che non iniettare dipendenze. Per tutto il mio vetriolo contro il modello (anti), è infinitamente meglio che costruire dipendenze manualmente. Dopotutto, abilita ancora quei punti di estensione importantissimi forniti da interfacce che consentono decoratori, adattatori, e benefici simili ".

- Hall, Gary McLean. Codice adattivo tramite C #: codifica agile con modelli di progettazione e principi SOLID (riferimento per gli sviluppatori) (p. 309). Pearson Education.


0

L'autore ritiene che "il compilatore non ti aiuterà" - ed è vero. Quando degni una classe, dovrai scegliere attentamente la sua interfaccia, tra gli altri obiettivi per renderla indipendente come ... come ha senso.

Avendo il client accettare il riferimento a un servizio (a una dipendenza) attraverso un'interfaccia esplicita, tu

  • ottenere implicitamente il controllo, quindi il compilatore "aiuta".
  • Stai anche eliminando la necessità per il client di sapere qualcosa sul "Locator" o meccanismi simili, quindi il client è in realtà più indipendente.

Hai ragione sul fatto che DI ha i suoi problemi / svantaggi, ma i vantaggi citati li superano di gran lunga ... IMO. Hai ragione, con DI c'è una dipendenza introdotta nell'interfaccia (costruttore) - ma si spera che sia proprio la dipendenza di cui hai bisogno e che vuoi rendere visibile e verificabile.


Zrin, grazie per i tuoi pensieri. A quanto ho capito, con un approccio DI "adeguato" non dovrei istanziare le mie dipendenze ovunque tranne che nei test unitari. Quindi, il compilatore mi aiuterà solo con i miei test. Ma come ho descritto nella mia domanda originale, questo "aiuto" con i test rotti non mi dà nulla. Vero?
davidoff,

L'argomento "informazione statica" / "verifica della compilazione" è un uomo di paglia. Come ha sottolineato @davidoff, DI è altrettanto suscettibile agli errori di runtime. Vorrei anche aggiungere che i moderni IDE forniscono visualizzazioni descrittive dei commenti / informazioni di riepilogo per i membri e anche in quelli che non lo fanno, qualcuno continuerà a guardare la documentazione fino a quando non "conosceranno" l'API. La documentazione è documentazione, sia che si tratti di un parametro costruttore necessario o di un'osservazione su come configurare una dipendenza.
tuespetre,

Pensare all'implementazione / codifica e alla garanzia della qualità - la leggibilità del codice è cruciale - specialmente per la definizione dell'interfaccia. Se puoi fare a meno dei controlli automatici del tempo di compilazione e se commenti / documenti adeguatamente la tua interfaccia, suppongo che tu possa almeno parzialmente rimediare agli svantaggi della dipendenza nascosta da una sorta di variabile globale con contenuti non facilmente visibili / prevedibili. Direi che hai bisogno di buoni motivi per usare questo schema che supera questi svantaggi.
Zrin,

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.