Law of Demeter : le principe qui tue les chaînes d'appels interminables dans votre code
Quand un changement dans une classe lointaine casse trois fichiers que vous pensiez indépendants, le problème n'est pas la malchance. C'est la loi de Déméter qui a été ignorée. Voici ce que ce principe corrige concrètement.
Il y a une ligne de code qui revient dans tous les projets un peu vieillissants. Elle ressemble à ça :
const taxe = commande.getClient().getAdresse().getPays().getTauxTVA();
À première vue, elle n'a rien d'anormal. Elle va chercher la TVA d'une commande en traversant les objets. Elle compile, elle fonctionne, les tests passent. Sauf que six mois plus tard, quelqu'un décide de changer la manière dont un pays expose son taux de TVA. Et là, vingt-sept endroits dans le code cassent d'un coup, dont certains que personne ne se rappelait écrits comme ça.
La loi de Déméter, ou Law of Demeter en anglais, est le principe qui aurait empêché cette situation. Elle est moins connue que SOLID ou DRY, mais elle s'applique à une situation très concrète que tout développeur rencontre.
Le problème concret : les "train wrecks"
On appelle "train wreck" en anglais (littéralement "train qui déraille") ce genre de ligne où les points s'enchaînent les uns à la suite des autres, comme les wagons d'un train. Chaque point est une promesse implicite sur la structure interne de l'objet précédent.
// Exemple 1
user.getProfil().getPreferences().getLangue().getCode();
// Exemple 2
panier.getArticles()[0].getProduit().getCategorie().getParent().getNom();
// Exemple 3
request.getSession().getUtilisateur().getRole().getPermissions().contains('admin');
Chacune de ces lignes écrite à un moment donné crée une dépendance invisible. L'objet qui l'écrit ne dépend pas seulement de son voisin direct. Il dépend de toute la structure de données derrière. Si Preferences change son interface, c'est user qui est impacté, alors que user n'aurait jamais dû connaître Preferences directement.
Ce problème n'apparaît pas au moment où on écrit la ligne. Il se révèle au moment où quelque chose doit changer. Et c'est là le coût réel : pas dans l'écriture, mais dans la maintenance.
Ce que dit la loi de Déméter, concrètement
Formulée en 1987 par Ian Holland à la Northeastern University de Boston, dans le cadre d'un projet de recherche baptisé Demeter (en référence à Déméter, la déesse grecque de l'agriculture), la loi s'énonce en une phrase :
"Parle seulement à tes amis proches, pas aux amis de tes amis." Ian Holland, 1987
En termes opérationnels, une méthode d'un objet peut appeler uniquement :
- Ses propres méthodes (c'est-à-dire les méthodes de la classe elle-même).
- Les méthodes des objets passés en paramètre.
- Les méthodes des objets qu'elle instancie elle-même.
- Les méthodes des attributs directs de l'objet (pas les attributs des attributs).
Tout le reste est une violation. Concrètement, si vous devez écrire plus d'un point dans une chaîne d'appels pour atteindre une donnée, vous êtes probablement en train de violer la loi de Déméter.
Refactoriser une violation : un exemple complet
Reprenons l'exemple de la TVA. Voici le code d'origine, dans un contexte e-commerce :
class Facture {
calculerMontant(commande) {
const total = commande.getTotalHT();
const taux = commande.getClient().getAdresse().getPays().getTauxTVA();
return total * (1 + taux);
}
}
La classe Facture connaît ici la structure interne de Commande, de Client, de Adresse et de Pays. Quatre classes dépendantes pour une seule ligne.
La version respectueuse de Déméter pousse la logique à l'intérieur des objets :
class Facture {
calculerMontant(commande) {
return commande.getMontantTTC();
}
}
class Commande {
getMontantTTC() {
return this.totalHT * (1 + this.client.getTauxTVAApplicable());
}
}
class Client {
getTauxTVAApplicable() {
return this.adresse.getTauxTVA();
}
}
class Adresse {
getTauxTVA() {
return this.pays.getTauxTVA();
}
}
À première vue, on a ajouté du code. En réalité, on a transféré la connaissance au bon endroit. La classe Facture ne sait plus rien de Pays. Si la manière de calculer la TVA change (par exemple avec une TVA qui dépend aussi du type de produit), seule la chaîne interne Client → Adresse → Pays est impactée, pas tous les appelants de Facture.
C'est le cœur du gain : on échange une connaissance globale de la structure contre une série de connaissances locales, chacune limitée à son voisin direct.
Le lien avec "Tell, Don't Ask"
La loi de Déméter va de pair avec un autre principe, plus récent : "Tell, Don't Ask" (Dis-lui, ne lui demande pas). L'idée est la même, exprimée autrement : au lieu de demander à un objet ses données pour faire le calcul à sa place, dites-lui de faire le calcul.
// "Ask" : on demande les données et on fait le boulot à sa place
if (compte.getSolde() >= montant) {
compte.setSolde(compte.getSolde() - montant);
}
// "Tell" : on délègue la responsabilité à l'objet concerné
compte.debiter(montant);
La version "Tell" respecte naturellement Déméter : l'appelant ne sait plus comment le solde est stocké, ni même qu'il existe sous cette forme. Il fait confiance à Compte pour gérer sa propre cohérence. Ce glissement de responsabilité est l'esprit même du principe de responsabilité unique vu dans l'article SOLID.
Les situations où la loi ne s'applique pas
Appliquer Déméter de manière dogmatique est une erreur. Il existe plusieurs situations où enchaîner les appels est parfaitement sain.
Les fluent APIs et les builders
Quand on écrit :
const query = queryBuilder
.select('nom', 'email')
.from('utilisateurs')
.where('age', '>=', 18)
.orderBy('nom')
.limit(10);
Chaque appel retourne le même builder. Il n'y a pas de traversée d'objets différents, c'est une convention d'écriture fluide. Pareil pour Lodash, jQuery, les chaînes de promises, Pandas, les streams Java. Ce ne sont pas des violations de Déméter.
Les structures de données pures (DTO, records)
Un objet qui sert uniquement à transporter des données, sans logique métier, n'a pas d'encapsulation à protéger. Accéder à dto.utilisateur.email est lisible et direct. La loi de Déméter parle d'objets métier, pas de structures de données passives. C'est d'ailleurs une distinction importante : un DTO n'est pas un objet au sens orienté objet strict du terme.
Les frameworks qui imposent une structure
En développement web, certains frameworks obligent à accéder à des propriétés imbriquées. Par exemple, dans une requête Express, req.body.user.email est une pratique courante et acceptée. Lutter contre ces conventions ne serait pas pragmatique. Ce qui compte, c'est qu'une fois les données extraites du framework, elles soient encapsulées proprement dans vos propres objets métier.
Les signaux qui indiquent une violation
Dans la pratique, repérer une violation de Déméter est relativement simple. Voici les indices les plus fiables.
Trois points ou plus dans une même expression
La règle empirique la plus rapide. a.b().c().d() est déjà suspect. Il ne s'agit pas d'une interdiction absolue, mais chaque cas de ce type mérite une question : cet objet devrait-il vraiment connaître la structure interne de ce qu'il manipule ?
Un test unitaire qui oblige à mocker trois niveaux d'objets
Quand un test commence à ressembler à mockCommande.getClient().getAdresse().getPays() avec trois niveaux de stubs à configurer, c'est que le code testé connaît trop de choses. Chaque mock imbriqué est une dépendance implicite.
Le bug en cascade après un refactor local
Vous modifiez un détail dans une classe et, subitement, une dizaine de tests échouent dans des fichiers qui n'ont aucun lien apparent avec cette classe. Le couplage invisible était en réalité assuré par des chaînes d'appels qui traversaient la classe modifiée.
Le besoin fréquent d'importer des classes "à travers"
Un fichier qui importe Pays alors qu'il manipule uniquement des Commande est un signal. L'import lui-même raconte que le code traverse une chaîne d'objets qu'il ne devrait pas connaître.
Ce que Déméter apporte vraiment
Respecter ce principe produit des effets qui se cumulent à mesure que le projet grandit.
Moins de couplage invisible. Chaque classe ne connaît que ses voisins directs. Le graphe de dépendance du projet devient plus plat, plus prévisible. Un changement dans une classe a un rayon d'impact limité.
Des tests plus simples. Tester une méthode ne demande plus de configurer des mocks sur trois niveaux. Les dépendances directes suffisent. C'est un gain énorme sur la durée de vie d'un projet, parce que tester devient moins coûteux, donc on teste plus, donc la qualité grimpe naturellement.
Une API métier plus parlante. Appeler commande.getMontantTTC() est plus explicite que de recalculer le TTC à l'extérieur en traversant quatre objets. L'intention du code devient visible directement dans la méthode appelée.
Un refactoring plus sûr. Quand la structure interne d'un objet doit changer, seul cet objet et ses voisins directs sont concernés. Pas de ripple effect à travers tout le projet.
Déméter et les autres principes de code
La loi de Déméter n'est pas un principe isolé. Elle s'articule avec l'écosystème complet des bonnes pratiques de développement.
Elle renforce directement le principe de responsabilité unique (SRP) vu dans SOLID. Quand une classe doit déléguer à un voisin plutôt que traverser ses propres données, elle concentre naturellement ses responsabilités.
Elle prolonge la Separation of Concerns au niveau des interactions entre objets. Ce que la séparation des préoccupations fait à l'échelle d'une application, Déméter le fait à l'échelle d'une conversation entre deux objets.
Elle peut tensionner avec DRY. Quand chaque couche doit exposer une méthode pour relayer une information vers sa voisine, on écrit parfois du code qui ressemble à de la duplication. Ce n'est pas le cas : chaque méthode a un sens dans sa couche, même si elle se contente d'appeler la couche suivante. Le nom et la couche lui donnent sa raison d'être.
Elle peut aussi entrer en tension avec KISS. Une application stricte de Déméter sur un tout petit projet peut ajouter de la cérémonie inutile. Le bon équilibre dépend de la taille et de la durée de vie du projet. Sur une API de trois endpoints pour un hackathon, un user.address.city ne mérite pas une refactorisation. Sur une application métier qui va vivre cinq ans, oui.
Comment s'entraîner au réflexe Déméter
Comme tous les principes de code, celui-ci s'acquiert par l'exposition régulière à du code propre et par la pratique sur des projets réels. Quelques habitudes accélèrent l'apprentissage.
Pendant les revues de code, chaque fois que vous voyez un train wreck, prenez le temps de réfléchir à ce qu'il faudrait changer. Même si vous ne modifiez pas la ligne, formuler l'alternative ancre le réflexe. Au bout de quelques semaines, vous verrez ces chaînes avant même de les écrire.
Écrire des tests unitaires est aussi un excellent révélateur. Dès qu'un test devient inconfortable à écrire, cherchez la cause dans le code testé. Souvent, c'est une violation de Déméter qui se cache derrière la complexité du mock. Les formations LaPolaris sur PHP orienté objet, les fondamentaux de Java et POO et Python POO mettent cet aspect au centre des exercices pratiques.
Pour aller plus loin sur l'architecture globale d'une application bien découplée, la formation Symfony 7 offre un terrain d'exercice idéal. Le framework encourage nativement un découpage en services et entités qui se prête bien à l'application de Déméter.
FAQ
La loi de Déméter interdit-elle toute chaîne d'appels avec plusieurs points ?
Non, c'est une règle empirique, pas une interdiction absolue. Les fluent APIs, les builders, les chaînes sur des structures de données pures (DTO, records) sont acceptables parce qu'elles ne traversent pas réellement plusieurs objets métier distincts. Ce que la loi combat, c'est la connaissance de la structure interne d'objets lointains, pas le fait d'écrire plusieurs points à la suite.
Est-ce que respecter Déméter produit toujours plus de code ?
Souvent oui, mais ce code supplémentaire a une valeur. Chaque méthode relais qu'on ajoute encapsule une connaissance locale et protège les appelants contre un changement futur. Sur un projet qui vit longtemps, ce surcoût initial est largement compensé par le gain en maintenabilité. Sur un projet court, il peut être disproportionné.
Déméter s'applique-t-il dans les langages qui ne sont pas orientés objet ?
L'esprit du principe, oui. L'idée de ne pas faire dépendre un module d'un détail interne d'un autre module se transpose à la programmation fonctionnelle ou procédurale. En Go, par exemple, la discipline équivalente est de ne pas exposer les champs internes d'une structure et de proposer des fonctions qui encapsulent les opérations. Le vocabulaire change, le principe reste.
Comment gérer Déméter dans une API REST ou GraphQL ?
Les réponses JSON sont des structures de données, pas des objets métier. Il est normal d'y accéder par chaînes de propriétés côté client. Ce qui compte, c'est que ces données soient encapsulées dans des objets métier dès qu'elles entrent dans la couche de logique applicative. La frontière est claire : dans le transport, Déméter ne s'applique pas, dans le domaine, si.
Est-ce que Déméter est compatible avec le développement frontend ?
Oui, notamment au niveau des composants et des stores. Un composant ne devrait pas plonger dans la structure interne du store pour extraire ses données. Un sélecteur ou un hook dédié (comme avec Redux Toolkit, Zustand ou React Query) applique la loi de Déméter en exposant une API claire. Les formations React et JavaScript avancé donnent des exemples concrets de ce découpage.
Quelle différence entre Déméter et le principe d'encapsulation classique ?
L'encapsulation dit "cache tes données derrière des méthodes". Déméter va plus loin : "ne laisse pas tes utilisateurs deviner tes collaborateurs internes". Un objet peut être parfaitement encapsulé (avec des getters et setters propres) tout en violant Déméter si ces getters retournent d'autres objets qu'il faut ensuite manipuler. La bonne encapsulation exige aussi une bonne discipline sur les chaînes d'appels.
Peut-on introduire Déméter dans un projet existant sans tout réécrire ?
Oui, par itérations. La meilleure entrée consiste à identifier les train wrecks les plus fréquents dans le code et à les transformer un par un, en ajoutant les méthodes relais manquantes dans les bonnes classes. Chaque refactor isolé améliore la zone concernée sans perturber le reste. Au fil des mois, la tendance globale du projet bascule naturellement vers plus de respect du principe.
Existe-t-il des outils qui détectent les violations de Déméter ?
Quelques linters et analyseurs statiques signalent les chaînes d'appels longues, avec des règles configurables sur le nombre maximal de points autorisés. PHPStan, ESLint avec certaines règles personnalisées, SonarQube en détectent une partie. Aucun outil ne capture le principe à 100%, parce que la nuance entre fluent API et vraie violation demande un jugement contextuel. Ces outils sont utiles comme garde-fou, pas comme substitut à la revue de code.