La modularità è un principio in base al quale un'applicazione viene assemblata da moduli indipendenti, ciascuno responsabile di una singola funzionalità. Secondo Android Developers, la suddivisione in moduli accelera la compilazione grazie alla compilazione parallela e consente ai team di lavorare su diverse parti dell'applicazione in modo indipendente. L'architettura modulare è diventata lo standard per i grandi progetti mobile con decine di sviluppatori.
Punti chiave
La modularità è un modo di organizzare il codice in cui un'applicazione è composta da moduli debolmente accoppiati, ciascuno dei quali fornisce una funzionalità strettamente definita attraverso un'interfaccia pubblica. A differenza dell'architettura monolitica in cui tutte le classi risiedono in un unico progetto, l'approccio modulare suddivide il codice in unità di compilazione fisicamente indipendenti.
L'obiettivo principale della modularità è la gestione della complessità. Uno sviluppatore può concentrarsi su un modulo senza avere in mente l'intera base di codice. Ogni modulo ha la propria area di responsabilità e può essere sviluppato, testato e distribuito indipendentemente dagli altri. Questo è particolarmente prezioso in progetti con 10+ sviluppatori, dove il lavoro parallelo su un monolite porta a frequenti conflitti di unione.
È importante distinguere la modularità dall'architettura a strati. Gli strati (Presentation, Domain, Data) dividono il codice per criteri tecnici, mentre i moduli lo dividono per criteri funzionali. Un modulo “Profilo utente” può contenere i propri strati interni. In pratica, l'approccio modulare e l'architettura a strati sono combinati: ogni modulo ha la propria struttura a tre strati.
I moduli di funzionalità sono il tipo di modulo più popolare. Ogni schermata o gruppo di schermate correlate viene separato in un modulo proprio: Onboarding, Profile, Settings, Feed. Un modulo di funzionalità contiene tutto il necessario per il funzionamento della funzionalità: UI, logica di business, strato dati. I confini del modulo sono protetti — altre funzionalità non possono accedere alle sue classi interne.
I moduli core contengono l'infrastruttura comune: rete, database, analisi, sistema di design. Non dipendono dai moduli di funzionalità, ma i moduli di funzionalità dipendono da essi. Questa separazione garantisce che la modifica di un SDK di analisi non influisca sullo strato di rete, e viceversa. I moduli core sono riutilizzati tra le funzionalità senza duplicazione di codice.
I moduli condivisi contengono codice utilizzato da più funzionalità: modelli di dati, utilità, costanti, viste personalizzate. Il problema principale dei moduli condivisi è il rischio di trasformarsi in un deposito (“modulo miscellaneo”) dove si accumula codice eterogeneo nel tempo. Regola: un modulo condiviso deve avere un tema chiaro, ad esempio “shared-ui” o “shared-models”.
Su Android, i moduli condivisi sono spesso separati in librerie con il prefisso lib: lib-network, lib-database, lib-ui-components. Su iOS, la stessa funzione è svolta da Swift Packages interni all'interno di un Workspace. In pratica, i team limitano il numero di moduli condivisi a 3–5 per evitare di creare una rete di dipendenze eccessiva che complichi la compilazione.
Moduli di test separati consentono di eseguire i test solo per il modulo modificato senza eseguire l'intera suite di test. Questo riduce i tempi della pipeline CI/CD da ore a minuti. L'isolamento a livello di modulo garantisce SoC a livello di compilazione: un modulo dello strato di rete non può importare accidentalmente librerie UI nei suoi test.
Ogni modulo deve avere un'API pubblica chiaramente definita. Su Android, ciò si ottiene tramite modificatori di accesso e api vs implementation in Gradle. Su iOS, tramite modificatori di accesso public/internal e dipendenze gestite tramite Package.swift. Ridurre la visibilità al minimo necessario è una pratica chiave del design modulare.
Gradle supporta nativamente l'architettura modulare: ogni modulo è un'unità di compilazione separata con il proprio file build.gradle. I progetti Android utilizzano una combinazione di un modulo applicazione (app) e diversi moduli libreria. I moduli libreria non possono essere eseguiti come applicazione ma possono essere pubblicati come AAR in un repository.
Una caratteristica chiave di Gradle è la compilazione parallela di moduli indipendenti. Se i moduli A, B e C non dipendono l'uno dall'altro, Gradle li compila simultaneamente utilizzando tutti i core della CPU. In progetti con 20+ moduli, questo riduce una compilazione completa da 15 a 3–5 minuti. Le compilazioni incrementali di un modulo modificato richiedono secondi.
Gradle fornisce due tipi di dipendenze tra moduli: api (transitive) e implementation (non transitive). La differenza è criticamente importante per la modularità: implementation nasconde le dipendenze transitive dai consumatori del modulo. Se il modulo :profile utilizza :networking tramite implementation, i consumatori di :profile non conoscono :networking e non possono accedervi.
// settings.gradle — dichiarazione dei moduli
include ':app'
include ':feature:profile'
include ':feature:settings'
include ':core:network'
include ':core:database'
// build.gradle feature/profile — dipendenze del modulo
dependencies {
implementation project(':core:network')
implementation project(':core:database')
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0'
}
Il codice mostra la struttura di un progetto Android modulare. Settings.gradle elenca tutti i moduli, e il build.gradle di ogni modulo di funzionalità specifica solo i moduli core di cui ha bisogno. Il sistema di compilazione risolve automaticamente le dipendenze transitive e compila i moduli nell'ordine corretto.
Swift Package Manager (SPM) è lo strumento di modularità standard su iOS dal 2019. SPM consente di suddividere un'applicazione in Swift Packages, ciascuno dei quali può essere una libreria o un eseguibile. Un Package definisce i moduli (targets) e le loro dipendenze tramite Package.swift. SPM è integrato in Xcode e non richiede strumenti aggiuntivi.
CocoaPods rimane il gestore di dipendenze principale per librerie di terze parti. Podfile e Podspec definiscono la struttura modulare e CocoaPods genera un workspace con progetti pod separati. Per la modularità del proprio progetto, i team scelgono sempre più SPM perché è integrato in Xcode e non richiede installazione.
Nella modularità iOS, il controllo di accesso gioca un ruolo importante: public, package, internal, fileprivate e private. Un modulo pubblica solo i tipi che devono essere accessibili ad altri moduli. I dettagli di implementazione interna sono nascosti dietro i modificatori internal e private. Questo impedisce dipendenze nascoste tra i moduli.
// Package.swift — struttura modulare di un progetto iOS
let package = Package(
name: "MyApp",
platforms: [.iOS(SupportedPlatform.iOSVersion.v17)],
products: [
.library(name: "ProfileFeature", targets: ["ProfileFeature"]),
.library(name: "NetworkCore", targets: ["NetworkCore"]),
],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.0")
],
targets: [
.target(name: "ProfileFeature", dependencies: ["NetworkCore"]),
.target(name: "NetworkCore", dependencies: ["Alamofire"]),
]
)
Package.swift dichiara due prodotti libreria: ProfileFeature e NetworkCore. ProfileFeature dipende da NetworkCore ma non conosce l'esistenza di Alamofire — è nascosto all'interno di NetworkCore. Questo isolamento è un'applicazione diretta del SoC a livello di modulo: le modifiche al client HTTP non richiedono la ricompilazione di ProfileFeature.
Il principale vantaggio della modularità è la velocità di sviluppo. I team lavorano in parallelo su diversi moduli senza conflitti di codice. La pipeline CI/CD compila solo i moduli modificati ed esegue solo i loro test. Il tempo di feedback diminuisce e la frequenza di rilascio aumenta. Spotify, Uber e Airbnb hanno pubblicato casi di studio di migrazione all'architettura modulare con miglioramenti delle metriche di 2–3 volte.
Il secondo vantaggio è l'isolamento degli errori. Un bug nel modulo Profile non influisce sul modulo Payments se non ci sono dipendenze dirette tra loro. Questo è particolarmente importante nelle applicazioni con funzionalità ad alto rischio (pagamenti, dati medici), dove un errore in una schermata non correlata non dovrebbe bloccare il rilascio di funzionalità critiche.
La sfida principale è la gestione delle dipendenze. Con una progettazione inadeguata, emerge un grafo di moduli in cui la modifica di un modulo causa la ricompilazione a cascata di dozzine di altri. La soluzione è seguire la regola dell'aciclicità: il grafo delle dipendenze dei moduli deve essere un grafo aciclico diretto (DAG). Strumenti come Gradle Module Graph Assert aiutano a rilevare cicli al momento della compilazione.
La seconda sfida è il tempo di configurazione iniziale maggiore. Creare un'architettura modulare richiede più tempo nella fase di inizializzazione del progetto. I progetti piccoli con 1–3 sviluppatori potrebbero non trarre vantaggio dalla modularità, dedicando tempo a mantenere i confini dei moduli senza una reale necessità di parallelizzazione. La soluzione è iniziare con un monolite ed estrarre i moduli man mano che il team cresce.
L'approccio feature-first raggruppa i moduli per funzionalità: ogni schermata o gruppo di schermate diventa un modulo separato. L'approccio layer-first divide il codice per criteri tecnici: moduli separati per UI, logica di business e dati. In pratica, la maggior parte dei team sceglie feature-first con moduli core — questo offre un migliore isolamento e una navigazione chiara del progetto.
La scelta tra gli approcci dipende dalla dimensione del team e dalla prevedibilità delle funzionalità. Se sai esattamente quali schermate saranno nel progetto, feature-first consente a ogni sviluppatore di essere responsabile del proprio modulo. Se la funzionalità cambia frequentemente e si sovrappone tra le schermate, layer-first offre maggiore flessibilità nel riutilizzo del codice tra diverse funzionalità.
Domande frequenti
Il numero ottimale dipende dalle dimensioni del progetto e del team. Per un team di 5 persone, 6–10 moduli sono sufficienti. Per 20+ sviluppatori, 20–40 moduli. Regola: un modulo dovrebbe essere abbastanza piccolo da essere compreso interamente da uno sviluppatore, e abbastanza grande da non creare una rete di dipendenze eccessiva.
La modularità corretta accelera la compilazione grazie alla compilazione parallela e alla memorizzazione nella cache. Tuttavia, un numero eccessivo di moduli con dipendenze strette rallenta la compilazione — Gradle e Xcode impiegano tempo per risolvere il grafo. La chiave per compilazioni veloci è minimizzare le dipendenze transitive e mantenere l'aciclicità.
Sì, ma in modo iterativo. Inizia estraendo i moduli core (rete, database), poi estrai le funzionalità una per una. Usa i feature flags per abilitare il nuovo codice modulare parallelamente al vecchio codice monolitico. La migrazione completa di una grande applicazione richiede da 3 a 12 mesi.
I moduli sono unità di compilazione all'interno di una singola applicazione. I microservizi sono processi separati che girano su server diversi. I moduli dividono il codice, i microservizi dividono il runtime. Nello sviluppo mobile, il termine “microapps” è spesso usato come ibrido: moduli di funzionalità che possono funzionare come applicazioni autonome.
Ogni modulo ha i propri test unitari che vengono eseguiti indipendentemente. I test di integrazione verificano l'interazione tra i moduli. I test UI coprono i moduli di funzionalità con dati mock. L'architettura modulare semplifica i test: simulare una dipendenza di un altro modulo è più facile che simulare parte di un monolite.
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