Dependency Hell — una situazione in cui il gestore di pacchetti non riesce a risolvere conflitti di versioni di librerie in un progetto. Nello sviluppo mobile, Dependency Hell è particolarmente doloroso: Gradle su Android e CocoaPods/SPM su iOS affrontano spesso conflitti transitivi. Secondo un rapporto di Sonatype (2024), il numero medio di dipendenze dirette in un progetto mobile supera 80, e quelle transitive — più di 400, ciascuna richiedente compatibilità di versioni.
Punti chiave
Dependency Hell è un termine che descrive una situazione in cui il sistema di gestione delle dipendenze non riesce a risolvere un conflitto di versioni tra librerie. Il progetto richiede la libreria A versione 1.x e la libreria B versione 2.x, ma A dipende da C versione 1.0, mentre B dipende da C versione 2.0, e C:1.0 e C:2.0 sono incompatibili.
Il problema è comune in tutti gli ecosistemi con gestori di pacchetti. In Android — conflitti Gradle tra support library e AndroidX. In iOS — conflitti CocoaPods tra diverse versioni di Alamofire. In Node.js — conflitti di peer dependency di npm. In Python — errori di risoluzione di pip.
I gestori di dipendenze moderni (npm v7+, Gradle 7+, SwiftPM) hanno migliorato gli algoritmi di risoluzione, ma l'eliminazione completa dei conflitti è impossibile con centinaia di dipendenze transitive. Dependency Hell è passato dalla categoria “errore di compilazione” alla categoria “gestione del rischio”.
Diamond dependency — il caso classico. La libreria A dipende da D:1.0, la libreria B dipende da D:2.0. Se A e B sono usate insieme, il gestore di pacchetti deve decidere quale versione di D installare. Nella maggior parte dei casi, viene selezionata la versione massima (2.0), ma se A non è compatibile con D:2.0 — il conflitto è irrisolvibile.
Version conflict — una discrepanza esplicita dei requisiti. A richiede Logging >=2.0, B richiede Logging <2.0. Il gestore non può soddisfare entrambe le condizioni. Peer dependency conflict — il plugin A richiede React 17, ma il progetto usa React 18 con modifiche sostanziali. npm mostra un avviso, ma l'installazione procede — il comportamento diventa imprevedibile.
Transitive dependency hell — quando una dipendenza non è diretta ma indiretta. Lo sviluppatore non sa che la libreria A dipende da B, e B dipende da C. Gradle Dependency Tree — uno strumento per visualizzare l'intera catena di dipendenze, mostrando da dove proviene la libreria in conflitto.
Circular dependency — A dipende da B, e B dipende da A. I gestori moderni (Gradle, npm) bloccano le dipendenze circolari in fase di compilazione. Soluzione — estrarre un modulo comune C da cui dipendono sia A che B, rompendo il ciclo.
Crescita del numero di librerie — il prerequisito principale. Ogni modulo aggiunge dipendenze dirette e transitive. In un progetto Android con Jetpack Compose, Firebase, Retrofit e Coil, il numero di dipendenze transitive supera facilmente 500. Ogni nuova libreria è un conflitto potenziale.
Aggiornamenti non sincronizzati — i team aggiornano le librerie in momenti diversi. Il backend aggiorna Jackson a 2.15, il team Analytics usa 2.12. Integrando i moduli, sorge un conflitto. Soluzione — versioni centralizzate (Bill of Materials) in un file BOM di Gradle o catalogo di versioni.
Versioni diverse della stessa libreria — la situazione classica: il modulo A usa OkHttp 3.12, il modulo B usa OkHttp 4.0. Se l'aggiornamento a 4.0 rompe il modulo A, il progetto rimane bloccato su due versioni, il che può portare a conflitti di classpath in Java o simboli duplicati in iOS.
Gradle Dependency Tree — il comando `gradle dependencies` produce l'albero completo delle dipendenze con l'indicazione dei conflitti. La versione risolta mostra quale versione Gradle ha selezionato, e le versioni in conflitto sono contrassegnate con frecce. Esempio: `com.squareup.okhttp3:okhttp -> 4.9.3 (*)` — versione risolta, (*) — duplicazione.
npm ls — un comando simile per Node.js. Il flag `--all` mostra l'albero completo. I conflitti di peer dependency vengono mostrati con avvisi. SwiftPM Graph — `swift package show-dependencies` mostra il grafo delle dipendenze per progetti iOS, inclusi rami e revisioni.
Dependency Analysis Plugin — un plugin Gradle di Autonomy che trova dipendenze inutilizzate e conflitti. Ben Manes Versions Plugin — controlla quali dipendenze sono obsolete e mostra gli aggiornamenti disponibili. Entrambi gli strumenti automatizzano il controllo di routine della compatibilità.
// Conflitto: il modulo A necessita di okhttp 3.x, il modulo B necessita di okhttp 4.x
dependencies {
implementation("com.example:module-a:1.0") // -> okhttp 3.12
implementation("com.example:module-b:2.0") // -> okhttp 4.0
}
// Soluzione: forzare una versione specifica
configurations.all {
resolutionStrategy {
force "com.squareup.okhttp3:okhttp:4.9.3"
}
}
Version Catalog (Gradle 7+) — dichiarazione centralizzata delle versioni in un file TOML. Tutti i moduli usano le stesse versioni di librerie. Esempio: il file `libs.versions.toml` contiene `okhttp = “4.9.3”`, e tutti i moduli fanno riferimento a questo catalogo. I conflitti di versione tra moduli vengono eliminati.
Bill of Materials (Spring BOM) — un concetto Maven in cui vengono specificate versioni compatibili di librerie. Il team di Google Android usa Compose BOM per le librerie Jetpack. Utilizzando un BOM, ottieni la garanzia che tutte le versioni di Compose siano compatibili tra loro.
Renovate e Dependabot — creatori automatici di PR per gli aggiornamenti delle dipendenze. Renovate raggruppa aggiornamenti compatibili, verifica le modifiche sostanziali tramite immagini Docker. Dependabot è una soluzione integrata di GitHub che aggiorna le dipendenze e verifica la compatibilità tramite CI.
Semantic Versioning — usa caret `^1.2.3` per aggiornamenti di patch/minor e tilde `~1.2.3` solo per patch. Ma anche semver non garantisce la compatibilità — le violazioni reali di semver si verificano nel 15% dei casi (secondo uno studio dell'Università del Lussemburgo, 2024). I lock file fissano la versione esatta che ha superato i test.
Minimizzare le dipendenze — ogni libreria deve essere giustificata. Se puoi implementare la funzionalità in 20 righe del tuo codice — non aggiungere una libreria. Esempio: invece di una libreria per la formattazione delle date (4 dipendenze transitive), usa gli strumenti integrati della piattaforma. La regola del “budget delle dipendenze” — non più di 50 dipendenze dirette per progetto.
Aggiornamenti regolari — aggiorna le dipendenze a piccoli passi, non una volta all'anno. Dependabot crea un PR per ogni aggiornamento. CI dovrebbe eseguire la suite completa di test. DevContainer — un ambiente di sviluppo unificato in cui le versioni delle dipendenze corrispondono a quelle di produzione, eliminando i conflitti tra ambienti.
Domande frequenti
Prima, esegui `gradle dependencies` (Gradle), `npm ls` (Node.js) o `swift package show-dependencies` (SwiftPM). Trova la libreria in conflitto. Tre soluzioni: forzare una versione tramite resolutionStrategy, escludere la dipendenza transitiva (`exclude group:`), o aggiornare una delle librerie in conflitto a una versione compatibile.
Version Catalog (libs.versions.toml) — un'unica fonte di verità per le versioni di tutte le librerie. Tutti i moduli del progetto fanno riferimento a un catalogo. Quando una libreria viene aggiornata, la versione cambia in un unico posto. Questo evita la situazione in cui due moduli usano versioni diverse della stessa libreria.
Le dipendenze transitive sono librerie che una dipendenza diretta si porta dietro. Lo sviluppatore spesso non ne è a conoscenza. Il pericolo: una dipendenza transitiva può entrare in conflitto con un'altra dipendenza diretta. La soluzione è controllare regolarmente l'albero delle dipendenze e includere solo librerie con un minimo di dipendenze transitive.
Non necessariamente ogni sprint, ma regolarmente — sì. Raccomandazione: una volta al mese, esegui Dependabot o Renovate per creare PR. Le patch di sicurezza critiche dovrebbero essere aggiornate entro una settimana. Gli aggiornamenti minori — entro uno sprint normale. Gli aggiornamenti maggiori richiedono una valutazione separata delle modifiche sostanziali.
Una libreria non mantenuta è un rischio per la sicurezza e la compatibilità. Strategia: trova un'alternativa con una community attiva (stelle GitHub, data dell'ultimo commit), pianifica la migrazione tramite astrazione (Interface/Protocol), sostituisci la libreria in 2–3 sprint. Se non c'è alternativa — fai un fork del repository e mantieni la versione all'interno del team.
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