In questo modo sto scrivendo questo codice è verificabile, ma mi manca qualcosa che non va?


13

Ho un'interfaccia chiamata IContext. A tal fine, non importa cosa fa, tranne quanto segue:

T GetService<T>();

Quello che fa questo metodo è guardare l'attuale contenitore DI dell'applicazione e tenta di risolvere la dipendenza. Penso che sia abbastanza standard.

Nella mia applicazione ASP.NET MVC, il mio costruttore ha questo aspetto.

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetService<ISomeService>();
    AnotherService = ctx.GetService<IAnotherService>();
}

Quindi, piuttosto che aggiungere più parametri nel costruttore per ogni servizio (perché questo diventerà davvero fastidioso e dispendioso in termini di tempo per gli sviluppatori che estendono l'applicazione) Sto usando questo metodo per ottenere servizi.

Ora, si sente male . Tuttavia, il modo in cui attualmente lo giustifico nella mia testa è questo: posso deriderlo .

Io posso. Non sarebbe difficile prendere in giro IContextper testare il controller. Dovrei comunque:

public class MyMockContext : IContext
{
    public T GetService<T>()
    {
        if (typeof(T) == typeof(ISomeService))
        {
            // return another mock, or concrete etc etc
        }

        // etc etc
    }
}

Ma come ho detto, sembra sbagliato. Eventuali pensieri / abusi sono benvenuti.


8
Questo si chiama Service Locator e non mi piace. Ci sono stati molti scritti sull'argomento - vedi martinfowler.com/articles/injection.html e blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern per iniziare.
Benjamin Hodgson, l'

Dall'articolo di Martin Fowler: "Ho spesso sentito la lamentela che questo tipo di localizzatori di servizi sono una brutta cosa perché non sono testabili perché non puoi sostituirli con le implementazioni. Certamente puoi progettarli male per entrare in questo tipo di problema, ma non è necessario. In questo caso l'istanza del localizzatore di servizi è solo un semplice supporto di dati. Posso facilmente creare il localizzatore con implementazioni di test dei miei servizi. " Potresti spiegare perché non ti piace? Forse in una risposta?
LiverpoolsNumber9

8
Ha ragione, questo è cattivo design. E 'facile: public SomeClass(Context c). Questo codice è abbastanza chiaro, no? Si afferma, that SomeClassdipende da a Context. Err, ma aspetta, non lo fa! Si basa solo sulla dipendenza Xche ottiene dal contesto. Ciò significa che ogni volta che apporti una modifica Context, potrebbe rompersi SomeObject, anche se hai cambiato solo Contexts Y. Ma sì, si sa che avete solo cambiato Y, non X, quindi SomeClassva bene. Ma scrivere un buon codice non riguarda ciò che sai, ma ciò che il nuovo dipendente sa quando guarda il tuo codice per la prima volta.
valenterry,

@DocBrown Per me è esattamente quello che ho detto: non vedo la differenza qui. Puoi per favore spiegare ulteriormente?
valenterry,

1
@DocBrown Vedo il tuo punto ora. Sì, se il suo contesto è solo un insieme di tutte le dipendenze, questo non è un cattivo design. Potrebbe essere comunque una cattiva denominazione, ma anche questo è solo un presupposto. OP dovrebbe chiarire se ci sono più metodi (oggetti interni) del contesto. Inoltre, discutere del codice va bene, ma questo è programmers.stackexchange, quindi per me dovremmo anche provare a vedere "dietro" le cose per migliorare l'OP.
valenterry,

Risposte:


5

Avere uno invece di molti parametri nel costruttore non è la parte problematica di questo progetto . Finché la tua IContextclasse non è altro che una facciata di servizio , in particolare per fornire le dipendenze utilizzate MyControllerBasee non un localizzatore di servizi generale utilizzato in tutto il tuo codice, quella parte del tuo codice è IMHO ok.

Il tuo primo esempio potrebbe essere cambiato in

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetSomeService();
    AnotherService = ctx.GetAnotherService();
}

quello non sarebbe un cambiamento sostanziale di progettazione di MyControllerBase. Se questo disegno è buono o cattivo dipende solo dal fatto se lo desideri

  • assicurati TheContext, SomeServicee AnotherServicesono sempre tutti inizializzati con oggetti finti, o tutti con oggetti reali
  • oppure, per consentire l'inizializzazione con diverse combinazioni dei 3 oggetti (il che significa che in questo caso è necessario passare i parametri singolarmente)

Pertanto, l'utilizzo di un solo parametro anziché 3 nel costruttore può essere del tutto ragionevole.

La cosa problematica è IContextesporre il GetServicemetodo in pubblico. IMHO dovresti evitarlo, invece mantieni espliciti i "metodi di fabbrica". Quindi sarà ok implementare i metodi GetSomeServicee GetAnotherServicedal mio esempio usando un localizzatore di servizi? IMHO che dipende. Fino a quando la IContextclasse continua a diventare una semplice fabbrica astratta allo scopo specifico di fornire un elenco esplicito di oggetti di servizio, questo è accettabile per IMHO. Le fabbriche astratte sono in genere solo codice "colla", che non deve essere testato da solo. Tuttavia, dovresti chiederti se nel contesto di metodi come GetSomeService, se hai davvero bisogno di un localizzatore di servizi, o se una chiamata esplicita del costruttore non sarebbe più semplice.

Quindi fai attenzione, quando ti attieni a un progetto in cui l' IContextimplementazione è solo un involucro attorno a un GetServicemetodo pubblico e generico , che consente di risolvere eventuali dipendenze arbitrarie da classi arbitrarie, allora tutto applica ciò che @BenjaminHodgson ha scritto nella sua risposta.


Sono d'accordo con questo. Il problema con l'esempio è il GetServicemetodo generico . Il refactoring a metodi esplicitamente denominati e digitati è meglio. Meglio ancora sarebbe precisare esplicitamente le dipendenze IContextdell'implementazione nel costruttore.
Benjamin Hodgson, l'

1
@BenjaminHodgson: a volte può essere migliore, ma non è sempre migliore. Si nota l'odore del codice quando l'elenco dei parametri ctor diventa sempre più lungo. Vedi la mia precedente risposta qui: programmers.stackexchange.com/questions/190120/…
Doc Brown

@DocBrown "odore di codice" di over-injection del costruttore è indicativo di una violazione dell'SRP, che è il vero problema. Il semplice raggruppamento di diversi servizi in una classe Facade, solo per esporli come proprietà, non fa nulla per risolvere l'origine del problema. Quindi una facciata non dovrebbe essere un semplice wrapper attorno ad altri componenti, ma dovrebbe idealmente offrire un'API semplificata con cui consumarli (o dovrebbe semplificarli in qualche altro modo) ...
AlexFoxGill

... Ricorda che un "odore di codice" come l'iniezione eccessiva del costruttore non è un problema in sé. È solo un suggerimento che c'è un problema più profondo nel codice che di solito può essere risolto rifattorizzando sensibilmente una volta che il problema è stato identificato
AlexFoxGill

Dov'eri quando Microsoft l'ha sfornato IValidatableObject?
RubberDuck,

15

Questo design è noto come Service Locator * e non mi piace. Ci sono molti argomenti contrari:

Service Locator ti accoppia al tuo container . Usando l'iniezione regolare di dipendenze (in cui il costruttore spiega esplicitamente le dipendenze) puoi facilmente sostituire il tuo contenitore con uno diverso o tornare a new-espressioni. Con il tuo IContextnon è davvero possibile.

Service Locator nasconde le dipendenze . Come cliente, è molto difficile dire di cosa hai bisogno per costruire un'istanza di una classe. Hai bisogno di una sorta di IContext, ma devi anche impostare il contesto per restituire gli oggetti corretti al fine di far MyControllerBasefunzionare il lavoro. Questo non è affatto ovvio dalla firma del costruttore. Con un normale DI il compilatore ti dice esattamente di cosa hai bisogno. Se la tua classe ha molte dipendenze, dovresti provare quel dolore perché ti stimolerà a fare il refactoring. Service Locator nasconde i problemi con progetti errati.

Service Locator causa errori di runtime . Se chiami GetServicecon un parametro di tipo errato otterrai un'eccezione. In altre parole, la vostra GetServicefunzione non è una funzione totale. (Le funzioni totali sono un'idea del mondo FP, ma fondamentalmente significa che le funzioni dovrebbero sempre restituire un valore.) Meglio lasciare che il compilatore ti aiuti e ti dica quando hai sbagliato le dipendenze.

Service Locator viola il principio di sostituzione di Liskov . Poiché il suo comportamento varia in base all'argomento del tipo, Service Locator può essere visualizzato come se avesse effettivamente un numero infinito di metodi sull'interfaccia! Questo argomento è spiegato in dettaglio qui .

Service Locator è difficile da testare . Hai fornito un esempio di un falso IContextper i test, il che va bene, ma sicuramente è meglio non dover scrivere quel codice in primo luogo. Basta iniettare direttamente le dipendenze false, senza passare dal localizzatore di servizi.

In breve, non farlo . Sembra una soluzione seducente al problema delle classi con molte dipendenze, ma alla lunga renderai la tua vita miserabile.

* Sto definendo Service Locator come un oggetto con un Resolve<T>metodo generico che è in grado di risolvere dipendenze arbitrarie e viene utilizzato in tutto il codebase (non solo nel Root di composizione). Questo non è lo stesso di Service Facade (un oggetto che raggruppa un piccolo insieme noto di dipendenze) o Abstract Factory (un oggetto che crea istanze di un singolo tipo: il tipo di una Fabbrica astratta può essere generico ma il metodo non lo è) .


1
Ti stai lamentando del modello di Service Locator (che accetto). Ma in realtà, nell'esempio del PO, MyControllerBasenon è accoppiato a un contenitore DI specifico, né questo "realmente" è un esempio per l'anti-pattern di Service Locator.
Doc Brown,

@DocBrown Sono d'accordo. Non perché mi semplifichi la vita, ma perché la maggior parte degli esempi sopra riportati non sono rilevanti per il mio codice.
LiverpoolsNumber9

2
Per me, il segno distintivo dell'anti-pattern di Service Locator è il GetService<T>metodo generico . Risolvere dipendenze arbitrarie è il vero odore, che è presente e corretto nell'esempio del PO.
Benjamin Hodgson, l'

1
Un altro problema con l'utilizzo di un Service Locator è che riduce la flessibilità: è possibile avere solo un'implementazione di ciascuna interfaccia di servizio. Se crei due classi che si basano su un IFrobnicator, ma in seguito decidi che uno dovrebbe usare l'implementazione DefaultFrobnicator originale, ma l'altro dovrebbe davvero usare un decoratore CacheingFrobnicator attorno ad esso, devi cambiare il codice esistente, mentre se sei iniettando dipendenze direttamente tutto quello che devi fare è cambiare il tuo codice di installazione (o configurare il file se stai usando un framework DI). Quindi questa è una violazione OCP.
Jules,

1
@DocBrown Il GetService<T>()metodo consente di richiedere classi arbitrarie: "Quello che fa questo metodo è guardare l'attuale contenitore DI dell'applicazione e tenta di risolvere la dipendenza. Penso che sia abbastanza standard". . Stavo rispondendo al tuo commento all'inizio di questa risposta. Questo è al 100% un Service Locator
AlexFoxGill,

5

I migliori argomenti contro l'anti-pattern di Service Locator sono chiaramente indicati da Mark Seemann, quindi non entrerò troppo nel perché questa è una cattiva idea - è un viaggio di apprendimento che devi prendere il tempo di capire da solo (I consiglio anche il libro di Mark ).

OK, quindi per rispondere alla domanda - riaffermiamo il tuo vero problema :

Quindi, piuttosto che aggiungere più parametri nel costruttore per ogni servizio (perché questo diventerà davvero fastidioso e dispendioso in termini di tempo per gli sviluppatori che estendono l'applicazione) Sto usando questo metodo per ottenere servizi.

C'è una domanda che risolve questo problema su StackOverflow . Come menzionato in uno dei commenti lì:

La migliore osservazione: "Uno dei meravigliosi benefici dell'iniezione del costruttore è che rende palesemente evidenti le violazioni del principio della responsabilità singola".

Stai cercando nel posto sbagliato la soluzione al tuo problema. È importante sapere quando una lezione sta facendo troppo. Nel tuo caso sospetto fortemente che non sia necessario un "controller di base". In effetti, in OOP non c'è quasi sempre bisogno di eredità . Le variazioni nel comportamento e la funzionalità condivisa possono essere ottenute interamente attraverso un uso appropriato delle interfacce, che di solito si traduce in un migliore codice fattorizzato e incapsulato - e non è necessario passare dipendenze ai costruttori di superclassi.

In tutti i progetti su cui ho lavorato dove è presente un controller di base, è stato fatto esclusivamente allo scopo di condividere proprietà e metodi convenienti, come IsUserLoggedIn()e GetCurrentUserId(). STOP . Questo è un orribile abuso dell'eredità. Invece, crea un componente che esponga questi metodi e prendi una dipendenza da esso dove ne hai bisogno. In questo modo, i componenti rimarranno testabili e le loro dipendenze saranno evidenti.

A parte qualsiasi altra cosa, quando uso il modello MVC consiglierei sempre i controller skinny . Puoi leggere di più su questo qui, ma l'essenza del modello è semplice, che i controller in MVC dovrebbero fare solo una cosa: gestire gli argomenti passati dal framework MVC, delegando altre preoccupazioni ad altri componenti. Questo è di nuovo il principio della responsabilità singola al lavoro.

Sarebbe davvero utile conoscere il tuo caso d'uso per dare un giudizio più accurato, ma onestamente non riesco a pensare a nessuno scenario in cui una classe base è preferibile a dipendenze ben ponderate.


+1 - questa è una nuova interpretazione della domanda che nessuna delle altre risposte ha realmente affrontato
Benjamin Hodgson

1
"Uno dei meravigliosi benefici dell'iniezione del costruttore è che rende palesemente evidenti le violazioni del Principio della singola responsabilità" Lo adoro. Davvero una buona risposta. Non sono d'accordo con tutto questo. Il mio caso d'uso è, stranamente, non dover duplicare il codice in un sistema che avrà oltre 100 controller (almeno). Comunque riguardo a SRP - ogni servizio iniettato ha una sola responsabilità, così come il controllore che li usa tutti - come sarebbe un rifrattore che ??!
LiverpoolsNumber9

1
@ LiverpoolsNumber9 La chiave è spostare la funzionalità da BaseController in dipendenze, fino a quando non rimarrà nulla in BaseController tranne protectedle delegazioni di un liner ai nuovi componenti. Quindi puoi perdere BaseController e sostituire quei protectedmetodi con chiamate dirette alle dipendenze - questo semplificherà il tuo modello e renderà esplicite tutte le tue dipendenze (il che è una cosa molto positiva in un progetto con centinaia di controller!)
AlexFoxGill

@ LiverpoolsNumber9 - se potessi copiare una parte del tuo BaseController su un pastebin potrei dare alcuni suggerimenti concreti
AlexFoxGill

-2

Sto aggiungendo una risposta a questo sulla base dei contributi di tutti gli altri. Mille grazie a tutti. Prima di tutto ecco la mia risposta: "No, non c'è niente di sbagliato in questo".

La risposta "Service Facade" di Doc Brown Ho accettato questa risposta perché quello che stavo cercando (se la risposta era "no") era qualche esempio o qualche espansione su quello che stavo facendo. Ha fornito questo suggerendo che A) ha un nome e B) ci sono probabilmente modi migliori per farlo.

Risposta di "Service Locator" di Benjamin Hodgson Per quanto apprezzo le conoscenze acquisite qui, quello che ho non è un "Service Locator". È una "facciata di servizio". Tutto in questa risposta è corretto ma non per le mie circostanze.

Risposte dell'USR

Lo affronterò più in dettaglio:

Stai cedendo molte informazioni statiche in questo modo. Stai rinviando le decisioni al runtime come fanno molti linguaggi dinamici. In questo modo perdi la verifica statica (sicurezza), la documentazione e il supporto degli strumenti (completamento automatico, refactoring, ricerca di usi, flusso di dati).

Non perdo alcun attrezzo e non perdo nessuna digitazione "statica". La facciata del servizio restituirà ciò che ho configurato nel mio contenitore DI, oppure default(T). E ciò che restituisce è "digitato". Il riflesso è incapsulato.

Non vedo perché l'aggiunta di servizi aggiuntivi come argomenti del costruttore sia un grosso onere.

Non è certamente "raro". Dato che sto usando un controller di base , ogni volta che devo cambiare il suo costruttore, potrei dover cambiare altri 10, 100, 1000 controller.

Se si utilizza un framework di iniezione delle dipendenze, non sarà nemmeno necessario passare manualmente i valori dei parametri. Quindi perdi alcuni vantaggi statici ma non così tanti.

Sto usando l'iniezione di dipendenza. Questo è il punto.

E infine, il commento di Jules sulla risposta di Benjamin non sto perdendo alcuna flessibilità. È la mia facciata di servizio. Posso aggiungere tutti i parametri a GetService<T>cui voglio distinguere tra diverse implementazioni, proprio come si farebbe quando si configura un contenitore DI. Quindi, ad esempio, potrei cambiare GetService<T>()per GetService<T>(string extraInfo = null)aggirare questo "potenziale problema".


Comunque grazie ancora a tutti. È stato davvero utile. Saluti.


4
Non sono d'accordo sul fatto che tu abbia una facciata di servizio qui. Il GetService<T>()metodo è in grado di (tentare di) risolvere dipendenze arbitrarie . Questo lo rende un Service Locator, non una Service Facade, come ho spiegato nella nota a piè di pagina della mia risposta. Se dovessi sostituirlo con un piccolo set di GetServiceX()/ GetServiceY()metodi, come suggerisce @DocBrown, sarebbe una facciata.
Benjamin Hodgson,

2
Dovresti leggere e prestare molta attenzione all'ultima sezione di questo articolo sul Localizzatore di servizi astratti , che è essenzialmente quello che stai facendo. Ho visto questo anti-pattern corrompere un intero progetto - presta particolare attenzione alla citazione "peggiorerà la tua vita come sviluppatore di manutenzione perché dovrai usare notevoli quantità di potenza del cervello per cogliere le implicazioni di ogni cambiamento che fai "
AlexFoxGill

3
Sulla digitazione statica - ti manca il punto. Se provo a scrivere codice che non fornisce una classe usando DI con un parametro di costruzione di cui ha bisogno, non verrà compilato. Questa è sicurezza statica in fase di compilazione contro i problemi. Se scrivi il tuo codice, fornendo al costruttore un IContext che non è stato configurato correttamente per fornire l'argomento di cui ha effettivamente bisogno, allora si compilerà e fallirà in fase di esecuzione
Ben Aaronson,

3
@ LiverpoolsNumber9 Bene, allora perché avere un linguaggio fortemente tipizzato? Gli errori del compilatore sono una prima linea di difesa contro i bug. I test unitari sono il secondo. I tuoi non verrebbero rilevati dai test unitari nemmeno perché si tratta di un'interazione tra una classe e la sua dipendenza, quindi saresti nella terza linea di difesa: i test di integrazione. Non so quanto spesso li esegui, ma ora stai parlando di feedback sull'ordine dei minuti o più, piuttosto che sui millisecondi necessari al tuo IDE per sottolineare un errore del compilatore.
Ben Aaronson,

2
@ LiverpoolsNumber9 sbagliato: i tuoi test ora devono popolare IContexte iniettare quello, e soprattutto: se aggiungi una nuova dipendenza, il codice verrà comunque compilato - anche se i test falliranno in fase di esecuzione
AlexFoxGill
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.