Dependency Injection e Inversion of Control senza religione 🔌
Dependency Injection viene spesso raccontata come una liturgia fatta di container, annotation, reflection e altri modi fantasiosi di complicare ciò che poteva essere un costruttore con due parametri. Conviene ripartire dall’idea più importante: prima della DI viene la Inversion of Control.
Inversion of Control: chi controlla il flusso? 🧠
La IoC è il principio per cui il controllo del flusso non resta tutto nelle mani del tuo codice applicativo. Invece di chiamare ogni dipendenza direttamente e decidere ogni passo internamente, deleghi parte del controllo a un framework, a un runtime, a una callback o a un orchestratore.
Esempi quotidiani:
- un web server invoca il tuo handler
- un test runner esegue i tuoi test
- un consumer di eventi riceve messaggi dal broker
Il principio, riassunto brutalmente, è: don’t call us, we’ll call you.
Dependency Injection: rendere esplicite le collaborazioni 🪛
La Dependency Injection è un modo pratico per applicare IoC alle dipendenze di un componente. Invece di crearle dentro il componente, le ricevi dall’esterno.
Vantaggi immediati:
- dipendenze più visibili
- test più semplici
- sostituibilità di infrastruttura e collaboratori
- meno accoppiamento ai dettagli concreti
Il punto non è l’astrazione in sé. Il punto è evitare che il dominio si costruisca da solo il proprio database, il proprio logger, il proprio client HTTP e magari anche la propria crisi esistenziale.
Constructor injection: il default sano 🏗️
La forma più chiara e noiosa di DI è spesso la migliore: constructor injection.
|
|
Qui le dipendenze sono esplicite, leggibili e facili da sostituire nei test. Nessuna magia. Nessun santino del container.
Interfacce piccole, non interfacce decorative ✂️
La DI funziona bene quando i contratti sono piccoli e coerenti. Se definisci un’interfaccia da quindici metodi solo per poter dire che hai fatto dependency injection, stai spostando il problema, non risolvendolo.
Buona regola:
- definisci interfacce dove c’è vera variabilità
- tienile vicine al chiamante o al dominio che le usa
- non introdurle per puro folclore architetturale
In Go questo è ancora più importante: interfacce piccole e locali tendono a funzionare meglio di gerarchie “enterprise” trapiantate a forza.
Service Locator: il cugino elegante del globale tossico 🚫
Il Service Locator viene spesso venduto come soluzione elegante. In pratica, molto spesso è uno stato globale con un vestito migliore.
Problemi tipici:
- le dipendenze reali non sono visibili dalla firma
- capire cosa usa un componente richiede seguire registrazioni e wiring sparsi
- i test diventano più fragili e dipendono dal setup globale
Se per capire di cosa ha bisogno un servizio devo leggere mezzo bootstrap applicativo, non sto guardando buon design. Sto facendo spelunking.
DI container: utili, ma non gratis 🧰
Un container può avere senso in applicazioni grandi o molto modulari, soprattutto quando il wiring diventa ripetitivo. Però non è gratuito:
- nasconde il grafo delle dipendenze
- sposta errori da compile time a runtime
- rende più opaco l’avvio dell’applicazione
Se la tua applicazione ha dieci dipendenze esplicite e un main leggibile, probabilmente hai già tutto quello che ti serve. Un container non è automaticamente maturità. A volte è solo decorazione industriale.
Quando la DI è davvero utile 🎯
La DI porta valore soprattutto quando:
- vuoi isolare il dominio dall’infrastruttura: repository, store, gateway e simili non dovrebbero essere costruiti dentro il dominio stesso
- hai collaboratori sostituibili: clock di sistema, client HTTP, notificatori, generatori di ID, storage provider
- vuoi test veloci e mirati: con dipendenze iniettate puoi sostituire l’I/O reale con un double leggero, senza avviare l’intera applicazione
- alcune dipendenze cambiano più spesso di altre: il meccanismo di notifica, il provider di storage, il sistema di autenticazione sono buoni candidati
Il segnale più affidabile che vale la pena iniettare una dipendenza è semplice: ha senso averne un’implementazione diversa in test rispetto a produzione? Se la risposta è sì, iniettarla. Se no, probabilmente va bene usarla direttamente, e la firma del codice ne guadagna in chiarezza.
Non serve per ogni funzione, struct o package. Un servizio che calcola l’IVA non ha bisogno di un’interfaccia per la moltiplicazione. Se non c’è variabilità reale, una dipendenza concreta è spesso più leggibile e altrettanto corretta.
Segnali che stai esagerando 🚨
Quando la DI smette di aiutare e inizia a costare, si riconosce abbastanza bene:
- interfacce create prima ancora di avere un secondo implementatore plausibile: una
UserRepositorycon un solo implementatore concreto non ha bisogno di un contratto separato fin dal giorno uno - costruttori che assemblano metà sistema solo per restituire un wrapper sottile: se il grafo delle dipendenze cresce più velocemente della logica, qualcosa non torna
- container ovunque, ma responsabilità poco chiare: il container non è un sostituto per un design ragionevole; è un modo per gestire il wiring, non per evitare di pensarci
- doppioni, mock e boilerplate che costano più del problema iniziale: se aggiungere una feature richiede di aggiornare tre mock, due interfacce e un file di registration, l’overhead ha superato il beneficio
La DI è utile perché rende esplicite le dipendenze. Se il risultato è invece nasconderle dietro uno strato di magia, il meccanismo si è inceppato.
In sintesi 🧾
La Inversion of Control è il concetto. La Dependency Injection è una tecnica utile per applicarlo alle dipendenze. Il criterio non è la purezza ideologica, ma la riduzione dell’accoppiamento e del costo del cambiamento.
Se vuoi il seguito naturale, abbina questa guida ai principi di software design, a S.O.L.I.D. e alla guida sugli anti-pattern di design.