Quanto costa troppa iniezione di dipendenza?


38

Lavoro in un progetto che utilizza (Spring) Dependency Injection per letteralmente tutto ciò che è una dipendenza di una classe. Siamo a un punto in cui il file di configurazione di Spring è cresciuto a circa 4000 righe. Non molto tempo fa ho visto uno dei discorsi di zio Bob su YouTube (purtroppo non sono riuscito a trovare il link) in cui mi consiglia di iniettare solo un paio di dipendenze centrali (ad esempio fabbriche, database, ...) nel componente principale da cui poi faranno essere distribuito.

I vantaggi di questo approccio sono il disaccoppiamento del framework DI dalla maggior parte dell'applicazione e inoltre rende la configurazione Spring più pulita poiché le fabbriche conterranno molto di più di ciò che era nella configurazione prima. Al contrario, ciò comporterà la diffusione della logica di creazione tra molte classi di fabbrica e i test potrebbero diventare più difficili.

Quindi la mia domanda è davvero quali altri vantaggi o svantaggi vedi nell'uno o nell'altro approccio. Ci sono delle migliori pratiche? Grazie mille per le tue risposte!


3
Avere un file di configurazione a 4000 righe non è colpa dell'iniezione di dipendenza ... ci sono molti modi per risolverlo: modularizzare il file in più file più piccoli, passare all'iniezione basata su annotazioni, utilizzare i file JavaConfig anziché xml. Fondamentalmente, il problema che si verifica è un errore nella gestione della complessità e delle dimensioni delle esigenze di iniezione della dipendenza dell'applicazione.
Maybe_Factor

1
@Maybe_Factor TL; DR ha suddiviso il progetto in componenti più piccoli.
Walfrat,

Risposte:


44

Come sempre, Depends ™. La risposta dipende dal problema che si sta cercando di risolvere. In questa risposta, proverò ad affrontare alcune forze motivanti comuni:

Favorire basi di codice più piccole

Se hai 4.000 righe di codice di configurazione Spring, suppongo che la base di codice abbia migliaia di classi.

Non è quasi un problema che puoi affrontare dopo il fatto, ma come regola pratica, tendo a preferire applicazioni più piccole, con basi di codice più piccole. Se ti interessa la progettazione basata su dominio , ad esempio, potresti creare una base di codice per contesto limitato.

Sto basando questo consiglio sulla mia esperienza limitata, dal momento che ho scritto codice line-of-business basato sul web per la maggior parte della mia carriera. Potrei immaginare che se stai sviluppando un'applicazione desktop, un sistema incorporato o altro, le cose sono più difficili da separare.

Mentre mi rendo conto che questo primo consiglio è facilmente il meno pratico, credo anche che sia il più importante, ed è per questo che lo includo. La complessità del codice varia in modo non lineare (possibilmente in modo esponenziale) con la dimensione della base di codice.

Favor Pure DI

Mentre mi rendo ancora conto che questa domanda presenta una situazione esistente, raccomando Pure DI . Non usare un contenitore DI, ma se lo fai, almeno utilizzalo per implementare la composizione basata su convenzioni .

Non ho alcuna esperienza pratica con Spring, ma suppongo che dal file di configurazione sia implicito un file XML.

Configurare le dipendenze usando XML è il peggiore di entrambi i mondi. Innanzitutto, perdi la sicurezza del tipo in fase di compilazione, ma non guadagni nulla. Un file di configurazione XML può essere facilmente grande quanto il codice che tenta di sostituire.

Rispetto al problema che pretende di affrontare, i file di configurazione dell'iniezione di dipendenza occupano la posizione sbagliata sull'orologio della complessità della configurazione .

Il caso dell'iniezione di dipendenza a grana grossa

Posso fare un caso per l'iniezione di dipendenza a grana grossa. Posso anche fare un caso per l'iniezione di dipendenza a grana fine (vedere la sezione successiva).

Se si iniettano solo alcune dipendenze "centrali", la maggior parte delle classi potrebbe apparire così:

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

Questo è ancora adatta Design Patterns 's composizione oggetto favore nel ereditarietà di classe , perché Foosi compone Bar. Dal punto di vista della manutenibilità, questo potrebbe ancora essere considerato mantenibile, perché se è necessario modificare la composizione, è sufficiente modificare il codice sorgente perFoo .

Questo è difficilmente meno gestibile dell'iniezione di dipendenza. In effetti, direi che è più semplice modificare direttamente la classe che utilizza Bar, invece di seguire la direzione indiretta inerente all'iniezione di dipendenza.

Nella prima edizione del mio libro su Dependency Injection , faccio la distinzione tra dipendenze volatili e stabili.

Le dipendenze volatili sono quelle dipendenze che dovresti considerare di iniettare. Loro includono

  • Dipendenze che devono essere riconfigurabili dopo la compilazione
  • Dipendenze sviluppate in parallelo da un'altra squadra
  • Dipendenze con comportamento non deterministico o comportamento con effetti collaterali

Le dipendenze stabili, d'altra parte, sono dipendenze che si comportano in modo ben definito. In un certo senso, si potrebbe sostenere che questa distinzione è il caso dell'iniezione di dipendenza a grana grossa, anche se devo ammettere che non me ne sono reso conto completamente quando ho scritto il libro.

Dal punto di vista dei test, tuttavia, ciò rende più difficile il test unitario. Non è più possibile effettuare unit test Fooindipendentemente da Bar. Come spiega JB Rainsberger , i test di integrazione soffrono di un'esplosione combinatoria di complessità. Dovrai letteralmente scrivere decine di migliaia di casi di test se vuoi coprire tutti i percorsi attraverso un'integrazione di persino 4-5 classi.

Il contrario è che spesso il tuo compito non è programmare una classe. Il tuo compito è sviluppare un sistema che risolva alcuni problemi specifici. Questa è la motivazione alla base dello sviluppo guidato dal comportamento (BDD).

Un altro punto di vista su questo è presentato da DHH, che afferma che TDD porta a danni alla progettazione indotti dal test . Favorisce inoltre i test di integrazione a grana grossa.

Se prendi questa prospettiva sullo sviluppo del software, allora l'iniezione di dipendenza a grana grossa ha senso.

Il caso dell'iniezione di dipendenza a grana fine

L'iniezione di dipendenza a grana fine, d'altra parte, potrebbe essere descritta come iniettare tutte le cose!

La mia principale preoccupazione per l'iniezione di dipendenza a grana grossa è la critica espressa da JB Rainsberger. Non è possibile coprire tutti i percorsi di codice con i test di integrazione, poiché è necessario scrivere letteralmente migliaia o decine di migliaia di casi di test per coprire tutti i percorsi di codice.

I sostenitori di BDD si opporranno all'argomento secondo cui non è necessario coprire tutti i percorsi del codice con i test. Devi solo coprire quelli che producono valore commerciale.

Nella mia esperienza, tuttavia, tutti i percorsi di codice "esotici" verranno eseguiti anche in una distribuzione ad alto volume e, se non verificati, molti di questi avranno difetti e causeranno eccezioni di runtime (spesso eccezioni a riferimento null).

Questo mi ha portato a favorire l'iniezione di dipendenza a grana fine, perché mi consente di testare gli invarianti di tutti gli oggetti in isolamento.

Favorire la programmazione funzionale

Mentre mi sposto verso l'iniezione di dipendenza a grana fine, ho spostato la mia enfasi sulla programmazione funzionale, tra le altre ragioni perché è intrinsecamente testabile .

Più passi al codice SOLIDO, più diventa funzionale . Prima o poi, puoi anche fare il grande passo. L'architettura funzionale è l'architettura Ports and Adapters e anche l' iniezione di dipendenza è un tentativo e Ports and Adapters . La differenza, tuttavia, è che un linguaggio come Haskell applica tale architettura attraverso il suo sistema di tipi.

Favorire la programmazione funzionale tipizzata staticamente

A questo punto, ho essenzialmente rinunciato alla programmazione orientata agli oggetti (OOP), sebbene molti dei problemi di OOP siano intrinsecamente accoppiati a linguaggi tradizionali come Java e C # più del concetto stesso.

Il problema con i linguaggi OOP tradizionali è che è quasi impossibile evitare il problema dell'esplosione combinatoria, che, non testato, porta a eccezioni di runtime. I linguaggi digitati staticamente come Haskell e F #, d'altra parte, consentono di codificare molti punti di decisione nel sistema di tipi. Ciò significa che invece di dover scrivere migliaia di test, il compilatore ti dirà semplicemente se hai gestito tutti i possibili percorsi del codice (in una certa misura; non è un proiettile d'argento).

Inoltre, l' iniezione di dipendenza non è funzionale . La vera programmazione funzionale deve respingere l'intera nozione di dipendenze . Il risultato è un codice più semplice.

Sommario

Se costretto a lavorare con C #, preferisco l'iniezione di dipendenza a grana fine perché mi permette di coprire l'intera base di codice con un numero gestibile di casi di test.

Alla fine, la mia motivazione è il feedback rapido. Tuttavia, i test unitari non sono l'unico modo per ottenere feedback .


1
Mi piace molto questa risposta, e in particolare questa affermazione usata nel contesto di Spring: "Configurare le dipendenze usando XML è il peggiore di entrambi i mondi. Innanzitutto, perdi la sicurezza del tipo di compilazione, ma non ottieni nulla. Una configurazione XML il file può essere facilmente grande quanto il codice che tenta di sostituire. "
Thomas Carlisle,

@ThomasCarlisle non era il punto di configurazione XML che non è necessario toccare il codice (anche compilare) per cambiarlo? Non l'ho mai usato (o appena) a causa delle due cose che Mark ha menzionato, ma in cambio ottieni qualcosa.
El Mac,

1

Enormi classi di installazione DI sono un problema. Ma pensa al codice che sostituisce.

È necessario creare un'istanza di tutti quei servizi e quant'altro. Il codice per farlo sarebbe o nel app.main()punto di partenza e iniettato manualmente, o strettamente accoppiato come this.myService = new MyService();all'interno delle classi.

Riduci le dimensioni della tua classe di installazione suddividendola in più classi di configurazione e chiamandole dal punto di inizio del programma. vale a dire.

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

Non è necessario fare riferimento a service1 o 2 se non per passarli in altri metodi di configurazione ci.

È meglio che tentare di recuperarli dal contenitore poiché impone l'ordine di chiamare le configurazioni.


1
Per coloro che vogliono approfondire questo concetto (costruendo il tuo albero di classe all'inizio del programma), il termine migliore da cercare è "root della composizione"
e_i_pi,

@Ewan cos'è quella civariabile?
Superjos,

la tua classe di installazione ci con metodi statici
Ewan

urg dovrebbe essere di. continuo a pensare 'container'
Ewan
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.