Quando non è appropriato usare il modello di iniezione delle dipendenze?

112

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

    
posta Tom Squires 20.02.2012 - 18:57
fonte

7 risposte

111

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

  • Innanzitutto, in pratica, DI presuppone che l'accoppiamento stretto delle implementazioni degli oggetti sia SEMPRE cattivo . Questa è l'essenza del principio di inversione di dipendenza: "una dipendenza non dovrebbe mai essere fatta su una concrezione, solo su un'astrazione".

    Questo chiude l'oggetto dipendente per cambiare in base a una modifica all'implementazione concreta; una classe seconda ConsoleWriter specificamente dovrà cambiare se l'uscita ha bisogno di andare in un file, invece, ma se la classe fosse dipende solo un iWriter esponendo un metodo Write (), si può sostituire il ConsoleWriter attualmente in uso con un FileWriter e la nostra la classe dipendente non conoscerebbe la differenza (principio di sostituzione di Liskhov).

    Tuttavia, un progetto non può MAI essere chiuso a tutti i tipi di cambiamento; se il design dell'interfaccia IWriter cambia di per sé, per aggiungere un parametro a Write (), un oggetto di codice aggiuntivo (l'interfaccia IWriter) deve ora essere modificato, oltre all'oggetto / metodo di implementazione e ai suoi usi. Se le modifiche all'interfaccia reale sono più probabili rispetto alle modifiche all'implementazione di detta interfaccia, l'accoppiamento libero (e le dipendenze con accoppiamento lento) può causare più problemi di quanti ne risolva.

  • In secondo luogo, e corollario, DI presuppone che la classe dipendente non sia MAI un buon posto per creare una dipendenza . Questo va al Principio di Responsabilità Unica; se si dispone di un codice che crea una dipendenza e la utilizza anche, allora ci sono due ragioni 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 quella logica è l'unica implementazione di tale dipendenza, allora è più doloroso codificare la risoluzione della dipendenza (iniezione, posizione del servizio, fabbrica) poco accoppiato di quanto sarebbe per utilizzare solo new e non pensarci più.

  • 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'assembly che esegue l'iniezione e, nella maggior parte dei casi, NON riduce il numero di riferimenti richiesti dagli assiemi delle classi dipendenti effettive.

    QUALCOSA, QUALCHE PARTE, deve avere conoscenza del dipendente, dell'interfaccia delle dipendenze e dell'implementazione delle dipendenze per "connettere i punti" e soddisfare tale dipendenza. DI tende a collocare tutta questa conoscenza ad 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 ha bisogno solo di questa conoscenza per "nasconderlo" dalle classi dipendenti reali (che da una prospettiva molto basilare è il il miglior posto per avere questa conoscenza, dove è usato).

    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:

    • tutto in un unico assembly "Interfacce" che diventa molto centrato sull'applicazione,
    • ciascuno a fianco dell'implementazione primaria (s), rimuovendo il vantaggio di non dover ricompilare i dipendenti quando cambiano le dipendenze, o
    • uno o due a testa in gruppi altamente coesivi, il che complica il numero di assiemi, aumenta drasticamente i tempi di "full build" e diminuisce le prestazioni delle applicazioni.

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

risposta data 21.02.2012 - 20:11
fonte
27

Al di fuori dei quadri di iniezione delle dipendenze, l'iniezione di dipendenza (tramite iniezione del costruttore o iniezione setter) è quasi un gioco a somma zero: si riduce l'accoppiamento tra l'oggetto A e la dipendenza B, ma ora qualsiasi oggetto che necessita di un'istanza di A deve ora anche costruire 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 quanto utile.

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

Il test è un'altra giustificazione, per poter usare le dipendenze fittizie. Ciò dovrebbe significare l'aggiunta di un costruttore aggiuntivo utilizzato per i test che consente di iniettare le dipendenze: se invece si cambiano i costruttori per richiedere sempre le dipendenze da iniettare, improvvisamente, è necessario conoscere le dipendenze delle dipendenze delle dipendenze per costruire il proprio dipendenze dirette e non è possibile eseguire alcun lavoro.

Può essere utile, ma dovresti sicuramente chiedertelo per ogni dipendenza, il vantaggio del test vale il costo, e ho intenzione di voler prendere in giro questa dipendenza durante il test?

Quando viene aggiunta una struttura di dipendenza dall'iniezione e la costruzione delle dipendenze è delegata non al codice client ma al framework, l'analisi costi / benefici cambia notevolmente.

In un'infrastruttura per l'iniezione delle dipendenze, i compromessi sono leggermente diversi; quello che stai perdendo iniettando una dipendenza è la capacità di sapere facilmente a quale implementazione si sta facendo affidamento, e la responsabilità di decidere su quale dipendenza ti stai affidando a qualche processo di risoluzione automatica (ad esempio se richiediamo un @ Inject'ed Foo , ci deve essere qualcosa che @Provides Foo, e le cui dipendenze iniettate sono disponibili), o ad un file di configurazione di alto livello che prescrive quale provider dovrebbe essere usato per ogni risorsa, o ad alcuni ibridi tra i due (per esempio, ci può essere un processo di risoluzione automatica per le dipendenze che può essere sovrascritto, se necessario, utilizzando un file di configurazione).

Come nell'iniezione del costruttore, penso che il vantaggio di farlo sia, ancora una volta, molto simile al costo di farlo: non devi sapere chi fornisce i dati su cui fai affidamento e, se ci sono sono molteplici potenziali fornitori, non è necessario conoscere l'ordine preferito per verificare la presenza di fornitori, assicurarsi che ogni luogo che ha bisogno di controlli dei dati per tutti i potenziali fornitori, ecc., perché tutto ciò è gestito ad un livello elevato da la piattaforma di distribuzione delle dipendenze.

Anche se personalmente non ho molta esperienza con i framework DI, la mia impressione è che essi forniscano più benefici che costi quando il mal di testa di trovare il fornitore corretto dei dati o del servizio di cui hai bisogno ha un costo maggiore di il mal di testa, quando qualcosa non funziona, di non sapere immediatamente localmente quale codice ha fornito i dati non validi che hanno causato un errore successivo nel codice.

In alcuni casi, altri schemi che oscuravano le dipendenze (es. localizzatori di servizi) erano già stati adottati (e forse hanno anche dimostrato il loro valore) quando i quadri DI apparvero sulla scena, e i quadri DI furono adottati perché offrivano un vantaggio competitivo, come richiedere meno codice boilerplate o potenzialmente fare meno per oscurare il fornitore di dipendenza quando diventa necessario determinare quale fornitore è effettivamente in uso.

    
risposta data 21.02.2012 - 01:39
fonte
11
  1. se si stanno creando entità di database, si dovrebbe preferire una classe di produzione che verrà iniettata sul controller,

  2. se hai bisogno di creare oggetti primitivi come interi o long. Inoltre dovresti creare "a mano" la maggior parte degli oggetti della libreria standard come date, guids, ecc.

  3. se vuoi iniettare stringhe di configurazione è probabilmente meglio iniettare alcuni oggetti di configurazione (in generale si consiglia di avvolgere tipi semplici in oggetti significativi: temperatura intInInCelsiusDegrees - > temperatura di CelciusDeegree)

E non utilizzare Service locator come alternativa all'iniezione di dipendenza, è anti-pattern, maggiori informazioni: link

    
risposta data 20.02.2012 - 23:09
fonte
8

Quando non hai intenzione di guadagnare nulla rendendo il tuo progetto mantenibile e verificabile.

Seriamente, adoro IoC e DI in generale, e direi che il 98% delle volte userò questo modello senza errori. È particolarmente importante in un ambiente multiutente, in cui il codice può essere riutilizzato più e più volte dai diversi membri del team e da diversi progetti, in quanto separa la logica dall'implementazione. Il logging è un ottimo esempio di ciò, un'interfaccia ILog iniettata in una classe è mille volte più manutenibile rispetto al semplice collegamento del framework di registrazione-du-jour, poiché non si ha 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 implementati in un contesto statico da un inizializzatore non sovrascrivibile (WebMethods, sto osservando te, ma il tuo metodo Main () nella classe Program è un altro esempio) semplicemente non possono avere le dipendenze iniettate all'inizializzazione tempo. Mi spingo anche a dire che un prototipo, o qualsiasi pezzo di codice investigativo da buttare via, è anche un cattivo candidato; i vantaggi del DI sono piuttosto benefici a medio-lungo termine (testabilità e manutenibilità), se sei certo che getti via la maggior parte di un pezzo di codice entro una settimana o giù di lì direi che non ottieni nulla isolando dipendenze, dedica solo il tempo che normalmente impieghi a testare e isolare le dipendenze per far funzionare il codice.

Tutto sommato, è ragionevole 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 automatici: la mia definizione di questo è test funzionali automatici, ad esempio test di selenio con script se sei in un contesto web. Questi sono in genere test completamente black-box, senza bisogno di conoscere il funzionamento interno del codice. Se ti riferivi ai test di unità o di integrazione, direi che il pattern DI è quasi sempre applicabile a qualsiasi progetto che si basa pesantemente su quel tipo di test white-box, poiché, ad esempio, ti permette di testare cose come metodi che toccano il DB senza bisogno di un DB per essere presente.

    
risposta data 21.02.2012 - 20:42
fonte
1

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

Quando hai un'applicazione che si avvia una volta, viene eseguita per un lungo periodo di tempo (come un'app Web) DI potrebbe essere buono.

Quando hai un'applicazione che si avvia molte volte e viene eseguita per tempi più brevi (come un'app mobile) probabilmente non vuoi il contenitore.

    
risposta data 16.09.2016 - 09:28
fonte
0

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 della dipendenza da iniezione deve essere un successo.

  1. Ci dovrebbe essere una ragione per introdurlo.

    Questo sembra ovvio, ma se il codice ottiene solo elementi dal database e lo restituisce senza alcuna logica, l'aggiunta di un contenitore DI rende le cose più complesse con un vantaggio reale. I test di integrazione sarebbero più importanti qui.

  2. Il team deve essere addestrato e a bordo.

    A meno che la maggior parte della squadra non sia a bordo e comprenda che l'aggiunta di DI di inversione del container di controllo diventa un altro modo di fare le cose e rende il codice base ancora più complicato.

    Se il DI viene introdotto da un nuovo membro del team, perché lo capiscono e lo apprezzano e vogliono solo dimostrare che sono buoni, E il team non è coinvolto attivamente, c'è il rischio reale che in realtà diminuire la qualità del codice.

  3. Devi testare

    Sebbene il disaccoppiamento sia 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 prova 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 testano nella misura richiesta da DI.)

risposta data 16.09.2016 - 02:27
fonte
-1

Un'in alternativa a Dependency Injection sta utilizzando un Localizzatore di servizi . Un Localizzatore di servizi è più facile da capire, eseguire il debug e rende più semplice la costruzione di un oggetto, specialmente se non si utilizza un framework DI. Gli Localizzatori di servizio sono un buon modello per gestire le dipendenze statiche esterne , ad esempio un database che altrimenti dovresti passare in ogni oggetto nel tuo livello di accesso ai dati.

Quando refactoring code legacy , è spesso più facile da refactoring a un Service Locator che a Dependency Injection. Tutto quello che devi fare è sostituire le istanze con le ricerche di servizio e quindi falsificare il servizio nel test dell'unità.

Tuttavia, ci sono alcuni aspetti negativi per l'individuazione del servizio. Conoscere le depandance di una classe è più difficile perché le dipendenze sono nascoste nell'implementazione della classe, non in costruttori o setter. E la creazione di due oggetti che si basano su diverse implementazioni dello stesso servizio è difficile o impossibile.

TLDR : se la tua classe ha dipendenze statiche o stai rifattando il codice legacy, un Localizzatore di servizio è probabilmente migliore di DI.

    
risposta data 20.02.2012 - 19:54
fonte

Leggi altre domande sui tag