ServiceLocator è un anti-pattern?

Recentemente ho letto l’articolo di Mark Seemann sull’antipattern di Service Locator.

L’autore sottolinea due motivi principali per cui ServiceLocator è un anti-pattern:

  1. Problema di utilizzo dell’API (che sto perfettamente bene con)
    Quando la class utilizza un Localizzatore di servizi è molto difficile vedere le sue dipendenze poiché, nella maggior parte dei casi, la class ha un solo costruttore PARAMETERLESS. In contrasto con ServiceLocator, l’approccio DI espone esplicitamente le dipendenze tramite i parametri del costruttore, quindi le intese sono facilmente visibili in IntelliSense.

  2. Problema di manutenzione (che mi imbarazza)
    Considera il seguente esempio

Abbiamo un ‘MyType’ di class che impiega un approccio localizzatore di servizi:

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

Ora vogliamo aggiungere un’altra dipendenza alla class ‘MyType’

 public class MyType { public void MyMethod() { var dep1 = Locator.Resolve(); dep1.DoSomething(); // new dependency var dep2 = Locator.Resolve(); dep2.DoSomething(); } } 

E qui è dove inizia il mio fraintendimento. L’autore dice:

Diventa molto più difficile dire se stai introducendo o meno un cambiamento di rottura. È necessario comprendere l’intera applicazione in cui viene utilizzato il localizzatore di servizi e il compilatore non ti aiuterà.

Ma aspetta un secondo, se usassimo l’approccio DI, introdurremo una dipendenza con un altro parametro nel costruttore (nel caso di un’iniezione del costruttore). E il problema sarà ancora lì. Se potessimo dimenticare di installare ServiceLocator, potremmo dimenticare di aggiungere una nuova mapping nel nostro contenitore IoC e l’approccio DI avrebbe lo stesso problema di run-time.

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 hanno istanziato quella class? Li aggiorneremo per passare una nuova dipendenza falsa solo per rendere il nostro test compilabile. E non vedo alcun beneficio da questo aggiornamento e dalla spesa temporale.

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

AGGIORNAMENTO (SOMMARIO):

La risposta alla mia domanda “Il localizzatore di servizio è un anti-modello” dipende davvero dalle circostanze. E sicuramente non consiglierei di cancellarlo dal tuo elenco di strumenti. Potrebbe diventare molto utile quando inizi a gestire il codice legacy. Se sei abbastanza fortunato da essere all’inizio del tuo progetto, allora 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:

  • Il più ovvio e importante: Service Locator nasconde le dipendenze della class
  • Se si utilizza un contenitore IoC, è probabile che esegua la scansione di tutto il costruttore all’avvio per convalidare tutte le dipendenze e fornire un feedback immediato sui mapping mancanti (o configurazione errata); questo non è ansible se stai usando il tuo contenitore IoC come localizzatore di servizi

Per i dettagli, leggi le risposte eccellenti fornite di seguito.

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

Invece dobbiamo guardare se ci sono usi validi dei modelli, e per Service Locator ci sono diversi casi d’uso. Ma iniziamo guardando gli esempi che hai fornito.

 public class MyType { public void MyMethod() { var dep1 = Locator.Resolve(); dep1.DoSomething(); // new dependency var dep2 = Locator.Resolve(); dep2.DoSomething(); } } 

L’incubo di manutenzione con quella class è che le dipendenze sono nascoste. Se crei e utilizzi quella class:

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

Non capisci che ha delle dipendenze se sono nascoste usando il percorso del servizio. Ora, se invece usiamo l’iniezione di dipendenza:

 public class MyType { public MyType(IDep1 dep1, IDep2 dep2) { } public void MyMethod() { dep1.DoSomething(); // new dependency dep2.DoSomething(); } } 

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

In una linea tipica di applicazione aziendale è necessario evitare l’uso del servizio di assistenza per tale ragione. Dovrebbe essere il modello da usare quando non ci sono altre opzioni.

Il pattern è un anti-pattern?

No.

Ad esempio, l’inversione dei contenitori di controllo non funzionerebbe senza la posizione di servizio. È come risolvono i servizi internamente.

Ma un esempio migliore è ASP.NET MVC e WebApi. Cosa ne pensi rende l’iniezione della dipendenza ansible nei controller? Proprio così – posizione di servizio.

Le tue domande

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

Ci sono altri due problemi seri:

  1. Con la posizione del servizio si aggiunge anche un’altra dipendenza: il localizzatore di servizi.
  2. Come si fa a stabilire quale durata debbano avere le dipendenze e come / quando dovrebbero essere ripulite?

Con l’iniezione del costruttore utilizzando un contenitore, lo si ottiene gratuitamente.

Se potessimo dimenticare di installare ServiceLocator, potremmo dimenticare di aggiungere una nuova mapping nel nostro contenitore IoC e l’approccio DI avrebbe lo stesso problema di run-time.

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

Inoltre alcuni contenitori migliori convalidano tutte le dipendenze all’avvio (mediante la scansione di tutti i costruttori). Quindi con questi 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. Come non si ha una dipendenza da un localizzatore di servizio statico. Hai provato a ottenere test paralleli che funzionano con dipendenze statiche? Non è divertente.

Vorrei anche precisare che SE si sta rifattando il codice legacy che il pattern Locator del servizio non solo non è un anti-pattern, ma è anche una necessità pratica. Nessuno mai saluterà una bacchetta magica su milioni di righe di codice e improvvisamente tutto quel codice sarà pronto. Quindi, se vuoi iniziare a introdurre DI su una base di codice esistente, spesso cambierai le cose per diventare lentamente servizi DI e il codice che fa riferimento a questi servizi NON sarà DI. Quindi, questi servizi dovranno utilizzare il Service Locator per ottenere istanze di quei servizi che sono stati convertiti per utilizzare DI.

Quindi, quando refactoring delle applicazioni legacy di grandi dimensioni per iniziare a utilizzare i concetti DI, direi che non solo Localizzatore di servizi NON è un anti-pattern, ma che è l’unico modo per applicare gradualmente i concetti DI al codice base.

Dal punto di vista del test, il Localizzatore di servizio è cattivo. 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 il denaro invece di dare il tuo portafoglio da dove verranno prelevati i soldi. Inoltre confronta Service Locator con un pagliaio che ha l’ago necessario e sa come recuperarlo. Le classi che utilizzano Service Locator sono difficili da riutilizzare per questo motivo.

Problema di manutenzione (che mi imbarazza)

Ci sono 2 diversi motivi per cui l’uso del localizzatore di servizi è negativo a questo riguardo.

  1. Nel tuo esempio, stai codificando a fondo un riferimento statico al localizzatore di servizi nella tua class. Questo collega strettamente la tua class direttamente al localizzatore di servizi, che a sua volta significa che non funzionerà senza il localizzatore di servizi . Inoltre, i test di unità (e chiunque altro usi la class) dipendono implicitamente anche dal localizzatore di servizi. Una cosa che è sembrata passare inosservata è che quando si utilizza l’iniezione del costruttore non è necessario un contenitore DI quando si esegue il test dell’unità , il che semplifica considerevolmente i test dell’unità (e la capacità degli sviluppatori di capirli). Questo è il vantaggio di test unitario ottenuto dall’utilizzo dell’iniezione del costruttore.
  2. Per quanto riguarda il motivo per cui il costruttore Intellisense è importante, la gente qui sembra aver perso completamente il punto. Una class viene scritta una volta, ma può essere utilizzata in diverse applicazioni (ovvero diverse configurazioni DI) . Nel tempo, paga i dividendi se si può guardare la definizione del costruttore per capire le dipendenze di una class, piuttosto che guardare la documentazione (auspicabilmente aggiornata) o, in mancanza, tornare al codice sorgente originale (che potrebbe non essere a portata di mano) per determinare quali sono le dipendenze di una class. La class con il localizzatore di servizi è generalmente più facile da scrivere , ma è più che si paga il costo di questa convenienza nella manutenzione continua del progetto.

Semplice e chiaro: una class con un localizzatore di servizi è più difficile da riutilizzare di una che accetta le sue dipendenze attraverso il suo costruttore.

Considerare il caso in cui è necessario utilizzare un servizio di LibraryA che il suo autore abbia deciso di utilizzare ServiceLocatorA e un servizio di LibraryB cui 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 di ipotesi se non abbiamo una buona documentazione, codice sorgente, o l’autore su composizione rapida. In mancanza di queste opzioni, potrebbe essere necessario utilizzare un decompilatore solo per capire quali sono le dipendenze. Potremmo dover configurare 2 API di localizzazione del servizio completamente diverse e, a seconda del progetto, potrebbe non essere ansible semplicemente racchiudere il contenitore DI esistente. Potrebbe non essere ansible 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 si trovino effettivamente nelle stesse librerie dei servizi di cui abbiamo bisogno – stiamo implicitamente trascinando ulteriori riferimenti bibliografici 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 non si applica solo quando si utilizza un localizzatore di servizi da un’altra libreria, ma anche quando si utilizzano valori predefiniti stranieri nei servizi.

L’autore spiega che “il compilatore non ti aiuterà” ed è vero. Quando si degna una class, si vorrà scegliere con cura la sua interfaccia – tra gli altri obiettivi per renderla indipendente … come ha senso.

Avendo il client accetta il riferimento a un servizio (a una dipendenza) tramite un’interfaccia esplicita, tu

  • ottenere implicitamente un controllo, quindi il compilatore “aiuta”.
  • Stai anche rimuovendo la necessità che il client sappia qualcosa sul “Locator” o meccanismi simili, quindi il cliente è in realtà più indipendente.

Hai ragione che DI ha i suoi problemi / svantaggi, ma i vantaggi menzionati sono di gran lunga superiori a loro … IMO. Hai ragione, che con DI c’è una dipendenza introdotta nell’interfaccia (costruttore) – ma si spera che questa sia la dipendenza di cui hai bisogno e che tu voglia rendere visibile e controllabile.

La mia conoscenza non è abbastanza buona per giudicare questo, ma in generale, penso che se qualcosa ha un uso in una situazione particolare, non significa necessariamente che non può essere un anti-modello. Soprattutto quando si hanno a che fare con librerie di terze parti, non si ha il pieno controllo su tutti gli aspetti e si può finire per utilizzare la soluzione non ottimale.

Ecco un paragrafo tratto dal codice adattivo tramite C # :

“Sfortunatamente, il localizzatore di servizi è a volte un anti-pattern inevitabile: 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 è utilizzare un localizzatore di servizi. meglio che non iniettare dipendenze affatto.Per tutto il mio vetriolo contro il pattern (anti-), è infinitamente migliore rispetto alla costruzione manuale delle dipendenze.Dopo tutto, consente comunque quegli importantissimi punti di estensione 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.