DIP (Dependency Inversion Principle) — il quinto principio SOLID che definisce le regole per costruire dipendenze tra moduli: i moduli di alto livello non devono dipendere da moduli di basso livello, entrambi devono dipendere da astrazioni. Le astrazioni non devono dipendere dai dettagli — i dettagli devono dipendere dalle astrazioni. Questo principio, descritto da Robert Martin in Clean Architecture (2017), è alla base dell'architettura a basso accoppiamento. Secondo questo libro, il principio di inversione delle dipendenze elimina gli accoppiamenti rigidi tra i livelli dell'applicazione.
Punti chiave
DIP (Dependency Inversion Principle) è il principio di inversione delle dipendenze che inverte la visione tradizionale sulla direzione delle dipendenze tra moduli. I moduli di alto livello (logica di business) non devono dipendere direttamente da moduli di basso livello (database, rete, UI). Invece, entrambi i livelli dipendono da astrazioni definite nel modulo di alto livello.
La formulazione formale del DIP include due regole: A — i moduli di alto livello non devono dipendere da moduli di basso livello, entrambi devono dipendere da astrazioni. B — le astrazioni non devono dipendere dai dettagli, i dettagli devono dipendere dalle astrazioni. La seconda regola deriva dalla prima: se un'astrazione dipende dai dettagli, non può essere una base stabile per un modulo di alto livello.
Senza DIP, un'architettura tipica si presenta così: BusinessLogic → DatabaseRepository — la logica di business dipende direttamente da un repository concreto. Con DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic non conosce l'esistenza di DatabaseRepository, conosce solo l'interfaccia DatabaseService, che viene implementata al di fuori della logica di business.
Inversione significa che il flusso di controllo e il flusso delle dipendenze vanno in direzioni opposte. Il flusso di controllo va dall'alto verso il basso: UI → ViewModel → UseCase → Repository. Il flusso delle dipendenze va dal basso verso l'alto: Repository implementa un'interfaccia definita in UseCase. Repository (basso livello) dipende da UseCase (alto livello).
Questa inversione è la differenza chiave tra DIP e la separazione ordinaria in livelli. Nell'architettura tradizionale a livelli, ogni livello dipende dal livello sottostante. Nell'architettura con DIP, tutti i livelli dipendono da astrazioni, mentre l'implementazione di queste astrazioni risiede nel livello di infrastruttura, che viene “collegato” ai livelli superiori tramite meccanismi DI.
Il meccanismo DIP viene implementato definendo astrazioni nei moduli di alto livello e realizzandole nei moduli di basso livello. Il modulo di alto livello dichiara un'interfaccia per la funzionalità di cui ha bisogno. Il modulo di basso livello implementa questa interfaccia. Il cablaggio (wiring) avviene nella radice di composizione dell'applicazione.
Il processo di introduzione del DIP in codice esistente: estrarre un'interfaccia per il modulo di basso livello, spostare questa interfaccia nel modulo di alto livello (o in un livello di astrazione separato), riscrivere la dipendenza del modulo di alto livello per utilizzare l'interfaccia, far implementare questa interfaccia al modulo di basso livello. Dopo questi passaggi, la direzione della dipendenza è stata invertita.
DIP richiede un meccanismo di radice di composizione — un punto nell'applicazione dove tutte le dipendenze vengono create e collegate tra loro. In Android è Application.get() o il componente Hilt, in iOS — AppDelegate o SceneDelegate. La radice di composizione è l'unico posto dove il codice conosce le implementazioni concrete.
DIP crea confini architetturali tra i livelli dell'applicazione. Quando ViewModel dipende dall'interfaccia UserRepository, si forma un confine tra il livello di presentazione e quello di dominio: ViewModel (presentazione) non sa da dove provengono i dati. Questo confine permette di cambiare l'implementazione di UserRepository (Room → REST → Mock) senza influenzare ViewModel. Più confini di questo tipo, più l'applicazione è resiliente ai cambiamenti di framework e librerie.
Nell'architettura Android raccomandata da Google, DIP viene implementato tramite UseCase che risiedono nel livello di dominio e dipendono da interfacce Repository. RepositoryImpl si trovano nel livello dati e implementano queste interfacce. Il livello di presentazione (ViewModel) dipende da UseCase. La direzione delle dipendenze va dalla presentazione al dominio, dal dominio ai dati — ma nessun livello conosce le implementazioni concrete di un altro livello.
DIP e DI sono spesso confusi, ma sono concetti diversi. DIP è un principio architetturale (COSA fare: dipendere da astrazioni). DI è un pattern implementativo (COME farlo: passare dipendenze tramite costruttore). DIP risponde alla domanda “su cosa devono basarsi i moduli?”, DI risponde a “come gli oggetti ottengono le loro dipendenze?”.
Dependency Injection è un modo per iniettare dipendenze in un oggetto tramite costruttore, metodo o proprietà. Quando una classe Kotlin riceve un'interfaccia Repository tramite il suo costruttore — questo è DI. Il fatto che una classe ViewModel dipenda dall'interfaccia Repository e non da un'implementazione concreta di RoomRepository — questo è DIP. DI è lo strumento, DIP è l'obiettivo.
Si può seguire DIP senza un framework DI: il cablaggio manuale delle dipendenze nella radice di composizione è anch'esso DI (DI manuale). Si può usare un framework DI (Dagger, Hilt, Koin) violando DIP: se ViewModel crea direttamente un oggetto Repository tramite new() — DIP è violato, anche se il framework è installato. DIP è una decisione architetturale, DI è un dettaglio tecnico.
Consideriamo un esempio Android di applicazione del DIP al livello dati. Senza DIP, un ViewModel crea direttamente una RoomDatabase e DAO. Con DIP — il ViewModel dipende dall'interfaccia UserRepository, e l'implementazione concreta RoomUserRepository viene fornita esternamente.
// L'astrazione appartiene al livello di dominio (alto livello)
interface UserRepository {
fun getUser(id: Int): User
}
// Il livello di dominio dipende solo dall'astrazione
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// L'implementazione nel livello dati dipende dall'astrazione del livello di dominio
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// Radice di composizione
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
Un esempio iOS con un Application Coordinator e un protocollo di navigazione:
// Astrazione di navigazione nel livello di dominio
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// ViewModel dipende dall'astrazione, non da UIKit
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (livello UIKit) implementa il protocollo del livello di dominio
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// Codice di navigazione UIKit
}
func navigateToLogin() {
// Codice di navigazione UIKit
}
}
Il punto chiave: AuthViewModel (dominio) non conosce l'esistenza di AppCoordinator (UIKit). Conosce solo il protocollo AuthNavigation. Se UIKit venisse sostituito da SwiftUI domani — AuthViewModel non richiede modifiche. DIP rende il livello di dominio indipendente dai framework e dalle librerie UI.
Hilt è lo strumento DI standard per Android, raccomandato da Google. È integrato in Jetpack, supporta ViewModel, Fragment, Service e altri componenti Android. Hilt automatizza la creazione della radice di composizione tramite le annotazioni @Module, @Provides, @Inject. Usare Hilt non garantisce il rispetto del DIP — l'interfaccia UserRepository deve essere definita nel livello di dominio, non nel livello dati.
Koin è un framework DI leggero per Kotlin senza generazione di codice o elaborazione di annotazioni. Il DSL di Koin (module, single, factory) è più facile da imparare, ma la verifica delle dipendenze avviene a runtime anziché in fase di compilazione. Koin è popolare nei progetti multipiattaforma (KMP) grazie al supporto iOS.
Dagger 2 è il predecessore di Hilt, ancora utilizzato in grandi progetti. Dagger genera codice DI in fase di compilazione, offrendo massime prestazioni e diagnosi degli errori in fase di build. Hilt è costruito su Dagger e fornisce un'API semplificata. Per i nuovi progetti, Google raccomanda Hilt come framework DI principale.
I moduli DI devono corrispondere ai livelli architetturali ed essere separati in DomainModule, DataModule, PresentationModule. DomainModule fornisce solo astrazioni e UseCase. DataModule fornisce implementazioni per le astrazioni. PresentationModule collega ViewModel a UseCase. Questa organizzazione garantisce che il livello di dominio rimanga indipendente dalle librerie di infrastruttura.
Durante la migrazione tra framework DI (ad esempio, da Koin a Hilt), la struttura di DomainModule non cambia — cambiano solo i metodi di cablaggio in DataModule e PresentationModule. DIP garantisce l'isolamento della logica di dominio, mentre il framework DI è un meccanismo tecnico di cablaggio.
Domande frequenti
DIP è necessario ai confini architetturali — tra i livelli dell'applicazione (dominio → dati, presentazione → dominio). All'interno di un singolo livello, DIP può essere eccessivo. Ad esempio, una classe utility StringFormatter all'interno del livello di dominio non richiede un'interfaccia — se non c'è motivo di sostituirla.
No. DIP è un principio: i moduli devono dipendere da astrazioni. DI è un pattern: un oggetto riceve dipendenze dall'esterno anziché crearle da sé. DI è un modo per implementare DIP, ma DIP può essere seguito senza DI (tramite fabbriche o service locator). DI senza DIP è possibile ma non ha valore architetturale.
Le interfacce appartengono al modulo che le usa, non al modulo che le implementa. UserRepository viene dichiarato nel livello di dominio e implementato nel livello dati. Questa è la regola chiave di DIP: il proprietario dell'astrazione è il consumatore, non il fornitore dell'implementazione.
DIP rende possibile il test su livelli isolati. Un ViewModel che dipende da UserRepository (interfaccia) può essere testato con un'implementazione mock senza database. Senza DIP, il ViewModel dipenderebbe da RoomUserRepository e richiederebbe la configurazione del database per ogni test. DIP + DI forniscono un completo isolamento dei moduli durante i test.
Hilt è la scelta standard per i progetti Android, raccomandata da Google. Koin è un'alternativa per i progetti Kotlin Multiplatform. Dagger 2 è per progetti esistenti dove la migrazione a Hilt non è giustificata. La scelta del framework non elimina la necessità di seguire DIP a livello architetturale.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche