Le couplage : Ce boa constrictor qui étouffe vos projets à petit feu


Définition

Quand on parle de qualité de code, on pense souvent aux tests, aux design patterns, au clean code. Mais il y a un concept plus fondamental que tous les autres : le couplage.

Le couplage entre deux composants mesure le volume d’informations qu’ils échangent. Plus ils échangent, plus ils sont couplés. Plus ils sont couplés, plus modifier l’un implique de modifier l’autre.

On parle de couplage fort (ou serré) quand deux composants échangent beaucoup de données. Et de couplage faible quand ils sont indépendants ou n’échangent qu’un minimum d’information.

Ce qui se passe quand le couplage n’est pas maîtrisé

Plus le couplage est fort, plus :

  • Modifier un composant entraîne la modification des autres — l’effet domino
  • La structure du programme est rigide — chaque évolution coûte cher
  • L’estimation est impossible — on ne peut pas prédire l’étendue des modifications nécessaires
  • Les tests sont difficiles — impossible de tester un composant isolément
  • Le DRY devient un piège — vouloir mutualiser du code entre composants couplés ne fait qu’aggraver le problème

La première victime du couplage fort, c’est le planning.

Et le problème ne s’arrête pas là : le couplage est transitif. Si A dépend de B et B dépend de C, alors A dépend indirectement de C — même si A ne connaît pas l’existence de C.

graph LR
    A[OrderController] --> B[OrderService]
    B --> C[OrderRepository]
    C --> D[("Base de données")]

    style A fill:#15803d,stroke:#86efac,color:#fff
    style D fill:#8B0000,stroke:#FFD700,color:#fff

Dans cet exemple, OrderController ne parle pas directement à la base de données. Mais si le schéma de la base change, le repository change, le service change… et le contrôleur aussi. Le couplage s’est propagé à travers toute la chaîne.

C’est rarement un problème quand la chaîne est courte. Mais dans un projet réel, les chaînes de dépendances forment un graphe complexe :

graph TD
    A[OrderController] --> B[OrderService]
    A --> Auth[AuthService]
    B --> C[OrderRepository]
    B --> D[PaymentService]
    B --> E[NotificationService]
    D --> F[PaymentGateway]
    D --> Auth
    E --> G[EmailProvider]
    E --> Auth
    C --> DB[("Base de données")]
    F --> ExtAPI[("API Bancaire")]

    style DB fill:#8B0000,stroke:#FFD700,color:#fff
    style ExtAPI fill:#8B0000,stroke:#FFD700,color:#fff

Modifier AuthService impacte potentiellement OrderController, PaymentService et NotificationService. Modifier le schéma de la base de données se propage de OrderRepository jusqu’au contrôleur. Un changement dans un composant bas niveau peut remonter et impacter toute l’application.

Le couplage ne se limite presque jamais à deux composants. L’effet domino est la norme, pas l’exception.

Les 7 niveaux de couplage

Maintenant qu’on a vu les dégâts, comment les diagnostiquer ? Selon Pressman, il existe sept niveaux de couplage, du plus faible au plus fort. Les connaître permet de diagnostiquer la santé d’une codebase.

1. Sans couplage

Pas d’échange d’information. Les composants sont totalement indépendants.

// Deux services indépendants
public class EmailService { /* ... */ }
public class PdfGenerator { /* ... */ }
// Aucun ne connaît l'existence de l'autre

2. Par données

Les composants échangent via des paramètres de type simple (nombre, chaîne, booléen).

public class PriceCalculator
{
    public decimal Calculate(decimal unitPrice, int quantity)
        => unitPrice * quantity;
}

C’est le couplage le plus sain : chaque composant ne connaît que les données dont il a besoin, sous leur forme la plus simple.

C’est d’ailleurs celui qui est utilisé pour la communication entre un front-end et un back-end, ou entre des applications qui n’utilisent pas le même langage de programmation.

3. Par paquet

Les composants échangent des objets ou structures composées.

Le couplage est légèrement plus fort : les composants doivent connaître la structure de ces objets pour les utiliser.

public class OrderService
{
    public void Process(Order order)
    {
        // Reçoit un objet complet — connaît la structure de Order
        var total = order.Lines.Sum(l => l.Price * l.Quantity);
    }
}

La conséquence : si la structure de Order change, tous les consommateurs doivent s’adapter.

4. Par contrôle

Un composant contrôle le comportement d’un autre via un drapeau ou un paramètre de type “mode”.

public class ReportGenerator
{
    public string Generate(Order order, bool detailed)
    {
        if (detailed)
            return GenerateDetailedReport(order);
        else
            return GenerateSummaryReport(order);
    }
}

Le composant appelant décide comment l’autre doit travailler. C’est un signal qu’il en sait trop sur son fonctionnement interne.

5. Externe

Les composants communiquent via un moyen externe : fichier partagé, pipeline, API tierce.

graph LR
    A[Application A] -->|écrit| F[("/shared/data.json")]
    F -->|lit| B[Application B]

    style F fill:#8B0000,stroke:#FFD700,color:#fff

Le couplage est implicite et fragile : rien dans le code ne documente cette dépendance.

Ce couplage peut être assez vicieux, en particulier quand ce sont des équipes séparées qui maintiennent les composants.

Les bugs liés à ce type de couplage sont souvent difficiles à diagnostiquer : il n’y a aucune analyse statique possible, et on n’a pas toujours la possibilité d’instancier un environnement complet dans des tests auto ou une pipeline de CI/CD.

6. Commun (global)

Les composants partagent des variables globales ou un état commun.

public static class AppState
{
    public static User CurrentUser { get; set; }
    public static string ConnectionString { get; set; }
}

// N'importe quel composant peut lire et modifier cet état
public class OrderService
{
    public void Process()
    {
        var user = AppState.CurrentUser; // dépendance cachée
    }
}

Le cas le plus courant de ce couplage : le singleton. DatabasePool.getInstance(), SessionManager.getInstance(), ConfigurationManager.getInstance()… Chaque appel crée une dépendance invisible vers un état global partagé.

public class OrderService
{
    public void Process(int orderId)
    {
        var db = DatabasePool.getInstance(); // couplage caché
        var session = SessionManager.getInstance(); // encore un
        // ...
    }
}

Le problème devient évident quand on visualise les dépendances :

graph TD
    A[OrderService] -->|getInstance| DB[("DatabasePool<br/>(singleton)")]
    B[UserService] -->|getInstance| DB
    C[NotificationService] -->|getInstance| DB
    D[PaymentService] -->|getInstance| DB
    E[ReportService] -->|getInstance| DB

    style DB fill:#8B0000,stroke:#FFD700,color:#fff,stroke-width:3px

Chaque service de la codebase est couplé au singleton. Si la connexion change, si le pool doit être configuré différemment, si on veut tester un service isolément — tout est impacté. Le singleton est un état global déguisé en “bonne pratique”.

Chaque composant dépend de l’état global sans que cette dépendance soit visible dans sa signature. Les bugs deviennent imprévisibles.

Ici j’ai choisi un exemple de singleton, mais le même problème se pose avec n’importe quelle variable globale partagée entre plusieurs composants.

Un autre exemple qu’on retrouve souvent sur les projets, c’est la classe de configuration : AppSettings, ConfigManager, EnvironmentVariables… Toutes ces classes sont des états globaux partagés, et chaque composant qui les utilise est implicitement couplé à elles.

Ou encore, la brique d’authentification et de gestion des permissions : AuthService, PermissionManager, UserContext… Chaque composant qui a besoin de vérifier les permissions ou d’authentifier un utilisateur est implicitement couplé à ce service, même si ce n’est pas visible dans sa signature.

7. Par contenu (interne)

Un composant lit ou écrit directement dans les données internes d’un autre.

public class OrderService
{
    public void ForceDiscount(PriceCalculator calculator)
    {
        // Accès direct aux champs internes via réflexion
        var field = typeof(PriceCalculator)
            .GetField("_discountRate", BindingFlags.NonPublic | BindingFlags.Instance);
        field.SetValue(calculator, 0.5m);
    }
}

C’est le niveau le plus fort : les composants n’ont plus aucune frontière. Toute modification est un risque.

Et il y a une forme de couplage par contenu qu’on utilise tous les jours sans y penser : l’héritage. Quand une classe hérite d’une autre, elle a accès à ses champs internes, ses méthodes protégées, sa logique d’initialisation. Elle dépend de tout ce que fait la classe parente — y compris de ses détails d’implémentation.

public class SpecialOrder : Order
{
    public override decimal GetTotal()
    {
        // Dépend du calcul interne de Order
        // Si Order change sa logique, SpecialOrder casse
        return base.GetTotal() * 0.9m;
    }
}

C’est pour cette raison que le Gang of Four recommande de favoriser la composition plutôt que l’héritage. L’héritage est la forme la plus forte de couplage entre deux classes.

Comment détecter un couplage trop fort

Quelques signaux d’alerte :

  • Une classe importe des dizaines de packages différents (DB, HTTP, utils, logging…)
  • Les classes métier instancient directement des classes techniques
  • Modifier un module déclenche des effets de bord dans des modules éloignés
  • Impossible de tester un composant sans monter toute l’infrastructure

La solution

Pour réduire le couplage, on met en place des standards de communication entre les composants. Les échanges doivent imposer le moins de contraintes possibles aux composants impliqués.

Concrètement, cela passe par :

  • Des abstractions : dépendre d’interfaces plutôt que d’implémentations concrètes (Dependency Inversion)
  • L’encapsulation : laisser les objets gérer leur propre état (Tell, Don’t Ask)
  • L’injection de dépendances : ne pas instancier ses dépendances, les recevoir
  • Des contrats minimaux : n’exposer que le strict nécessaire

En résumé

NiveauTypeRisque
1Sans couplageAucun
2Par donnéesFaible
3Par paquetModéré
4Par contrôleÉlevé
5ExterneÉlevé (et invisible)
6Commun (global)Très élevé
7Par contenuMaximal

Le couplage n’est pas un ennemi à éliminer — un système sans aucun couplage ne fait rien. L’objectif est de le maîtriser : garder le couplage au niveau le plus faible possible, et rendre les dépendances explicites plutôt que cachées.

Demain, on parlera de l’autre face de la pièce : la cohésion, le principe qui guide la structuration interne des composants.


Cet article est issu d’un séminaire Clean Code donné lors d’un Bretzel Craft. Pour approfondir avec des exemples concrets, consultez mes autres articles sur le blog ou invitez-moi pour un Brown Bag Lunch dans vos locaux.