Les Couplers : ces code smells qui trahissent un couplage fort
Un bon développeur se fie à son nez
Vous ouvrez une classe que vous n’avez jamais vue. Avant même de comprendre ce qu’elle fait, quelque chose vous dérange. Quarante imports en tête de fichier. Des chaînes d’appels à rallonge. Une logique métier qui semble éparpillée un peu partout.
Rien ne plante. Les tests passent. Et pourtant, ça sent mauvais.
C’est exactement ce que Kent Beck et Martin Fowler ont voulu capturer avec le terme code smell : une caractéristique de surface du code qui suggère - sans le prouver - un problème de conception plus profond.
Les code smells, c’est un peu comme les odeurs dans une cuisine. On n’est pas certain qu’il y a un problème. Mais quand ça sent le brûlé, on va vérifier le four avant de servir le plat.
Un smell n’est pas un bug
C’est la distinction essentielle. Un bug, c’est un comportement incorrect : le système fait quelque chose de faux. Un smell, c’est un symptôme structurel : le système fait peut-être la bonne chose, mais sa structure rend les évolutions futures risquées et coûteuses.
C’est ce qui rend les smells précieux :
- Ils sont détectables à la lecture. Pas besoin d’exécuter le code, pas besoin d’outillage. Votre œil suffit, et il s’entraîne.
- Ils précèdent les incidents. Un smell détecté en revue de code coûte quelques minutes de discussion. Le même problème découvert en production coûte des jours de debugging.
- Ils donnent un vocabulaire commun. Dire “cette méthode fait du Feature Envy” en revue de code est plus précis - et moins vexant - que “ce code est moche”.
Fowler regroupe les smells en familles : les Bloaters (le code qui grossit trop), les Change Preventers (le code qui résiste au changement), les Dispensables (le code inutile)… et la famille qui nous intéresse aujourd’hui : les Couplers.
Les Couplers sont les smells qui trahissent un couplage fort entre composants. Et comme le couplage est la première source de rigidité d’un projet, ce sont ceux qui méritent votre attention en priorité.
Feature Envy : la méthode jalouse
Une méthode est “envieuse” quand elle s’intéresse davantage aux données d’une autre classe qu’aux siennes.
public class InvoiceService
{
public decimal ComputeDiscount(Customer customer)
{
// Cette méthode ne touche à RIEN de InvoiceService.
// Elle ne fait que lire les données de Customer.
if (customer.YearsOfMembership > 5 && customer.TotalOrders > 100)
return 0.15m;
if (customer.YearsOfMembership > 2)
return 0.05m;
return 0m;
}
}
ComputeDiscount vit dans InvoiceService, mais toutes les données qu’elle manipule appartiennent à Customer. La règle métier “qu’est-ce qu’un client fidèle” est stockée en dehors de l’objet qui détient l’information.
Conséquence directe : si la structure de Customer change, InvoiceService casse. Et comme cette logique envieuse a tendance à se dupliquer (un autre service aura besoin de savoir si le client est fidèle), la connaissance métier s’éparpille dans toute la codebase.
Le remède, c’est de rapatrier le comportement auprès des données :
public class Customer
{
public int YearsOfMembership { get; private set; }
public int TotalOrders { get; private set; }
public decimal LoyaltyDiscount =>
(YearsOfMembership, TotalOrders) switch
{
( > 5, > 100) => 0.15m,
( > 2, _) => 0.05m,
_ => 0m
};
}
Vous reconnaissez le principe : c’est exactement Tell, Don’t Ask. Feature Envy est le smell ; Tell, Don’t Ask est le remède.
Message Chains : le train de wagons
C’est le smell le plus facile à repérer, parce qu’il se voit à l’œil nu :
var managerName = session.GetUser().GetTeam().GetManager().Name;
Chaque point de cette chaîne est un couplage supplémentaire. Cette ligne anodine dépend de quatre structures : Session, User, Team et Manager. Si l’une des quatre change - on renomme une propriété, on rend la relation optionnelle, on déplace Manager ailleurs - cette ligne casse.
Et le problème est rarement isolé : si cette chaîne existe ici, elle existe probablement à vingt autres endroits. Un changement dans Team déclenche alors une réaction en chaîne dans des fichiers qui n’ont, en apparence, rien à voir avec les équipes.
C’est précisément ce que le couplage transitif produit à l’échelle d’un système : l’appelant est couplé à toute la chaîne, pas seulement à son voisin immédiat.
La Loi de Déméter : ne parlez qu’à vos amis proches
Le garde-fou contre les Message Chains porte un nom : la Loi de Déméter, formulée en 1987 à la Northeastern University. Son énoncé informel tient en une phrase :
Ne parlez qu’à vos amis proches, pas aux étrangers.
Plus formellement, une méthode ne devrait invoquer que des méthodes appartenant à :
- son propre objet (
this) - ses paramètres
- les objets qu’elle crée elle-même
- les champs directs de sa classe
Tout le reste - les objets obtenus en traversant d’autres objets - sont des étrangers. Notre chaîne session.GetUser().GetTeam().GetManager().Name viole la loi trois fois : User, Team et Manager sont des étrangers obtenus par traversée.
La correction consiste à demander le résultat, pas l’itinéraire :
// Avant : l'appelant connaît toute la structure interne
var managerName = session.GetUser().GetTeam().GetManager().Name;
// Après : l'appelant pose une question, la structure reste cachée
var managerName = session.GetManagerName();
L’information traverse toujours les mêmes objets en interne. Mais ce trajet est désormais encapsulé : il n’existe qu’à un seul endroit. Si la structure change, une seule méthode est à adapter, pas vingt sites d’appel.
Inappropriate Intimacy : les classes trop intimes
Deux classes sont “inappropriées” quand elles connaissent trop de détails l’une de l’autre - champs censés être internes, ordre d’initialisation, comportements non documentés.
Le symptôme classique : la dépendance bidirectionnelle.
public class Order
{
public Customer Customer { get; set; }
public decimal Total => Customer.IsVip ? BaseTotal * 0.9m : BaseTotal;
}
public class Customer
{
public List<Order> Orders { get; set; }
public bool IsVip => Orders.Sum(o => o.Total) > 10_000;
}
Order a besoin de Customer pour calculer son total. Customer a besoin de ses Order pour savoir s’il est VIP. Lequel charge-t-on en premier ? Comment tester l’un sans l’autre ? Et avez-vous remarqué que IsVip dépend de Total qui dépend de IsVip ?
Ces deux classes ne sont plus deux composants : c’est un seul composant déguisé en deux, avec tous les inconvénients des deux options. Le remède passe par une remise à plat des responsabilités - souvent en extrayant le concept caché (ici, une politique de tarification VIP qui n’appartient ni à l’un ni à l’autre).
Une variante très répandue de ce smell : la classe métier qui instancie ses dépendances techniques.
public class OrderService
{
public void Confirm(Order order)
{
var mailer = new SmtpMailService("smtp.prod.internal", 587);
var db = DatabasePool.GetInstance();
// ...
}
}
OrderService connaît le serveur SMTP, son port, et le mécanisme d’accès à la base. C’est une intimité déplacée avec l’infrastructure - et c’est exactement le problème que résout l’inversion de dépendance.
Middle Man : l’intermédiaire inutile
Le dernier membre de la famille est le smell inverse : une classe qui ne fait que déléguer.
public class OrderFacade
{
private readonly OrderService _service;
public Order Get(int id) => _service.Get(id);
public void Confirm(Order o) => _service.Confirm(o);
public void Cancel(Order o) => _service.Cancel(o);
// ... 15 autres méthodes identiques
}
Si chaque méthode se contente de transférer l’appel, la classe n’apporte aucune valeur : elle ajoute une couche de couplage et d’indirection pour rien.
Le paradoxe mérite d’être souligné : le Middle Man apparaît souvent… en corrigeant trop zélément les Message Chains. À force de vouloir cacher chaque traversée, on crée des couches de délégation pure. C’est le rappel que les smells sont des heuristiques, pas des règles : appliqués mécaniquement, ils se contredisent.
Attention aux faux positifs
Justement, parlons-en. Un smell signale un problème potentiel. Quelques cas où ça sent fort mais où tout va bien :
- Les API fluentes et LINQ.
orders.Where(...).OrderBy(...).Take(10)ressemble à une Message Chain, mais chaque appel retourne le même type d’abstraction. Vous n’êtes couplé qu’à une seule interface, pas à une hiérarchie d’objets. - Les builders.
builder.WithName(...).WithAge(...).Build(): même raisonnement. - Les structures de données pures. Naviguer dans un DTO ou un objet de configuration (
config.Database.ConnectionString) ne viole pas Déméter au sens utile du terme : ces objets n’ont pas de comportement à encapsuler.
La question à se poser n’est jamais “est-ce que ça matche le pattern ?” mais “est-ce qu’un changement de structure ici se propagerait à des endroits qui ne devraient pas être concernés ?”. Si la réponse est non, votre nez vous a joué un tour - ça arrive aux meilleurs chiens de chasse.
En résumé
| Smell | Symptôme | Remède principal |
|---|---|---|
| Feature Envy | Une méthode n’utilise que les données d’une autre classe | Déplacer le comportement vers les données (Tell, Don’t Ask) |
| Message Chains | a.GetB().GetC().GetD() | Encapsuler la traversée, exposer une question |
| Inappropriate Intimacy | Dépendances bidirectionnelles, détails internes partagés | Extraire le concept caché, inverser les dépendances |
| Middle Man | Une classe qui ne fait que déléguer | Supprimer l’intermédiaire, appeler directement |
Les Couplers ne sont pas des bugs à corriger en urgence. Ce sont des signaux faibles de couplage fort - et le couplage fort, lui, finira par vous coûter cher : estimations impossibles, régressions en cascade, peur de toucher au code.
La prochaine fois que vous ouvrez une classe et que quelque chose vous dérange sans que vous sachiez quoi, prenez une seconde. Cherchez le Coupler. Nommez-le. Votre nez avait probablement raison.
Cet article est tiré de ma conférence Le Seigneur du Legacy, où Gandalf lui-même applique cette méthode dans la Moria : “I have no memory of this place” - mais son nez, lui, se souvient. Disponible en Brown Bag Lunch dans vos locaux.