Critica e svantaggi dell'iniezione di dipendenza

109

L'iniezione di dipendenza (DI) è un modello ben noto e alla moda. La maggior parte degli ingegneri conosce i suoi vantaggi, come:

  • Rendere possibile l'isolamento in test di unità / facile
  • Definizione esplicita delle dipendenze di una classe
  • Facilitare il buon design ( principio di responsabilità singola (SRP) per esempio)
  • Abilitare le implementazioni di commutazione rapidamente ( DbLogger invece di ConsoleLogger per esempio)

Ritengo che vi sia un ampio consenso a livello di settore sul fatto che DI sia un modello valido e utile. Non ci sono troppe critiche al momento. Gli svantaggi menzionati nella comunità sono in genere minori. Alcuni di loro:

  • Aumento del numero di classi
  • Creazione di interfacce non necessarie

Attualmente discutiamo del design dell'architettura con il mio collega. È piuttosto conservatore, ma di mentalità aperta. Gli piace mettere in discussione le cose, che considero buone, perché molte persone nell'IT copiano solo la tendenza più recente, ripetono i vantaggi e in generale non pensano troppo - non analizzano troppo in profondità.

Le cose che vorrei chiedere sono:

  • Dovremmo usare l'integrazione delle dipendenze quando abbiamo una sola implementazione?
  • Dovremmo vietare la creazione di nuovi oggetti ad eccezione di quelli linguaggio / framework?
  • Sta iniettando una sola cattiva idea di implementazione (diciamo che abbiamo solo un'implementazione, quindi non vogliamo creare un'interfaccia "vuota") se non pianifichiamo di testare una classe in particolare?
posta Landeeyo 29.05.2018 - 13:20
fonte

8 risposte

152

In primo luogo, vorrei separare l'approccio progettuale dal concetto di framework. L'iniezione di dipendenza al suo livello più semplice e fondamentale è semplicemente:

A parent object provides all the dependencies required to the child object.

Questo è tutto. Nota che niente in ciò richiede interfacce, framework, qualsiasi tipo di iniezione, ecc. Per essere onesti, ho imparato a conoscere questo modello 20 anni fa. Non è nuovo.

A causa di più di 2 persone che hanno confusione sul termine genitore e figlio, nel contesto dell'iniezione di dipendenza:

  • Il genitore è l'oggetto che crea un'istanza e configura l'oggetto figlio che usa
  • Il figlio è il componente progettato per essere istanziato passivamente. Cioè è progettato per utilizzare tutte le dipendenze fornite dal genitore e non crea un'istanza delle sue dipendenze.

L'iniezione di dipendenza è un modello per l'oggetto composizione .

Perché interfacce?

Le interfacce sono un contratto. Esistono per limitare quanto possono essere strettamente accoppiati due oggetti. Non tutte le dipendenze necessitano di un'interfaccia, ma aiutano a scrivere codice modulare.

Quando aggiungi il concetto di unit test, puoi avere due implementazioni concettuali per ogni interfaccia specifica: l'oggetto reale che vuoi usare nella tua applicazione e l'oggetto deriso o stubato che usi per testare il codice che dipende dal oggetto. Questo da solo può essere una giustificazione sufficiente per l'interfaccia.

Perché framework?

Inizializzare e fornire essenzialmente dipendenze agli oggetti figlio può essere scoraggiante quando ne esiste un gran numero. I framework offrono i seguenti vantaggi:

  • Autowiring dipendenze ai componenti
  • Configurazione dei componenti con le impostazioni di qualche tipo
  • Automatizzare il codice della piastra della caldaia in modo da non doverlo vedere scritto in più posizioni.

Hanno anche i seguenti svantaggi:

  • L'oggetto genitore è un "contenitore" e non qualcosa nel tuo codice
  • Rende il test più complicato se non puoi fornire le dipendenze direttamente nel tuo codice di prova
  • Può rallentare l'inizializzazione in quanto risolve tutte le dipendenze usando la riflessione e molti altri trucchi
  • Il debugging in fase di esecuzione può essere più difficile, in particolare se il contenitore inietta un proxy tra l'interfaccia e il componente reale che implementa l'interfaccia (viene in mente una programmazione orientata agli aspetti integrata a Spring). Il contenitore è una scatola nera e non sempre sono costruiti con alcun concetto di facilitazione del processo di debug.

Tutto ciò detto, ci sono dei compromessi. Per piccoli progetti in cui non ci sono molte parti mobili, e ci sono pochi motivi per usare un framework DI. Tuttavia, per progetti più complicati in cui alcuni componenti sono già realizzati per te, il framework può essere giustificato.

Che dire [articolo casuale su Internet]?

Che ne pensi? Molte volte le persone possono diventare eccessivamente zelanti e aggiungere un sacco di restrizioni e rimproverarle se non si stanno facendo le cose l '"unico vero modo". Non c'è un vero modo. Vedi se riesci a estrarre qualcosa di utile dall'articolo e ignorare le cose con le quali non sei d'accordo.

In breve, pensa a te stesso e prova le cose.

Lavorare con "vecchie teste"

Impara il più possibile. Quello che troverai con molti sviluppatori che lavorano nei loro anni '70 è che hanno imparato a non essere dogmatici su molte cose. Hanno metodi con cui hanno lavorato per decenni che producono risultati corretti.

Ho avuto il privilegio di lavorare con alcuni di questi, e possono fornire un feedback brutalmente onesto che ha molto senso. E dove vedono il valore, aggiungono quegli strumenti al loro repertorio.

    
risposta data 29.05.2018 - 15:03
fonte
80

L'iniezione di dipendenza è, come la maggior parte dei modelli, una soluzione ai problemi . Quindi inizia chiedendo se hai persino il problema in primo luogo. In caso contrario, utilizzare il modello molto probabilmente renderà il codice peggiore .

Considerare innanzitutto se è possibile ridurre o eliminare le dipendenze. A parità di altre condizioni, vogliamo che ogni componente di un sistema abbia il minor numero possibile di dipendenze. E se le dipendenze sono sparite, la questione dell'iniezione o meno diventa discutibile!

Considera un modulo che scarica alcuni dati da un servizio esterno, li analizza ed esegue analisi complesse e scrive in un file.

Ora, se la dipendenza dal servizio esterno è hardcoded, sarà davvero difficile testare l'elaborazione interna di questo modulo. Quindi potresti decidere di iniettare il servizio esterno e il file system come dipendenze dell'interfaccia, che ti permetteranno di iniettare mock invece che a sua volta rende possibile il test unitario della logica interna.

Ma una soluzione molto migliore è semplicemente quella di separare l'analisi dall'input / output. Se l'analisi viene estratta in un modulo senza effetti collaterali, sarà molto più facile da testare. Nota che il mocking è un odore di codice - non sempre è evitabile, ma in generale, è meglio se puoi testare senza fare affidamento sul beffardo. Quindi, eliminando le dipendenze, si evitano i problemi che DI dovrebbe alleviare. Nota che anche questo design aderisce molto meglio all'SRP.

Voglio sottolineare che DI non facilita necessariamente l'SRP o altri buoni principi di progettazione come separazione delle preoccupazioni, alta coesione / basso accoppiamento e così via. Potrebbe anche avere l'effetto opposto. Considera una classe A che utilizza internamente un'altra classe B. B è utilizzato solo da A e pertanto completamente incapsulato e può essere considerato un dettaglio di implementazione. Se modifichi questo per iniettare B nel costruttore di A, allora hai esposto questo dettaglio di implementazione e ora la conoscenza di questa dipendenza e su come inizializzare B, la vita di B e così via devono esistere in qualche altro posto nel sistema separatamente da A. Quindi hai un'architettura generale peggiore con problemi di perdite.

D'altra parte ci sono alcuni casi in cui DI è davvero utile. Ad esempio per servizi globali con effetti collaterali come un logger.

Il problema è quando schemi e architetture diventano obiettivi in sé stessi piuttosto che strumenti. Sto solo chiedendo "Dovremmo usare DI?" è una specie di mettere il carro davanti al cavallo. Dovresti chiedere: "Abbiamo un problema?" e "Qual è la migliore soluzione per questo problema?"

Una parte della tua domanda si riduce a: "Dovremmo creare interfacce superflue per soddisfare le richieste del modello?" Probabilmente già ti rendi conto della risposta a questo - assolutamente no ! Chiunque ti dica il contrario sta cercando di venderti qualcosa - molto probabilmente orari di consulenza costosi. Un'interfaccia ha valore solo se rappresenta un'astrazione. Un'interfaccia che imita la superficie di una singola classe è chiamata "interfaccia di intestazione" e questo è un antipattern noto.

    
risposta data 29.05.2018 - 15:34
fonte
33

Nella mia esperienza, ci sono un certo numero di svantaggi nell'iniezione di dipendenza.

Innanzitutto, l'utilizzo di DI non semplifica il test automatizzato quanto pubblicizzato. L'unità che esegue il test di una classe con un'implementazione fittizia di un'interfaccia consente di verificare in che modo tale classe interagirà con l'interfaccia. Cioè, ti permette di testare come la classe in prova utilizza il contratto fornito dall'interfaccia. Tuttavia, questo fornisce una garanzia molto maggiore che l'input dalla classe sotto test nell'interfaccia sia come previsto. Fornisce piuttosto scarsa garanzia che la classe in prova risponda come previsto per l'output dall'interfaccia in quanto è quasi universalmente un output fittizio, che è esso stesso soggetto a bug, semplificazioni eccessive e così via. In breve, NON consente di verificare che la classe si comporti come previsto con un'implementazione reale dell'interfaccia.

In secondo luogo, DI rende molto più difficile navigare attraverso il codice. Quando si cerca di passare alla definizione di classi utilizzate come input per le funzioni, un'interfaccia può essere qualsiasi cosa, da un fastidio minore (ad esempio, dove c'è una singola implementazione) a un time sink importante (ad esempio quando viene utilizzata un'interfaccia troppo generica come IDisposable) quando si cerca di trovare l'effettiva implementazione utilizzata. Questo può trasformare un semplice esercizio del tipo "Ho bisogno di correggere un'eccezione di riferimento null nel codice che accade subito dopo che questa istruzione di registrazione è stata stampata" in uno sforzo di un giorno.

In terzo luogo, l'uso di DI e strutture è un'arma a doppio taglio. Può ridurre notevolmente la quantità di codice della piastra della caldaia necessaria per le operazioni comuni. Tuttavia, ciò comporta la necessità di avere una conoscenza dettagliata della particolare struttura di DI per capire come queste operazioni comuni siano effettivamente collegate insieme. Comprendere come le dipendenze vengono caricate nel framework e aggiungere una nuova dipendenza nel framework da iniettare può richiedere la lettura di una buona quantità di materiale di background e il seguire alcuni tutorial di base sul framework. Questo può trasformare alcune semplici attività in cose piuttosto dispendiose in termini di tempo.

    
risposta data 29.05.2018 - 23:44
fonte
12

Ho seguito il consiglio di Mark Seemann da "Dependency injection in .NET" - per ipotizzare.

DI deve essere utilizzato quando si ha una "dipendenza volatile", ad es. c'è una ragionevole possibilità che cambi.

Quindi, se pensi che potresti avere più di un'implementazione in futuro o l'implementazione potrebbe cambiare, usa DI. Altrimenti new va bene.

    
risposta data 29.05.2018 - 13:38
fonte
2

Enabling switching implementations quickly (DbLogger instead of ConsoleLogger for example)

Mentre DI in generale è sicuramente una buona cosa, suggerirei di non usarla ciecamente per tutto. Ad esempio, non inserisco mai i logger. Uno dei vantaggi di DI sta rendendo le dipendenze esplicite e chiare. Non ha senso elencare ILogger come dipendenza di quasi tutte le classi - è solo confusione. È responsabilità del logger fornire la flessibilità necessaria. Tutti i miei logger sono membri finali statici, potrei prendere in considerazione l'iniezione di un registratore quando ho bisogno di uno strumento non statico.

Increased number of classes

Questo è uno svantaggio del framework DI o del framework mocking, non di DI stesso. Nella maggior parte dei casi le mie classi dipendono da classi concrete, il che significa che non c'è bisogno di una caldaia standard. Guice (un framework DI DI Java) lega per impostazione predefinita una classe a se stessa e ho solo bisogno di sovrascrivere l'associazione nei test (o collegarli manualmente invece).

Creation of unnecessary interfaces

Creo solo le interfacce quando necessario (che è piuttosto raro). Ciò significa che a volte devo sostituire tutte le occorrenze di una classe tramite un'interfaccia, ma l'IDE può farlo per me.

Should we use dependency injection when we have just one implementation?

Sì, ma evita qualsiasi testo aggiuntivo aggiunto .

Should we ban creating new objects except language/framework ones?

No. Ci saranno molte classi di valore (immutabili) e dati (mutabili), in cui le istanze vengono create e passate in giro e dove non c'è motivo iniettandoli - poiché non vengono mai memorizzati in un altro oggetto (o solo in altri oggetti simili).

Per loro, potrebbe essere necessario iniettare una fabbrica, ma la maggior parte delle volte non ha senso (ad esempio, @Value class NamedUrl {private final String name; private final URL url;} , in realtà non hai bisogno di una fabbrica e non c'è niente da iniettare).

Is injecting a single implementation bad idea (let's say we have just one implementation so we don't want to create "empty" interface) if we don't plan to unit test a particular class?

IMHO va bene fintanto che non causa alcun problema di codice. Inietti la dipendenza ma non creare l'interfaccia (e nessun file XML di configurazione pazzo!) Come puoi farlo in seguito senza problemi.

In realtà, nel mio progetto attuale ci sono quattro classi (su centinaia), che ho deciso di escludere da DI poiché sono classi semplici utilizzate in troppi luoghi, compresi gli oggetti dati.

Un altro svantaggio della maggior parte dei framework DI è il sovraccarico di runtime. Questo può essere spostato in fase di compilazione (per Java, c'è Dagger , non ho idea di altre lingue).

Ancora un altro svantaggio è che la magia sta succedendo ovunque, che può essere regolata verso il basso (ad es. ho disabilitato la creazione del proxy quando uso Guice).

    
risposta data 01.06.2018 - 11:16
fonte
2

Il mio più grande cruccio a proposito di DI è già stato citato in poche risposte in un certo senso, ma mi dilungherò un po 'qui. DI (come è fatto per lo più oggi, con contenitori, ecc.), Davvero, fa male la leggibilità del codice. E la leggibilità del codice è probabilmente la ragione alla base della maggior parte delle innovazioni di programmazione odierne. Come qualcuno ha detto, scrivere codice è facile. Il codice di lettura è difficile. Ma è anche estremamente importante, a meno che tu non stia scrivendo una sorta di minuscolo programma di utilità "usa e getta"

Il problema con DI a questo proposito è che è opaco. Il contenitore è una scatola nera. Gli oggetti appaiono semplicemente da qualche parte e non hai idea - chi li ha costruiti e quando? Cosa è stato passato al costruttore? Con chi condivido questa istanza? Chi lo sa ...

E quando lavori principalmente con le interfacce, tutte le funzionalità di "go to definition" del tuo IDE vanno in fumo. È terribilmente difficile tracciare il flusso del programma senza eseguirlo e basta passare a vedere QUALE implementazione dell'interfaccia è stata utilizzata in QUESTO punto particolare. E occasionalmente c'è qualche ostacolo tecnico che ti impedisce di fare un passo. E anche se è possibile, se si tratta di attraversare le viscere contorte del contenitore DI, l'intera faccenda diventa rapidamente un esercizio di frustrazione.

Per lavorare in modo efficiente con un pezzo di codice che utilizzava DI, devi conoscerlo e già sapere cosa va dove.

    
risposta data 03.06.2018 - 21:15
fonte
-4

Devo dire che, a mio parere, l'intera nozione di dipendenza da iniezione è sopravvalutata.

DI è l'equivalente moderno dei valori globali. Le cose che stai iniettando sono singoletti globali e oggetti di puro codice, altrimenti non potresti iniettarle. La maggior parte degli usi di DI sono obbligati a utilizzare una determinata libreria (JPA, Spring Data, ecc.). Per la maggior parte, DI fornisce l'ambiente perfetto per nutrire e coltivare gli spaghetti.

In tutta onestà, il modo più semplice per testare una classe è assicurarsi che tutte le dipendenze siano create in un metodo che può essere sovrascritto. Quindi crea una classe di test derivata dalla classe effettiva e sovrascrivi quel metodo.

Quindi istanziate la classe Test e testate tutti i suoi metodi. Questo non sarà chiaro ad alcuni di voi - i metodi che state testando sono quelli appartenenti alla classe in esame. E tutti questi test di metodo si verificano in un singolo file di classe, la classe di test dell'unità associata alla classe sottoposta a test. Qui non c'è un sovraccarico - questo è il modo in cui funziona il test delle unità.

Nel codice, questo concetto è simile a questo ...

class ClassUnderTest {

   protected final ADependency;
   protected final AnotherDependency;

   // Call from a factory or use an initializer 
   public void initializeDependencies() {
      aDependency = new ADependency();
      anotherDependency = new AnotherDependency();
   }
}

class TestClassUnderTest extends ClassUnderTest {

    @Override
    public void initializeDependencies() {
      aDependency = new MockitoObject();
      anotherDependency = new MockitoObject();
    }

    // Unit tests go here...
    // Unit tests call base class methods
}

Il risultato è esattamente equivalente all'utilizzo di DI - ovvero, ClassUnderTest è configurato per il test.

Le uniche differenze sono che questo codice è completamente conciso, completamente incapsulato, più facile da codificare, più facile da capire, più veloce, utilizza meno memoria, non richiede una configurazione alternativa, non richiede alcun framework, non sarà mai la causa di una traccia di stack di 4 pagine (WTF!) che include esattamente le classi ZERO (0) che hai scritto, ed è completamente ovvia per chiunque abbia anche la minima conoscenza OO, dal principiante al Guru (si penserebbe, ma si sbaglia).

Detto questo, ovviamente non possiamo usarlo - è troppo ovvio e non abbastanza alla moda.

Alla fine della giornata, però, la mia più grande preoccupazione per DI è quella dei progetti che ho visto fallire miseramente, tutti loro sono stati enormi basi di codice dove DI era la colla che tiene tutto insieme. DI non è un'architettura - è davvero rilevante solo in una manciata di situazioni, la maggior parte delle quali sono forzate a te per usare un'altra libreria (JPA, Spring Data, ecc.). Per la maggior parte, in una base di codice ben progettata, la maggior parte degli usi di DI si verificherebbe a un livello inferiore in cui si svolgono le attività quotidiane di sviluppo.

    
risposta data 30.05.2018 - 05:43
fonte
-6

In realtà la tua domanda si riduce a "Il test dell'unità è sbagliato?"

Il 99% delle classi alternative da iniettare saranno delle prese che abilitano il test delle unità.

Se esegui test unitari senza DI, hai il problema di come far sì che le classi usino dati derisi o un servizio deriso. Diciamo "parte della logica" in quanto potresti non separarlo in servizi.

Ci sono modi alternativi per farlo, ma DI è buono e flessibile. Una volta che lo hai provato, sei praticamente obbligato a usarlo ovunque, poiché hai bisogno di un altro pezzo di codice, anche se è il cosiddetto 'DI povero' che istanzia i tipi concreti.

È difficile immaginare uno svantaggio così grave che il vantaggio dei test unitari sia sopraffatto.

    
risposta data 29.05.2018 - 13:36
fonte

Leggi altre domande sui tag