Iniezione di dipendenza ambientale attraverso il localizzatore di servizio statico

5

Dopo alcuni googling ho trovato alcuni dibattiti sul fatto che l'iniezione del costruttore o l'iniezione di proprietà / campo siano migliori, ma c'è ancora un'altra alternativa che mi sembra più vantaggiosa.

Nella maggior parte degli ambienti di programmazione, c'è qualcosa chiamato "contesto di chiamata", che è un'informazione implicitamente passata dal chiamante al chiamato.

A volte questo contesto è semplicemente storage locale dei thread, a volte, nel caso della programmazione asincrona a thread incrociati, è più complesso di così.

In ogni caso, tali contesti memorizzano informazioni come cultura, utente o, per le applicazioni Web, la richiesta corrente.

Questi tipi di informazioni raramente vengono portati in giro esplicitamente, ma sono accessibili tramite metodi globali che accedono a tali contesti.

Nel caso d'uso dell'iniezione di dipendenza, questo significa una forma statica / globale di localizzatore di servizi (il mio esempio è in C #):

public static Sl
{
    public static Dependency Get<Dependency>();

    public static IDisposable Push<Dependency>(Dependency dependency);
}

L'utilizzo sarebbe:

public void SomeFunction()
{
    Sl.Get<Dependency>().UseItSomehow();
}

Per chiamare SomeFunction , la dipendenza può essere impostata o modificata per le calle in questo modo:

using (Sl.Push<SomeDependency>(someImplementation)
{
    SomeFunction();
}

Penso che questo modello sia superiore ad altri tipi di iniezione di dipendenza in quei casi in cui una dipendenza è richiesta in modo uniforme per gran parte di una sottostruttura di chiamata più grande .

Ad esempio, un repository, un logger o un'interfaccia di configurazione sono raramente richiesti solo per una singola funzione, ma anche per tutte le successive chiamate nidificate.

Soprattutto con l'iniezione di contructor questo porta a qualche confusione che può essere completamente evitata: SomeFunction sopra può chiamare altre funzioni che otterranno automaticamente la dipendenza. Può anche cambiare la dipendenza in quei casi in cui è effettivamente necessario.

Voglio affrontare alcune delle preoccupazioni che penso di aver letto da qualche parte, e la mia domanda sarebbe allora se qualcuno potesse pensare agli altri.

  • Esiste una dipendenza su Sl

    Questo mi sembra l'obiezione di un fondamentalista. La maggior parte del software dipende anche da numeri interi, stringhe e liste, senza che tali cose vengano correttamente estratte e trasmesse con il dovuto rispetto alla pura Chiesa dell'iniezione di dipendenza. Chiaramente alcune cose di basso livello su cui è necessario dipendere direttamente - si spera che siano ben progettate e idealmente risiedano nella libreria di runtime del rispettivo linguaggio. In caso contrario, potrebbero essere ancora il male minore.

  • Le dipendenze richieste dovrebbero essere esplicite

    L'unica forma veramente esplicita di DI è l'iniezione del contructor o il passare le dipendenze direttamente nelle chiamate di metodo. Sfortunatamente, questo è anche il modo più verboso e non flessibile di farlo. Le dipendenze aggiunte in seguito cambiano le firme del costruttore o del metodo, richiedendo un refactoring di potenzialmente diversi livelli di chiamate di funzione che non fanno altro che passare gli oggetti letteralmente. Nei linguaggi statici, questo è noioso. In quelli dinamici, è anche una fonte di bug.

    Inoltre, se è richiesta la dipendenza, la rispettiva funzione fallirà ogni volta che verrà usata comunque, rendendo molto probabile che un bug di dipendenza mancante non acceda involontariamente alla produzione.

    Quindi idealmente, sì, le dipendenze richieste dovrebbero essere esplicite. Ma c'è sempre un compromesso, e io sono davvero l'unico a pensare che le persone della costruzione del costruttore non abbiano le loro priorità dritte?

  • Non si dovrebbe fare affidamento sulla magia

    I contesti di chiamata e la statistica del filo non sono magici, sono semplicemente avanzati e forse più tecnici di quelli trattati nel tuo tipico corso di programmazione all'università.

    Inoltre, l'implementazione è nascosta dietro il localizzatore di servizio statico. Gli utenti non devono sapere come funziona per utilizzarlo.

Quindi la mia domanda: ci sono altre obiezioni? Ci sono motivi legittimi, reali, per cui la DI con un localizzatore di servizio statico / globale sarebbe male?

    
posta John 29.04.2016 - 18:49
fonte

3 risposte

5

Are there any other objections? Are there any legitimate, real-world reasons why DI with a static/global service locator would be bad?

Ugh, sì.

Statics / globals sono orribili. Assumono che il runtime della tua applicazione sia omogeneo - tutte le tue istanze richiedono tutto lo stesso tipo di istanze dappertutto. Questo è ingenuo. Interferiscono con la concorrenza, poiché ogni stato in qualsiasi di queste cose è uno stato intrinsecamente condiviso. Interferiscono con i test, dal momento che non è possibile prendere in giro in modo efficace istanze diverse che eseguono contemporaneamente o addirittura istanze diverse nel callstack. E poi ci sono i soliti problemi di debug con lo stato globale.

Nascondere i tuoi problemi non risolve i tuoi problemi. Se hai qualcosa che tutto ha bisogno, allora hai un accoppiamento diffuso. Se hai oggetti che hanno bisogno di un mucchio di dipendenze, allora hai un accoppiamento diffuso. Se hai un'enorme gerarchia di oggetti che rende difficile passare le cose intorno a dove devono andare, allora probabilmente hai un design scarso. DI non aggiusta queste cose! Tutto quello che stai facendo è nasconderlo dietro una magia che passa le dipendenze intorno. Non stai rimuovendo le dipendenze, rendendole meno visibili e un errore di runtime piuttosto che un errore di compilazione.

    
risposta data 29.04.2016 - 19:34
fonte
3

Penso che manchi il beneficio dell'iniezione di dipendenza - che qualsiasi cosa dipenda dalla classe viene data ad essa, piuttosto che presupposta. Quello che stai proponendo è il localizzatore di servizio anti-pattern. Ci sono alcuni casi in cui potrei essere tentato di usarlo (identità), ma preferirei un contratto pulito per una ragione - non so mai come qualcuno userà il mio codice. Ho visto questo sul mio posto di lavoro, dove qualcuno crea codice con riferimenti ambient a concetti web (come HttpContext ) all'interno di una catena di chiamate invece di iniettarlo. Un anno o due dopo, e il codice non può essere riutilizzato in un'applicazione console a causa delle dipendenze statiche. Lo stesso vale per provare a scrivere test unitari.

  • C'è una dipendenza su S1
    • Questa non è un'obiezione fondamentalista, è un'incapacità di comprendere il punto di iniezione della dipendenza (che è diverso dal preferire le interfacce, che credo tu stia confondendo con DI). Stringhe, interi e liste devono essere passati nel costruttore dove sono necessari. Il tuo costruttore dovrebbe sempre esporre il contratto della classe - quali requisiti deve essere chiarito e confermato prima della creazione dell'oggetto. DI non utilizza sempre un contenitore DI.
  • Le dipendenze richieste dovrebbero essere esplicite
    • Vedi la mia risposta sopra a non sapere mai come verrà usato il codice, specialmente in una libreria. Anche se è prolisso, questo è lo scopo - descrivere con precisione il contratto. L'aggiunta di dipendenze non dovrebbe cambiare le firme; nuovi metodi / classi dovrebbero essere introdotti per rendere obsoleti quelli vecchi. Questo è un principio base di versioning semantico ed è un buon modo per fare lo sviluppo di una libreria. Per quanto riguarda le dipendenze implicite, preferisco prendere qualcosa al momento della compilazione piuttosto che ricevere un bug arrabbiato da un utente.
  • Non si dovrebbe fare affidamento sulla magia
    • È magico, nel senso che le tue dipendenze appaiono solo, piuttosto che essere richieste. Questo è problematico quando la dipendenza che si ottiene ha dei vincoli speciali (come HttpContext).

Ecco alcuni link ad altre discussioni sull'anti-pattern del localizzatore server e motivi per evitarlo:

link

link

link

    
risposta data 29.04.2016 - 19:23
fonte
0

Penso che questa sia una grande idea in parte perché anch'io ho avuto esattamente la stessa idea. So che è praticabile almeno perché ho fatto qualcosa di molto simile. Sarei molto interessato a vedere quali altre obiezioni vengono sollevate. Per quanto riguarda quelli finora, non penso che siano sostanziali. Il più interessante è all'interno del link link . In breve si dice che un consumatore di terze parti di

SomeFunction()

potrebbe non riuscire a impostare il localizzatore di servizio in quanto non obbligato a farlo mentre nel caso

SomeFunction(dependency)

il compilatore lo obbligherà a fornire la dipendenza. Tuttavia, penso che dover fornire la dipendenza dal codice di terze parti usando SomeFunction potrebbe essere un problema tanto quanto lo è nel codice della prima parte.

Nel caso di un logger, si avrà il classico problema di reazione a catena di introdurre un parametro in una funzione comunemente utilizzata. Il chiamante della funzione deve trovare quel parametro, causandogli di solito il parametro stesso, e così via, causando un grande ripple sulla struttura delle chiamate. Nel contesto consigliato dell'utilizzo di DI per un logger, la modifica del codice in cui è necessario aggiungere la registrazione ad alcune funzioni fa sì che sia necessario aggiungere l'interfaccia del servizio logger al costruttore di tutte le classi della catena da qualunque parte venga utilizzata la funzione.

Questo è un costo di manutenzione significativo per DI in questo caso d'uso e potenzialmente molto difficile da gestire.

Questo costo non è isolato dalla base di codici in cui è richiesto il logger. Nell'esempio in cui SomeFunction viene utilizzato da una terza parte, sarà anche più efficiente fornire il servizio di registrazione una volta al localizzatore di servizio piuttosto che scriverlo nei costruttori di tutto il codice di terze parti che ne ha bisogno.

In che cosa consiste questo, ha sempre senso che esista un contesto globale? La risposta a questo deve essere sì, in quanto rende gli oggetti dati ampiamente utilizzati a tutti i livelli di codice molto più facili da accedere senza creare un gran numero di parametri per passarli intorno.

    
risposta data 28.05.2016 - 15:12
fonte

Leggi altre domande sui tag