Le couplage (Coupling) est une métrique qui montre à quel point un module d'une application dépend d'un autre. Selon Wikipédia, un faible couplage (low coupling) est le signe d'un système bien conçu, où les modules peuvent être modifiés sans casser les voisins. La gestion du couplage est l'une des principales tâches de l'architecte lors de la conception d'applications mobiles.
Points Clés
Le couplage (Coupling) est une métrique qui détermine à quel point un module ou une classe est connecté à un autre. Plus un module en sait sur la structure interne d'un autre, plus le couplage est élevé et plus il est difficile de modifier le système. Dans une architecture bien conçue, le couplage doit être minimal — les modules interagissent uniquement via des interfaces strictement définies.
Il existe deux aspects du couplage : afférent (dépendances entrantes — combien de modules dépendent de celui-ci) et efférent (dépendances sortantes — de combien de modules celui-ci dépend). L'analyse de ces métriques permet d'identifier les points chauds dans l'architecture où la modification d'un module en affecterait plusieurs autres. Des outils comme IntelliJ Dependency Analyzer et Xcode Graph visualisent ces connexions.
Il est important de comprendre qu'un couplage nul est impossible — les modules doivent interagir d'une manière ou d'une autre, sinon ce n'est pas un système mais un ensemble de programmes isolés. La tâche de l'architecte est de rendre le couplage gérable et transparent. L'idéal : les modules interagissent uniquement via des interfaces et ne transmettent que des données simples, sans connaître la structure interne des autres. C'est ce qu'on appelle le faible couplage (loose coupling).
Six types de couplage forment une échelle du meilleur au pire. Comprendre cette échelle aide à évaluer le code existant et à choisir la direction de la refactorisation. La plupart des projets mobiles ont des types de couplage mixtes, et la tâche de l'architecte est de remplacer progressivement les types forts par des types faibles.
Data coupling (couplage de données) — les modules échangent uniquement des données simples via des paramètres de méthodes. Le module A appelle la méthode du module B, en passant des primitifs ou des structures simples, et reçoit un résultat. Le module A ne sait pas comment B est implémenté en interne. C'est le type de couplage le plus souhaitable : il minimise l'impact des changements.
Exemple : EmailValidator.isValid(email: String): Boolean. La classe consommatrice passe une chaîne et reçoit un booléen, sans aucune idée des expressions régulières ou des règles de validation à l'intérieur du validateur. Modifier la logique de validation ne nécessite pas de modifier le consommateur — le couplage est minimal. Le data coupling est l'objectif pour toutes les interfaces publiques d'une application.
Stamp coupling (couplage par tampon) — les modules échangent des objets composites mais n'utilisent qu'une partie de leurs champs. Le module A passe un objet User à la méthode calculateDiscount, qui utilise uniquement user.status. Le problème : si la structure User change (un champ obligatoire est ajouté), le module calculateDiscount ne change pas, mais le consommateur qui crée l'objet User change.
En pratique, le stamp coupling est inévitable et acceptable si l'objet passé est un modèle de données standard (Entity). Le problème survient lorsqu'un module reçoit un objet entier juste pour un seul champ. Dans de tels cas, il est préférable de passer la valeur spécifique directement (data coupling). La solution est d'analyser l'utilisation des champs par la partie réceptrice.
Control coupling — un module passe un drapeau à un autre qui contrôle son comportement (calculate(useNewAlgorithm: Boolean)). C'est pire que le stamp coupling car le module consommateur doit connaître les variantes internes de fonctionnement du module appelé. Solution : diviser la méthode en deux — calculateWithNewAlgorithm() et calculateWithLegacyAlgorithm().
External coupling — les modules dépendent d'un protocole externe, d'un format de données ou d'une API. Tous les modules qui analysent le même JSON ou travaillent avec la même base de données ont un external coupling. On ne peut pas l'éviter complètement, mais on peut l'isoler : créer une couche de mappage entre le format externe et les modèles internes. Common coupling — les modules partagent un état global commun. Content coupling — le pire type, quand un module modifie directement les données internes d'un autre module.
| Type de couplage | Niveau | Description |
|---|---|---|
| Data | Meilleur | Passage de données simples par paramètres |
| Stamp | Acceptable | Passage d'objets avec utilisation partielle |
| Control | Moyen | Contrôle du comportement par drapeaux |
| External | Élevé | Dépendance à un protocole/format externe |
| Common | Très élevé | Partage d'état global |
| Content | Inacceptable | Modification directe des données internes d'un module |
L'échelle de couplage de data (idéal) à content (catastrophe) est un outil pratique pour les revues de code. Si vous voyez du common ou du content coupling dans un projet — ce sont des cibles prioritaires de refactorisation. Les data et stamp coupling sont acceptables et présents dans tout projet, mais leur quantité doit être contrôlée.
Un couplage élevé transforme le développement en un processus lent où chaque modification nécessite de vérifier des dizaines de modules potentiellement cassés. C'est particulièrement critique dans le développement mobile : les plateformes sont mises à jour annuellement (Android API Level, iOS SDK), les bibliothèques trimestriellement et les exigences commerciales en continu. Le faible couplage est le seul moyen de gérer ce flux de changements sans régressions constantes.
Exemple pratique : une application mobile où tous les écrans importent directement NetworkingManager et DatabaseManager. Lors du remplacement du client HTTP de Retrofit vers Ktor (Android) ou d'URLSession vers Alamofire (iOS), le développeur devrait modifier chaque écran. Avec un faible couplage, il suffit de changer une implémentation cachée derrière l'interface NetworkDataSource — les consommateurs ne remarqueront pas le remplacement.
L'impact du couplage sur les tests unitaires est également énorme. Une classe avec un couplage élevé (création directe de dépendances dans le constructeur) ne peut pas être testée isolément — elle entraîne avec elle la base de données, le réseau et l'interface utilisateur. Pour tester une telle classe, il faut lancer un émulateur et attendre les tests d'intégration. Une classe avec un faible couplage accepte les dépendances via l'injection par constructeur et peut être facilement mockée.
// Couplage élevé — la classe crée ses propres dépendances
class ProfileViewModelHigh {
private val api = RetrofitApi()
private val db = RoomDatabase.getInstance()
private val cache = MemoryCache()
}
// Couplage faible — les dépendances sont passées via le constructeur
class ProfileViewModelLow(
private val api: ApiService,
private val db: DatabaseService,
private val cache: CacheService
)
Dans le premier cas, ProfileViewModelHigh est fortement lié à des implémentations spécifiques — remplacer Retrofit par Ktor nécessite de modifier le code de la ViewModel. Dans le second cas, ProfileViewModelLow ne dépend que d'interfaces, dont les implémentations sont fournies de l'extérieur. Tester la seconde classe est trivial : passer des implémentations simulées et vérifier la logique sans émulateur.
Le principe d'inversion des dépendances (D dans SOLID) est le fondement de la réduction du couplage. Le principe stipule de dépendre des abstractions, et non des implémentations concrètes. Au lieu qu'une classe crée directement un objet RetrofitApi, elle doit recevoir une interface ApiService. Cela déplace la dépendance d'une bibliothèque spécifique vers le niveau d'abstraction, qui peut être remplacé sans modifier le consommateur.
Le motif Observer (ou ses versions réactives — StateFlow, Combine Publishers) réduit le couplage entre la source de données et les abonnés. L'abonné ne sait pas d'où viennent les données — il réagit simplement aux changements. Cela découple l'expéditeur et le récepteur : on peut ajouter une nouvelle source de données sans modifier les abonnés existants. EventBus et SharedFlow fonctionnent sur le même principe.
Le motif Bridge sépare l'abstraction de l'implémentation, leur permettant de changer indépendamment. Dans le développement mobile, Bridge est utilisé par exemple pour les modules dépendants de la plateforme : une interface commune ImageLoader avec différentes implémentations pour iOS (Kingfisher, Nuke) et Android (Glide, Coil). Le code qui travaille avec ImageLoader ne dépend pas de la bibliothèque choisie et peut la remplacer en changeant simplement l'implémentation.
L'injection de dépendances (DI) est l'outil le plus pratique pour réduire le couplage dans le développement mobile. Au lieu qu'une classe crée ses propres dépendances, un conteneur DI (Hilt, Koin, Dagger pour Android ; Swinject, Factory pour iOS) les fournit de l'extérieur. La classe reçoit les dépendances via l'injection par constructeur, méthode ou propriété, sans connaître les implémentations concrètes.
La DI documente explicitement les dépendances d'une classe : il suffit de regarder le constructeur pour comprendre avec quels modules la classe interagit. Si le constructeur accepte 8 paramètres de différentes couches — c'est un signe de couplage excessif nécessitant une refactorisation. Une bonne pratique est de ne pas dépasser 3-4 dépendances par classe. Un nombre plus élevé indique une violation du principe de responsabilité unique et un couplage excessif.
La DI simplifie également les tests : pour chaque test, vous créez une classe avec des dépendances simulées, sans nécessiter de base de données ou de réseau réel. Dans Flutter, la DI est implémentée via Provider, Riverpod ou GetIt. Quel que soit le framework, l'objectif est un : réduire le couplage entre les modules en rendant les dépendances explicites et remplaçables. L'utilisation de la DI dans un projet mobile est la norme de facto depuis les années 2020.
// Le conteneur DI construit le graphe de dépendances
protocol AuthServiceProtocol {
func login(email: String, password: String) async throws -> User
}
final class AuthService: AuthServiceProtocol {
func login(email: String, password: String) async throws -> User {
// implémentation
}
}
// ViewModel ne connaît pas le service spécifique — seulement le protocole
final class LoginViewModel {
private let auth: AuthServiceProtocol
init(auth: AuthServiceProtocol) {
self.auth = auth
}
}
// DI Container est le seul endroit où les types concrets sont créés
final class DIContainer {
lazy var authService: AuthServiceProtocol = AuthService()
lazy var loginViewModel: LoginViewModel {
LoginViewModel(auth: self.authService)
}
}
Ici, LoginViewModel ne dépend que du protocole AuthServiceProtocol, et non d'un AuthService spécifique. Remplacer l'implémentation (par exemple, passer de Firebase Auth à un serveur personnalisé) ne nécessite des modifications que dans DIContainer. Tous les consommateurs d'AuthServiceProtocol restent intacts — le couplage est minimisé par l'abstraction et la DI.
Foire Aux Questions
La cohésion mesure la cohérence interne d'un module, tandis que le couplage mesure l'interconnexion externe entre modules. Une bonne architecture vise une cohésion élevée et un couplage faible. Ces métriques sont inversement proportionnelles : augmenter la cohésion réduit généralement le couplage, et vice versa.
Data et stamp sont normaux et présents dans tout projet. Le control coupling est acceptable dans des scénarios limités (par exemple, le motif strategy). L'external coupling est inévitable lors du travail avec des API externes, mais doit être isolé derrière une couche de mappage. Le common et le content coupling sont des signes de problèmes architecturaux nécessitant une refactorisation immédiate.
Outils d'analyse statique : IntelliJ IDEA Dependency Matrix, Xcode Graph, rapport de dépendances Gradle, SonarQube. Métriques : couplage afférent (Ca), couplage efférent (Ce), Instabilité (Ce/(Ca+Ce)). Une instabilité élevée (proche de 1) signifie que le module est facile à modifier et que peu de choses le référencent — c'est bon.
Un couplage extrêmement faible peut signifier un nombre excessif d'abstractions et d'interfaces qui compliquent la navigation dans le code. Si une interface séparée est créée pour chaque classe, le programmeur perd du temps à sauter entre les fichiers. Équilibre : des interfaces pour l'API externe du module, mais pas pour chaque classe d'assistance interne.
Utilisez la technique Strangler Fig — remplacez progressivement les appels directs par des interfaces. Commencez par extraire des interfaces pour les classes les plus référencées. Introduisez ensuite un conteneur DI. Couvrez le code isolé avec des tests de caractérisation pour vous assurer que la refactorisation ne modifie pas le comportement du système.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi