Quando non è appropriato utilizzare il modello di iniezione di dipendenza?


124

Da quando ho imparato (e amato) i test automatizzati, mi sono ritrovato a utilizzare il modello di iniezione di dipendenza in quasi tutti i progetti. È sempre appropriato utilizzare questo modello quando si lavora con i test automatizzati? Ci sono situazioni in cui dovresti evitare di usare l'iniezione di dipendenza?


1
Related (probabilmente non duplicati) - stackoverflow.com/questions/2407540/...
Steve314

3
Sembra un po 'come l'anti-pattern Golden Hammer. en.wikipedia.org/wiki/Anti-pattern
Cuga

Risposte:


123

Fondamentalmente, l'iniezione di dipendenza fa alcune ipotesi (di solito ma non sempre valide) sulla natura dei tuoi oggetti. Se quelli sono sbagliati, DI potrebbe non essere la soluzione migliore:

  • In primo luogo, in sostanza, DI presume che l'accoppiamento stretto delle implementazioni di oggetti sia SEMPRE negativo . Questa è l'essenza del principio di inversione di dipendenza: "una dipendenza non dovrebbe mai essere fatta su una concrezione; solo su un'astrazione".

    Ciò chiude l'oggetto dipendente da modificare in base a una modifica dell'implementazione concreta; una classe che dipende specificamente da ConsoleWriter dovrà cambiare se l'output deve invece andare a un file, ma se la classe dipendesse solo da un IWriter che espone un metodo Write (), possiamo sostituire ConsoleWriter attualmente in uso con un FileWriter e il nostro la classe dipendente non conoscerebbe la differenza (principio di sostituzione di Liskhov).

    Tuttavia, un design non può MAI essere chiuso a tutti i tipi di modifica; se la progettazione dell'interfaccia IWriter stessa cambia, per aggiungere un parametro a Write (), ora è necessario modificare un oggetto di codice aggiuntivo (l'interfaccia IWriter), in cima all'oggetto / metodo di implementazione e al suo utilizzo. Se le modifiche all'interfaccia effettiva sono più probabili delle modifiche all'implementazione di detta interfaccia, l'accoppiamento lento (e la dipendenza da combinazioni vagamente accoppiate) possono causare più problemi di quanti ne risolva.

  • In secondo luogo, e corollario, DI presume che la classe dipendente non sia MAI un buon posto per creare una dipendenza . Questo vale per il principio di responsabilità singola; se si dispone di codice che crea una dipendenza e la utilizza anche, ci sono due motivi per cui la classe dipendente potrebbe dover cambiare (una modifica all'utilizzo o all'implementazione), violando SRP.

    Tuttavia, ancora una volta, l'aggiunta di livelli di riferimento indiretto per DI può essere una soluzione a un problema che non esiste; se è logico incapsulare la logica in una dipendenza, ma tale logica è la sola implementazione di una dipendenza, allora è più doloroso codificare la risoluzione della dipendenza (iniezione, ubicazione del servizio, fabbrica) liberamente accoppiata di quanto sarebbe per usare newe dimenticare.

  • Infine, DI per sua natura centralizza la conoscenza di tutte le dipendenze E delle loro implementazioni . Ciò aumenta il numero di riferimenti che deve avere l'assemblaggio che esegue l'iniezione e nella maggior parte dei casi NON riduce il numero di riferimenti richiesti dagli assiemi delle classi dipendenti effettive.

    QUALCOSA, IN QUALUNQUE MODO, deve avere conoscenza della dipendenza, dell'interfaccia di dipendenza e dell'implementazione della dipendenza per "connettere i punti" e soddisfare tale dipendenza. DI tende a collocare tutta quella conoscenza a un livello molto alto, sia in un contenitore IoC, sia nel codice che crea oggetti "principali" come la forma principale o il Controller che deve idratare (o fornire metodi di fabbrica per) le dipendenze. Questo può mettere un sacco di codice necessariamente strettamente accoppiato e molti riferimenti di assembly ad alti livelli della tua app, che necessita solo di questa conoscenza per "nasconderlo" alle classi dipendenti effettive (che da una prospettiva molto basilare è la miglior posto per avere questa conoscenza; dove viene utilizzato).

    Inoltre, normalmente non rimuove detti riferimenti dal basso verso il basso nel codice; un dipendente deve comunque fare riferimento alla libreria contenente l'interfaccia per la sua dipendenza, che si trova in una delle tre posizioni seguenti:

    • il tutto in un unico assembly "Interfaces" che diventa molto incentrato sull'applicazione,
    • ciascuno accanto alle implementazioni primarie, eliminando il vantaggio di non dover ricompilare le dipendenze quando cambiano le dipendenze, oppure
    • uno o due ciascuno in assiemi altamente coesivi, che gonfia il conteggio degli assiemi, aumenta notevolmente i tempi di "full build" e diminuisce le prestazioni dell'applicazione.

    Tutto questo, ancora una volta per risolvere un problema in luoghi dove potrebbe non essercene nessuno.


1
SRP = principio di responsabilità unica, per chiunque si chieda.
Theodore Murdock,

1
Sì, e LSP è il principio di sostituzione di Liskov; data una dipendenza da A, la dipendenza dovrebbe essere soddisfatta da B che deriva da A senza nessun altro cambiamento.
KeithS,

l'iniezione di dipendenza aiuta in particolare dove è necessario ottenere la classe A dalla classe B dalla classe D, ecc. in quel caso (e solo in quel caso) il framework DI può generare un gonfiamento di assemblaggio inferiore rispetto all'iniezione di povero. inoltre non ho mai avuto un collo di bottiglia causato da DI .. pensa ai costi di manutenzione, mai ai costi della CPU perché il codice può sempre essere ottimizzato, ma farlo senza un motivo ha un costo
GameDeveloper

Un'altra ipotesi sarebbe "se una dipendenza cambia, cambia ovunque"? Altrimenti devi comunque guardare tutte le classi consumatrici
Richard Tingle,

3
Questa risposta non riesce a risolvere l'impatto della testabilità dell'iniezione di dipendenze rispetto alla loro codifica. Vedi anche il principio delle dipendenze esplicite ( deviq.com/explicit-dependencies-principle )
ssmith

30

Al di fuori dei framework di iniezione di dipendenza, l'iniezione di dipendenza (tramite iniezione del costruttore o iniezione di setter) è quasi un gioco a somma zero: diminuisci l'accoppiamento tra l'oggetto A e la sua dipendenza B, ma ora qualsiasi oggetto che necessita di un'istanza di A ora deve costruisci anche l'oggetto B.

Hai leggermente ridotto l'accoppiamento tra A e B, ma hai ridotto l'incapsulamento di A e aumentato l'accoppiamento tra A e qualsiasi classe che deve costruire un'istanza di A, accoppiandoli anche alle dipendenze di A.

Quindi l'iniezione di dipendenza (senza un framework) è altrettanto dannosa in quanto utile.

Il costo aggiuntivo è spesso facilmente giustificabile, tuttavia: se il codice client sa di più su come costruire la dipendenza rispetto all'oggetto stesso, l'iniezione di dipendenza riduce davvero l'accoppiamento; ad esempio, uno Scanner non sa molto su come ottenere o costruire un flusso di input per analizzare l'input o da quale fonte il codice client vuole analizzare l'input, quindi l'iniezione del costruttore di un flusso di input è la soluzione ovvia.

Il test è un'altra giustificazione, al fine di poter utilizzare dipendenze simulate. Ciò dovrebbe significare l'aggiunta di un costruttore aggiuntivo utilizzato solo per il test che consente di iniettare dipendenze: se invece si modificano i costruttori per richiedere sempre l'iniezione di dipendenze, improvvisamente, è necessario conoscere le dipendenze delle dipendenze delle proprie dipendenze al fine di costruire le proprie dipendenze dirette e non è possibile svolgere alcun lavoro.

Può essere utile, ma dovresti sicuramente chiederti ogni dipendenza, il vantaggio del test vale il costo e voglio davvero deridere questa dipendenza durante il test?

Quando viene aggiunto un framework di iniezione delle dipendenze e la costruzione delle dipendenze viene delegata non al codice client ma al framework, l'analisi costi / benefici cambia notevolmente.

In un quadro di iniezione di dipendenza, i compromessi sono leggermente diversi; quello che stai perdendo iniettando una dipendenza è la capacità di sapere facilmente su quale implementazione fai affidamento e di spostare la responsabilità di decidere su quale dipendenza fai affidamento per un processo di risoluzione automatizzato (es. se abbiamo bisogno di un Foo @ Inject'ed , deve esserci qualcosa che @Provides Foo, e le cui dipendenze iniettate sono disponibili), o in alcuni file di configurazione di alto livello che prescrivono quale provider dovrebbe essere usato per ogni risorsa, o ad un ibrido dei due (ad esempio, potrebbe esserci essere un processo di risoluzione automatizzato per dipendenze che possono essere sovrascritte, se necessario, utilizzando un file di configurazione).

Come nell'iniezione del costruttore, penso che il vantaggio di farlo finisca, ancora una volta, molto simile al costo di farlo: non devi sapere chi sta fornendo i dati su cui fai affidamento e, se ci sono molteplici potenzialità fornitori, non è necessario conoscere l'ordine preferito per verificare la presenza di fornitori, assicurarsi che ogni posizione che necessita dei dati verifichi per tutti i potenziali fornitori, ecc., poiché tutto ciò è gestito ad alto livello dall'iniezione di dipendenza piattaforma.

Anche se personalmente non ho molta esperienza con i framework DI, la mia impressione è che forniscano maggiori vantaggi rispetto ai costi quando il mal di testa di trovare il fornitore corretto dei dati o del servizio di cui hai bisogno ha un costo superiore rispetto al mal di testa, quando qualcosa non riesce, di non sapere immediatamente a livello locale quale codice ha fornito i dati errati che hanno causato un successivo errore nel codice.

In alcuni casi, altri modelli che oscuravano le dipendenze (ad esempio i localizzatori di servizi) erano già stati adottati (e forse hanno anche dimostrato il loro valore) quando i framework DI sono apparsi sulla scena e i framework DI sono stati adottati perché offrivano alcuni vantaggi competitivi, come richiedere meno codice plateplate, o potenzialmente facendo di meno per oscurare il provider di dipendenza quando diventa necessario determinare quale provider è effettivamente in uso.


6
Solo un breve commento sulla derisione delle dipendenze nei test e "devi sapere delle dipendenze delle dipendenze delle tue dipendenze" ... Non è affatto vero. Non è necessario conoscere o preoccuparsi delle dipendenze di alcune implementazioni concrete se viene iniettata una simulazione. Basta conoscere le dipendenze dirette della classe sottoposta a test, che sono soddisfatte dalle finte.
Eric King,

6
Hai frainteso, non sto parlando di quando inietti un finto, sto parlando del vero codice. Considera la classe A con dipendenza B, che a sua volta ha dipendenza C, che a sua volta ha dipendenza D. Senza DI, A costruisce B, B costruisce C, C costruisce D. Con l'iniezione di costruzione, per costruire A, devi prima costruire B, per costruire B devi prima costruire C, e per costruire C devi prima costruire D. Quindi la classe A ora deve conoscere D, la dipendenza di una dipendenza di una dipendenza, al fine di costruire B. Questo porta ad un accoppiamento eccessivo.
Theodore Murdock,

1
Non ci sarebbero così tanti costi per avere un costruttore in più usato solo per i test che permette di iniettare le dipendenze. Proverò a rivedere ciò che ho detto.
Theodore Murdock,

3
Il dosatore di iniezione di dipendenza sposta le dipendenze solo in un gioco a somma zero. Sposta i tuoi dipendenti in luoghi già esistenti: il progetto principale. È anche possibile utilizzare approcci basati su convenzioni per assicurarsi che le dipendenze vengano risolte solo in fase di esecuzione, ad esempio specificando quali classi sono concrete per spazi dei nomi o altro. Devo dire che questa è una risposta ingenua
Sprague,

2
L'iniezione di dipendenza @Sprague si sposta attorno alle dipendenze in un gioco a somma leggermente negativa, se lo si applica nei casi in cui non dovrebbe essere applicato. Lo scopo di questa risposta è ricordare alle persone che l'iniezione di dipendenza non è intrinsecamente buona, è un modello di progettazione che è incredibilmente utile nelle giuste circostanze, ma un costo ingiustificabile in circostanze normali (almeno nei domini di programmazione in cui ho lavorato , Capisco che in alcuni domini è più spesso utile che in altri). DI a volte diventa una parola d'ordine e un fine in sé, che manca il punto.
Theodore Murdock,

11
  1. se stai creando entità di database, dovresti piuttosto avere qualche classe di fabbrica che inietterai invece al tuo controller,

  2. se devi creare oggetti primitivi come ints o long. Inoltre dovresti creare "a mano" la maggior parte degli oggetti della libreria standard come date, guide, ecc.

  3. se si desidera iniettare stringhe di configurazione, probabilmente è meglio iniettare alcuni oggetti di configurazione (in generale si consiglia di avvolgere tipi semplici in oggetti significativi: int temperatureInCelsiusDegrees -> CelciusDeegree temperature)

E non usare Service locator come alternativa per l'iniezione di dipendenza, è anti-pattern, maggiori informazioni: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx


Tutti i punti riguardano l'uso di diverse forme di iniezione piuttosto che non del tutto. +1 per il collegamento però.
Jan Hudec,

3
Service Locator NON è un anti-schema e il ragazzo a cui sei collegato non è un esperto di schemi. Il fatto che sia diventato famoso in StackExchange è una specie di disservizio nella progettazione di software. Martin Fowler (autore di Patterns of Enterprise Application Architecture) ha una visione più ragionevole della questione: martinfowler.com/articles/injection.html
Colin

1
@Colin, al contrario, Service Locator è sicuramente un anti-pattern, e qui in breve è il motivo .
Kyralessa,

@Kyralessa - ti rendi conto che i framework DI utilizzano internamente un modello di Service Locator per cercare i servizi, giusto? Ci sono circostanze in cui entrambi sono appropriati, e nessuno dei due è un modello anti, nonostante l'odio StackExchange che alcune persone vogliono scaricare.
Colin,

Certo, e la mia macchina usa internamente pistoni e candele e una miriade di altre cose che non sono, tuttavia, una buona interfaccia per un normale essere umano che vuole solo guidare una macchina.
Kyralessa,

8

Quando non riuscirai a guadagnare nulla rendendo il tuo progetto mantenibile e testabile.

Seriamente, adoro l'IoC e il DI in generale, e direi che il 98% delle volte userò questo schema senza fallo. È particolarmente importante in un ambiente multiutente, in cui il codice può essere riutilizzato più volte da diversi membri del team e diversi progetti, in quanto separa la logica dall'implementazione. La registrazione è un esempio lampante di ciò, un'interfaccia ILog iniettata in una classe è mille volte più gestibile della semplice connessione al tuo framework di registrazione-du-jour, poiché non hai alcuna garanzia che un altro progetto utilizzerà lo stesso framework di registrazione (se utilizza uno a tutti!).

Tuttavia, ci sono momenti in cui non è un modello applicabile. Ad esempio, i punti di ingresso funzionali che sono implementati in un contesto statico da un inizializzatore non sostituibile (WebMethods, ti sto guardando, ma il tuo metodo Main () nella tua classe Program è un altro esempio) semplicemente non possono avere delle dipendenze iniettate all'inizializzazione tempo. Vorrei anche dire che un prototipo, o qualsiasi pezzo di codice investigativo da buttar via, è anche un cattivo candidato; i vantaggi di DI sono praticamente vantaggi a medio-lungo termine (testabilità e manutenibilità), se sei sicuro che eliminerai la maggior parte di un pezzo di codice entro una settimana o giù di lì direi che non otterrai nulla isolando dipendenze, trascorri semplicemente il tempo che impiegheresti normalmente a testare e isolare le dipendenze per far funzionare il codice.

Tutto sommato, è sensato adottare un approccio pragmatico a qualsiasi metodologia o modello, poiché nulla è applicabile al 100% delle volte.

Una cosa da notare è il tuo commento sui test automatizzati: la mia definizione di questo è test funzionali automatizzati , ad esempio test di selenio con script se ti trovi in ​​un contesto web. Questi sono generalmente test completamente black-box, senza la necessità di conoscere il funzionamento interno del codice. Se ti riferissi a test di unità o di integrazione direi che il modello DI è quasi sempre applicabile a qualsiasi progetto che si basa fortemente su quel tipo di test in white box, poiché, ad esempio, ti consente di testare cose come metodi che toccano il DB senza che sia necessario che sia presente un DB.


Non capisco cosa intendi per registrazione. Sicuramente tutti i logger non si conformeranno alla stessa interfaccia, quindi dovresti scrivere il tuo wrapper per tradurre tra questo modo di logging di progetti e un logger specifico (supponendo che tu volessi essere in grado di cambiare facilmente logger). Quindi dopo cosa ti dà DI?
Richard Tingle,

@RichardTingle Direi che dovresti definire il tuo wrapper di registrazione come interfaccia e quindi scrivere un'implementazione leggera di tale interfaccia per ciascun servizio di registrazione esterno, invece di utilizzare una singola classe di registrazione che avvolge più astrazioni. È lo stesso concetto che stai proponendo ma sottratto a un livello diverso.
Ed James,

1

Mentre le altre risposte si concentrano sugli aspetti tecnici, vorrei aggiungere una dimensione pratica.

Nel corso degli anni sono giunto alla conclusione che ci sono diversi requisiti pratici che devono essere soddisfatti se l'introduzione dell'iniezione di dipendenza deve avere successo.

  1. Dovrebbe esserci un motivo per introdurlo.

    Sembra ovvio, ma se il tuo codice prende solo le cose dal database e lo restituisce senza alcuna logica, l'aggiunta di un contenitore DI rende le cose più complesse senza vantaggi reali. I test di integrazione sarebbero più importanti qui.

  2. La squadra deve essere addestrata e integrata.

    A meno che la maggior parte del team non sia a bordo e capisca che l'aggiunta di DI inversione del contenitore di controllo diventa un altro modo di fare le cose e rende la base di codice ancora più complicata.

    Se il DI viene introdotto da un nuovo membro del team, perché lo comprendono e gli piacciono e vogliono solo dimostrare che sono buoni, E il team non è attivamente coinvolto, c'è il rischio reale che riduca effettivamente la qualità di il codice.

  3. Devi testare

    Mentre il disaccoppiamento è generalmente una buona cosa, DI può spostare la risoluzione di una dipendenza dal tempo di compilazione al tempo di esecuzione. Questo è in realtà abbastanza pericoloso se non si esegue il test bene. Gli errori di risoluzione del tempo di esecuzione possono essere costosi da tracciare e risolvere.
    (È chiaro dal tuo test che lo fai, ma molti team non eseguono test nella misura richiesta da DI.)


1

Questa non è una risposta completa, ma solo un altro punto.

Quando un'applicazione viene avviata una volta, viene eseguita a lungo (come un'app Web) DI potrebbe essere utile.

Quando si dispone di un'applicazione che si avvia più volte e viene eseguita per periodi più brevi (come un'app mobile), probabilmente non si desidera il contenitore.


Non vedo come il tempo di applicazione in diretta sia correlato a DI
Michael Freidgeim

@MichaelFreidgeim Ci vuole tempo per inizializzare il contesto, di solito i container DI sono piuttosto pesanti, come Spring. Crea un'app ciao mondo con una sola classe e creane una con Spring e inizia entrambe 10 volte e vedrai cosa intendo.
Koray Tugay,

Sembra un problema con un singolo contenitore DI. Nel mondo .Net non ho sentito parlare del tempo di inizializzazione come un problema essenziale per i container DI
Michael Freidgeim,

1

Prova a usare i principi OOP di base: usa l' ereditarietà per estrarre funzionalità comuni, incapsulare (nascondere) cose che dovrebbero essere protette dal mondo esterno usando membri / tipi privati ​​/ interni / protetti. Utilizzare qualsiasi potente framework di test per iniettare codice solo per i test, ad esempio https://www.typemock.com/ o https://www.telerik.com/products/mocking.aspx .

Quindi prova a riscriverlo con DI e confronta il codice, cosa vedrai di solito con DI:

  1. Hai più interfacce (più tipi)
  2. Hai creato duplicati di firme di metodi pubblici e dovrai mantenerlo doppio (non puoi semplicemente modificare alcuni parametri una volta, dovrai farlo due volte, praticamente tutte le possibilità di refactoring e di navigazione diventano più complicate)
  3. Hai spostato alcuni errori di compilazione per eseguire errori di tempo (con DI puoi semplicemente ignorare una certa dipendenza durante la codifica e non certo che sarà esposto durante il test)
  4. Hai aperto l'incapsulamento. Ora i membri protetti, i tipi interni ecc. Sono diventati pubblici
  5. La quantità complessiva di codice è stata aumentata

Direi da quello che ho visto, quasi sempre, la qualità del codice viene ridotta con DI.

Tuttavia, se utilizzi solo il modificatore di accesso "pubblico" nella dichiarazione di classe e / o modificatori pubblici / privati ​​per i membri e / o non hai la possibilità di acquistare costosi strumenti di test e allo stesso tempo hai bisogno di test di unità che possono " essere sostituito da test integrativi e / o hai già interfacce per le classi che stai pensando di iniettare, DI è una buona scelta!

ps probabilmente avrò un sacco di svantaggi per questo post, credo perché la maggior parte degli sviluppatori moderni non capisce come e perché usare la parola chiave interna e come ridurre l'accoppiamento dei componenti e infine perché diminuirlo) finalmente, solo prova a codificare e confrontare


0

Un'alternativa al Dependency Injection sta usando un Service Locator . Un Service Locator è più facile da comprendere, eseguire il debug e semplifica la costruzione di un oggetto, soprattutto se non si utilizza un framework DI. I Service Locator sono un buon modello per la gestione delle dipendenze statiche esterne , ad esempio un database che altrimenti dovresti passare in ogni oggetto nel tuo livello di accesso ai dati.

Quando si esegue il refactoring del codice legacy , è spesso più semplice eseguire il refactoring in un Service Locator piuttosto che in Dependency Injection. Tutto ciò che devi fare è sostituire le istanze con ricerche di servizio e quindi falsificare il servizio nel test unitario.

Tuttavia, ci sono alcuni aspetti negativi di Service Locator. Conoscere le depandance di una classe è più difficile perché le dipendenze sono nascoste nell'implementazione della classe, non nei costruttori o nei setter. E creare due oggetti che si basano su diverse implementazioni dello stesso servizio è difficile o impossibile.

TLDR : se la tua classe ha dipendenze statiche o stai eseguendo il refactoring del codice legacy, un Service Locator è probabilmente migliore di DI.


12

Quando si tratta di dipendenze statiche, preferirei vedere facciate schiaffeggiate che implementano interfacce. Questi possono quindi essere iniettati dipendenza e cadono sulla granata statica per te.
Erik Dietrich,

@Kyralessa Sono d'accordo, un Service Locator ha molti aspetti negativi e DI è quasi sempre preferibile. Tuttavia, credo che ci siano un paio di eccezioni alla regola, come per tutti i principi di programmazione.
Garrett Hall,

L'uso principale accettato del luogo di servizio è all'interno di un modello di strategia, in cui una classe di "selezione strategia" è dotata di una logica sufficiente per trovare la strategia da utilizzare e restituirla o passarci una chiamata. Anche in questo caso, il selettore di strategia può essere una facciata per un metodo di fabbrica fornito da un contenitore IoC, a cui viene fornita la stessa logica; il motivo per cui lo faresti scoppiare è mettere la logica dove appartiene e non dove è più nascosta.
KeithS,

Per citare Martin Fowler, "La scelta tra (DI e Service Locator) è meno importante del principio di separazione della configurazione dall'uso". L'idea che DI sia SEMPRE migliore è hogwash. Se un servizio viene utilizzato a livello globale, è più ingombrante e fragile usare DI. Inoltre, DI è meno favorevole per le architetture di plug-in in cui le implementazioni non sono note fino al runtime. Pensa quanto sarebbe strano passare sempre ulteriori argomenti a Debug.WriteLine () e simili!
Colin,
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.