L'accoppiamento (Coupling) è una metrica che mostra quanto un modulo di un'applicazione dipende da un altro. Secondo Wikipedia, un basso accoppiamento (low coupling) è segno di un sistema ben progettato, dove i moduli possono essere modificati senza rompere quelli vicini. Gestire il coupling è uno dei compiti principali dell'architetto nella progettazione di applicazioni mobili.
Punti Chiave
Il coupling (accoppiamento) è una metrica che determina quanto strettamente un modulo o una classe è collegato a un altro. Più un modulo sa sulla struttura interna di un altro, più alto è il coupling e più difficile è modificare il sistema. In un'architettura ben progettata, il coupling dovrebbe essere minimo — i moduli interagiscono solo attraverso interfacce strettamente definite.
Esistono due aspetti del coupling: afferente (dipendenze in entrata — quanti moduli dipendono da questo) ed efferente (dipendenze in uscita — da quanti moduli questo dipende). L'analisi di queste metriche aiuta a identificare i punti caldi nell'architettura dove la modifica di un modulo influenzerebbe molti altri. Strumenti come IntelliJ Dependency Analyzer e Xcode Graph visualizzano queste connessioni.
È importante capire che il coupling zero è impossibile — i moduli devono interagire in qualche modo, altrimenti non è un sistema ma un insieme di programmi isolati. Il compito dell'architetto è rendere il coupling gestibile e trasparente. L'ideale: i moduli interagiscono solo attraverso interfacce e passano solo dati semplici, senza conoscere la struttura interna l'uno dell'altro. Questo si chiama basso accoppiamento (loose coupling).
Sei tipi di coupling formano una scala dal migliore al peggiore. Comprendere questa scala aiuta a valutare il codice esistente e scegliere la direzione del refactoring. La maggior parte dei progetti mobile ha tipi misti di coupling, e il compito dell'architetto è sostituire progressivamente i tipi forti con quelli deboli.
Data coupling (accoppiamento di dati) — i moduli scambiano solo dati semplici attraverso parametri di metodi. Il modulo A chiama il metodo del modulo B, passando primitivi o strutture semplici, e riceve un risultato. Il modulo A non sa come B è implementato internamente. Questo è il tipo di coupling più desiderabile: minimizza l'impatto delle modifiche.
Esempio: EmailValidator.isValid(email: String): Boolean. La classe consumatrice passa una stringa e riceve un Booleano, senza alcuna idea delle espressioni regolari o delle regole di validazione all'interno del validatore. Modificare la logica di validazione non richiede di modificare il consumatore — il coupling è minimo. Il data coupling è l'obiettivo per tutte le interfacce pubbliche in un'applicazione.
Stamp coupling (accoppiamento per timbro) — i moduli scambiano oggetti composti ma usano solo una parte dei loro campi. Il modulo A passa un oggetto User al metodo calculateDiscount, che usa solo user.status. Il problema: se la struttura di User cambia (un campo obbligatorio viene aggiunto), il modulo calculateDiscount non cambia, ma il consumatore che crea l'oggetto User cambia.
In pratica, lo stamp coupling è inevitabile e accettabile se l'oggetto passato è un modello dati standard (Entity). Il problema sorge quando un modulo riceve un intero oggetto solo per un singolo campo. In tali casi, è meglio passare il valore specifico direttamente (data coupling). La soluzione è analizzare l'uso dei campi da parte del ricevente.
Control coupling — un modulo passa un flag a un altro che controlla il suo comportamento (calculate(useNewAlgorithm: Boolean)). Questo è peggio dello stamp coupling perché il modulo consumatore deve conoscere le varianti interne di funzionamento del modulo chiamato. Soluzione: dividere il metodo in due — calculateWithNewAlgorithm() e calculateWithLegacyAlgorithm().
External coupling — i moduli dipendono da un protocollo esterno, formato dati o API. Tutti i moduli che analizzano lo stesso JSON o lavorano con lo stesso database hanno external coupling. Non può essere evitato completamente, ma può essere isolato: creare un livello di mappatura tra il formato esterno e i modelli interni. Common coupling — i moduli condividono uno stato globale comune. Content coupling — il tipo peggiore, quando un modulo modifica direttamente i dati interni di un altro modulo.
| Tipo di coupling | Livello | Descrizione |
|---|---|---|
| Data | Migliore | Passaggio di dati semplici tramite parametri |
| Stamp | Accettabile | Passaggio di oggetti con uso parziale |
| Control | Medio | Controllo del comportamento tramite flag |
| External | Alto | Dipendenza da protocollo/formato esterno |
| Common | Molto alto | Condivisione di stato globale |
| Content | Inaccettabile | Modifica diretta dei dati interni del modulo |
La scala del coupling da data (ideale) a content (disastro) è uno strumento pratico per le revisioni del codice. Se vedi common o content coupling in un progetto — questi sono obiettivi prioritari di refactoring. I data e stamp coupling sono accettabili e presenti in qualsiasi progetto, ma la loro quantità dovrebbe essere controllata.
L'alto coupling trasforma lo sviluppo in un processo lento dove ogni modifica richiede di verificare decine di moduli potenzialmente rotti. Questo è particolarmente critico nello sviluppo mobile: le piattaforme vengono aggiornate annualmente (Android API Level, iOS SDK), le librerie trimestralmente e i requisiti aziendali continuamente. Il basso accoppiamento è l'unico modo per gestire questo flusso di cambiamenti senza regressioni costanti.
Esempio pratico: un'applicazione mobile dove tutte le schermate importano direttamente NetworkingManager e DatabaseManager. Quando si sostituisce il client HTTP da Retrofit a Ktor (Android) o da URLSession ad Alamofire (iOS), lo sviluppatore dovrebbe modificare ogni schermata. Con un basso coupling, basta cambiare un'implementazione nascosta dietro l'interfaccia NetworkDataSource — i consumatori non noteranno la sostituzione.
L'impatto del coupling sui test unitari è anche enorme. Una classe con alto coupling (creazione diretta di dipendenze nel costruttore) non può essere testata isolatamente — trascina con sé database, rete e UI. Per testare tale classe, bisogna avviare un emulatore e attendere i test di integrazione. Una classe con basso coupling accetta dipendenze tramite injection nel costruttore e può essere facilmente mockata.
// Alto coupling — la classe crea le proprie dipendenze
class ProfileViewModelHigh {
private val api = RetrofitApi()
private val db = RoomDatabase.getInstance()
private val cache = MemoryCache()
}
// Basso coupling — le dipendenze vengono passate tramite costruttore
class ProfileViewModelLow(
private val api: ApiService,
private val db: DatabaseService,
private val cache: CacheService
)
Nel primo caso, ProfileViewModelHigh è strettamente legato a implementazioni specifiche — sostituire Retrofit con Ktor richiede di modificare il codice della ViewModel. Nel secondo caso, ProfileViewModelLow dipende solo da interfacce, le cui implementazioni sono fornite esternamente. Testare la seconda classe è banale: passare implementazioni mock e verificare la logica senza emulatore.
Il Principio di Inversione delle Dipendenze (D in SOLID) è il fondamento per ridurre il coupling. Il principio prescrive di dipendere dalle astrazioni, non dalle implementazioni concrete. Invece di creare direttamente un oggetto RetrofitApi, una classe dovrebbe ricevere un'interfaccia ApiService. Questo sposta la dipendenza da una libreria specifica al livello di astrazione, che può essere sostituito senza modificare il consumatore.
Il pattern Observer (o le sue versioni reattive — StateFlow, Combine Publishers) riduce il coupling tra la fonte dati e gli abbonati. L'abbonato non sa da dove provengono i dati — reagisce semplicemente ai cambiamenti. Questo disaccoppia mittente e destinatario: si può aggiungere una nuova fonte dati senza modificare gli abbonati esistenti. EventBus e SharedFlow funzionano sullo stesso principio.
Il pattern Bridge separa l'astrazione dall'implementazione, permettendo loro di cambiare indipendentemente. Nello sviluppo mobile, Bridge viene usato, ad esempio, per i moduli dipendenti dalla piattaforma: un'interfaccia comune ImageLoader con diverse implementazioni per iOS (Kingfisher, Nuke) e Android (Glide, Coil). Il codice che lavora con ImageLoader non dipende dalla libreria scelta e può sostituirla semplicemente cambiando l'implementazione.
Dependency Injection (DI) è lo strumento più pratico per ridurre il coupling nello sviluppo mobile. Invece di creare le proprie dipendenze, un contenitore DI (Hilt, Koin, Dagger per Android; Swinject, Factory per iOS) le fornisce dall'esterno. La classe riceve le dipendenze tramite injection nel costruttore, metodo o proprietà, rimanendo all'oscuro delle implementazioni concrete.
La DI documenta esplicitamente le dipendenze di una classe: basta guardare il costruttore per capire con quali moduli la classe interagisce. Se il costruttore accetta 8 parametri da diversi livelli — questo è un segnale di coupling eccessivo che richiede refactoring. Buona pratica è non più di 3-4 dipendenze per classe. Un numero maggiore indica una violazione del Principio di Responsabilità Singola e un coupling eccessivo.
La DI semplifica anche i test: per ogni test si crea una classe con dipendenze mock, senza bisogno di database o rete reali. In Flutter la DI viene implementata tramite Provider, Riverpod o GetIt. Indipendentemente dal framework, l'obiettivo è uno: ridurre l'accoppiamento tra moduli rendendo le dipendenze esplicite e sostituibili. L'uso della DI in un progetto mobile è lo standard de facto dagli anni 2020.
// Il container DI costruisce il grafo delle dipendenze
protocol AuthServiceProtocol {
func login(email: String, password: String) async throws -> User
}
final class AuthService: AuthServiceProtocol {
func login(email: String, password: String) async throws -> User {
// implementazione
}
}
// ViewModel non conosce il servizio specifico — solo il protocollo
final class LoginViewModel {
private let auth: AuthServiceProtocol
init(auth: AuthServiceProtocol) {
self.auth = auth
}
}
// DI Container è l'unico posto dove vengono creati i tipi concreti
final class DIContainer {
lazy var authService: AuthServiceProtocol = AuthService()
lazy var loginViewModel: LoginViewModel {
LoginViewModel(auth: self.authService)
}
}
Qui, LoginViewModel dipende solo dal protocollo AuthServiceProtocol, non da uno specifico AuthService. Sostituire l'implementazione (ad esempio, passare da Firebase Auth a un server personalizzato) richiede modifiche solo in DIContainer. Tutti i consumatori di AuthServiceProtocol rimangono intatti — il coupling è minimizzato attraverso astrazione e DI.
Domande Frequenti
La coesione misura la coerenza interna di un modulo, mentre il coupling misura l'interconnessione esterna tra moduli. Una buona architettura cerca alta coesione e basso coupling. Queste metriche sono inversamente proporzionali: aumentare la coesione di solito riduce il coupling, e viceversa.
Data e stamp sono normali e presenti in qualsiasi progetto. Il control coupling è accettabile in scenari limitati (ad esempio, pattern strategy). L'external coupling è inevitabile quando si lavora con API esterne, ma dovrebbe essere isolato dietro un livello di mappatura. Il common e il content coupling sono segni di problemi architetturali che richiedono refactoring immediato.
Strumenti di analisi statica: IntelliJ IDEA Dependency Matrix, Xcode Graph, rapporto dipendenze Gradle, SonarQube. Metriche: coupling afferente (Ca), coupling efferente (Ce), Instabilità (Ce/(Ca+Ce)). Un'alta Instabilità (vicina a 1) significa che il modulo è facile da modificare e poche cose lo referenziano — questo è buono.
Un coupling estremamente basso può significare un numero eccessivo di astrazioni e interfacce che complicano la navigazione nel codice. Se viene creata un'interfaccia separata per ogni classe, il programmatore perde tempo saltando tra i file. Equilibrio: interfacce per l'API esterna del modulo, ma non per ogni classe di supporto interna.
Usa la tecnica Strangler Fig — sostituisci gradualmente le chiamate dirette con interfacce. Inizia estraendo interfacce per le classi più referenziate. Poi introduci un contenitore DI. Copri il codice isolato con test di caratterizzazione per assicurarti che il refactoring non modifichi il comportamento del sistema.
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