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é Foo
si 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 Foo
indipendentemente 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 .