Le design émergent
Les quatre règles du design simple de Kent Beck : tests, zéro duplication, expressivité, minimalisme.
Et s'il existait quatre règles simples qui, suivies au fil de l'écriture, faisaient émerger un bon design ? Pas un diagramme dessiné à l'avance sur un tableau blanc, mais une structure saine qui se révèle d'elle-même, à mesure que l'on code et que l'on nettoie. C'est la promesse des quatre règles du design simple formulées par Kent Beck, et que Robert C. Martin reprend à son compte.
L'idée est subtile mais puissante : on n'a pas besoin d'être un architecte chevronné pour produire du code bien conçu. Il suffit de respecter, dans l'ordre, quatre contraintes mécaniques. Le respect de ces contraintes pousse naturellement vers des principes comme la responsabilité unique (SRP) ou l'inversion des dépendances (DIP). Le bon design n'est pas planifié : il émerge.
Les quatre règles, par ordre d'importance
Selon Kent Beck, un design est « simple » s'il, dans cet ordre précis :
- fait passer tous les tests ;
- ne contient aucune duplication ;
- exprime l'intention du programmeur ;
- minimise le nombre de classes et de méthodes.
L'ordre n'est pas décoratif. La règle 1 prime sur tout le reste : un code expressif et sans duplication qui ne marche pas ne vaut rien. Les règles 2 à 4 s'appliquent ensuite, pendant le refactoring, une fois le filet de sécurité des tests en place.
Note
Les exemples du livre sont en Java. Nous les transposons ici en TypeScript idiomatique, dans un contexte e-commerce ou métier, pour rendre chaque règle tangible.
Règle 1 : faire passer tous les tests
Avant tout, un design doit produire un système qui se comporte comme prévu. Un système peut avoir une conception parfaite sur le papier ; s'il n'existe aucun moyen simple de vérifier qu'il fonctionne réellement, tout cet effort de conception devient suspect.
Un système qui ne peut être vérifié ne devrait jamais être déployé.
Voilà l'évidence centrale : un système testable est un système qu'on peut vérifier en permanence. Mais cette évidence cache une mécanique vertueuse, et c'est tout l'intérêt de la règle.
Pourquoi les tests améliorent le design
Rendre un système testable nous pousse vers un meilleur design, presque malgré nous. Pour qu'une classe soit facile à tester, elle doit être petite et n'avoir qu'une seule responsabilité. Une classe qui fait dix choses exige une mise en place de test cauchemardesque ; une classe qui n'en fait qu'une se teste en trois lignes. Plus on écrit de tests, plus on est tiré vers le respect du SRP.
Prenons un service de commande qui mélange tout :
// ❌ Avant : impossible à tester sans une vraie base
// de données et un vrai serveur SMTP.
class OrderService {
passerCommande(panier: Panier): void {
const total = panier.lignes.reduce(
(s, l) => s + l.prix * l.quantite,
0,
);
db.execute("INSERT INTO orders ...", [total]);
smtp.send(panier.client.email, "Merci pour votre commande");
}
} Pour tester passerCommande, il faut une base et un serveur mail réels : c'est lent, fragile, et le couplage fort rend tout test unitaire pénible. Cette douleur est un signal. En la suivant, on découpe les responsabilités et on injecte les dépendances :
// ✅ Après : responsabilités séparées, dépendances injectées.
class CalculateurTotal {
calculer(panier: Panier): number {
return panier.lignes.reduce(
(somme, ligne) => somme + ligne.prix * ligne.quantite,
0,
);
}
}
class OrderService {
constructor(
private readonly total: CalculateurTotal,
private readonly commandes: DepotCommandes,
private readonly notifieur: Notifieur,
) {}
passerCommande(panier: Panier): void {
const montant = this.total.calculer(panier);
this.commandes.enregistrer(panier.client, montant);
this.notifieur.confirmerCommande(panier.client);
}
} CalculateurTotal se teste désormais sans aucune infrastructure. OrderService se teste avec des doublures (mocks) injectées. Le couplage fort empêche d'écrire des tests ; donc plus on écrit de tests, plus on adopte le DIP, les interfaces et l'injection de dépendances pour réduire ce couplage.
Astuce
Remarquable enchaînement : une règle aussi banale que « écris des tests et exécute-les en continu » améliore directement les deux objectifs cardinaux de l'orienté objet, le faible couplage et la forte cohésion. Écrire des tests conduit à de meilleurs designs.
Règles 2 à 4 : le refactoring sans peur
Une fois les tests en place, nous sommes libres de garder le code propre. On procède par refactoring incrémental : pour chaque poignée de lignes ajoutées, on s'arrête et l'on réfléchit. Vient-on de dégrader le design ? Si oui, on nettoie, puis on relance les tests pour prouver que rien n'est cassé.
C'est précisément le filet des tests qui élimine la peur : nettoyer du code ne risque plus de le casser silencieusement. C'est pendant cette étape de refactoring qu'on mobilise tout le corpus du bon design — augmenter la cohésion, diminuer le couplage, séparer les préoccupations, raccourcir fonctions et classes, choisir de meilleurs noms — et qu'on applique les trois dernières règles : éliminer la duplication, garantir l'expressivité, minimiser le nombre de classes et de méthodes.
Règle 2 : aucune duplication
La duplication est l'ennemie numéro un d'un système bien conçu. Elle représente du travail en plus, du risque en plus, et de la complexité inutile en plus : chaque copie devra être trouvée et corrigée le jour où la règle change.
La duplication prend bien des formes. Des lignes strictement identiques, évidemment. Mais aussi des lignes similaires, qu'on peut souvent rapprocher pour les rendre identiques et donc factorisables. Et la duplication peut être plus sournoise encore : une duplication d'implémentation. Dans une collection, on pourrait par exemple maintenir séparément un booléen pour estVide() et un compteur pour taille(). Mieux vaut relier l'un à l'autre :
// ✅ estVide() est définie en fonction de taille() :
// une seule source de vérité.
estVide(): boolean {
return this.taille() === 0;
} La réutilisation « en petit »
Créer un système propre exige la volonté d'éliminer la duplication, même sur quelques lignes. Reprenons l'exemple emblématique du livre — un traitement d'image — où deux méthodes partagent une fin identique :
// ❌ Avant : la séquence de remplacement de l'image
// est dupliquée dans les deux méthodes.
class Image {
redimensionner(cible: number, source: number): void {
if (Math.abs(cible - source) < SEUIL_ERREUR) return;
let facteur = cible / source;
facteur = Math.floor(facteur * 100) * 0.01;
const nouvelle = Outils.redimensionner(this.op, facteur);
this.op.liberer();
this.op = nouvelle;
}
pivoter(degres: number): void {
const nouvelle = Outils.pivoter(this.op, degres);
this.op.liberer();
this.op = nouvelle;
}
} Les trois dernières lignes de chaque méthode sont identiques. On les extrait :
// ✅ Après : la duplication est extraite dans
// une seule méthode privée.
class Image {
redimensionner(cible: number, source: number): void {
if (Math.abs(cible - source) < SEUIL_ERREUR) return;
let facteur = cible / source;
facteur = Math.floor(facteur * 100) * 0.01;
this.remplacer(Outils.redimensionner(this.op, facteur));
}
pivoter(degres: number): void {
this.remplacer(Outils.pivoter(this.op, degres));
}
private remplacer(nouvelle: OpImage): void {
this.op.liberer();
this.op = nouvelle;
}
} En extrayant la communauté à ce niveau minuscule, on commence à repérer des violations du SRP. On déplacera peut-être remplacer vers une autre classe, ce qui en augmente la visibilité. Un collègue y verra alors l'occasion d'abstraire davantage et de réutiliser la méthode ailleurs. Cette « réutilisation en petit » peut faire fondre la complexité globale. Savoir la pratiquer est la clé pour atteindre la réutilisation « en grand ».
Template Method : factoriser la structure
Quand la duplication est plus haute que quelques lignes — un algorithme entier répété avec une seule variation — le patron Template Method est l'outil. Voici une politique de congés dupliquée par région :
// ❌ Avant : l'algorithme est dupliqué ;
// seul le calcul des minimums légaux diffère.
class PolitiqueConges {
cumulerCongesUS(): void {
// calculer les heures de base
// garantir les minimums US
// appliquer à la paie
}
cumulerCongesEU(): void {
// calculer les heures de base
// garantir les minimums EU
// appliquer à la paie
}
} Le squelette est identique ; seul le calcul des minimums légaux change selon la région. Template Method capture le squelette dans la classe de base et laisse un « trou » que les sous-classes remplissent :
// ✅ Après : le squelette est défini une fois ;
// les sous-classes remplissent le seul trou variable.
abstract class PolitiqueConges {
cumuler(): void {
this.calculerHeuresDeBase();
this.ajusterMinimumsLegaux();
this.appliquerALaPaie();
}
private calculerHeuresDeBase(): void {
/* commun */
}
protected abstract ajusterMinimumsLegaux(): void;
private appliquerALaPaie(): void {
/* commun */
}
}
class PolitiqueUS extends PolitiqueConges {
protected ajusterMinimumsLegaux(): void {
/* logique US */
}
}
class PolitiqueEU extends PolitiqueConges {
protected ajusterMinimumsLegaux(): void {
/* logique EU */
}
} Les sous-classes ne fournissent que le fragment qui n'est pas dupliqué. Tout le reste vit une seule fois.
À retenir
Toute duplication est une décision de conception dupliquée. Le jour où la règle change — un nouveau taux, une nouvelle étape — vous voulez n'avoir qu'un seul endroit à modifier, pas la chasse au trésor.
Règle 3 : exprimer l'intention
La plus grande part du coût d'un projet logiciel se situe dans la maintenance à long terme. Il est facile d'écrire du code qu'on comprend au moment où on l'écrit, car on est alors plongé dans le problème. Mais les futurs mainteneurs n'auront pas cette compréhension — et le plus probable de ces futurs lecteurs, c'est vous-même dans six mois.
Le code doit donc clairement exprimer l'intention de son auteur. Plus l'auteur rend le code clair, moins les autres passeront de temps à le comprendre, et plus on réduit les défauts et le coût de maintenance. Comment s'exprimer ?
| Levier | Comment l'appliquer |
|---|---|
| Bons noms | Entendre le nom d'une classe ou fonction sans être surpris en découvrant ce qu'elle fait. |
| Petites unités | Des classes et fonctions courtes sont faciles à nommer, à écrire et à lire. |
| Vocabulaire standard | Les noms de patrons (Command, Visitor, Observer) décrivent un design en un mot. |
| Tests lisibles | Des tests unitaires bien écrits servent de documentation par l'exemple. |
Comparons un calcul d'éligibilité à la livraison gratuite, d'abord muet :
// ❌ Avant : le lecteur doit décrypter chaque condition.
function check(o: Order, u: User): boolean {
return (
o.total > 50 &&
!u.flags.includes(3) &&
u.country === "FR"
);
} Puis le même code, qui dit ce qu'il fait :
// ✅ Après : chaque intention porte un nom.
const SEUIL_LIVRAISON_GRATUITE = 50;
function donneDroitLivraisonGratuite(
commande: Commande,
client: Client,
): boolean {
const montantSuffisant =
commande.total > SEUIL_LIVRAISON_GRATUITE;
const livrableEnFrance = client.pays === "FR";
return (
montantSuffisant &&
!client.estBloque &&
livrableEnFrance
);
} Le comportement est identique, mais le second se lit comme une phrase. Le flags.includes(3) obscur est devenu estBloque, le 50 magique a un nom, et l'intention saute aux yeux.
Astuce
La manière la plus importante d'être expressif, c'est simplement d'essayer. Trop souvent, on fait marcher le code puis on file au problème suivant sans une pensée pour le prochain lecteur. Prenez un peu de fierté dans votre travail : passez quelques secondes sur chaque fonction pour mieux la nommer ou la découper. Le soin est une ressource précieuse.
Règle 4 : minimiser le nombre de classes et de méthodes
Même des principes aussi fondamentaux que l'élimination de la duplication, l'expressivité ou le SRP peuvent être poussés trop loin. À force de vouloir tout rendre minuscule, on peut créer une nuée de classes et de méthodes anémiques qui éparpillent la logique et nuisent à la lisibilité. Cette dernière règle rappelle qu'il faut aussi garder bas le nombre total d'unités.
Un nombre excessif de classes résulte souvent d'un dogmatisme stérile : une norme qui impose une interface pour chaque classe, ou des développeurs qui séparent systématiquement données et comportement dans des classes distinctes. Il faut résister à ce dogme et adopter une approche plus pragmatique.
| À éviter (dogme) | À préférer (pragmatisme) |
|---|---|
| Une interface pour chaque classe, par principe | Une interface quand il y a vraiment plusieurs implémentations |
| Séparer données et comportement en deux classes | Regrouper données et comportement quand c'est cohérent |
| Découper une fonction claire en cinq fragments | Garder une fonction lisible telle quelle |
Piège courant
Cette règle est la moins prioritaire des quatre. Il est important de garder le compte d'unités bas, mais il est plus important d'avoir des tests, d'éliminer la duplication et de s'exprimer clairement. Ne sacrifiez jamais les règles 1 à 3 sur l'autel du nombre de fichiers.
Pas de raccourci magique
Existe-t-il un ensemble de pratiques simples capable de remplacer l'expérience ? Clairement non. Mais les pratiques décrites dans ce chapitre — et dans tout le livre — sont une forme cristallisée de plusieurs décennies d'expérience accumulée par les auteurs. Suivre le design simple encourage et permet d'adhérer à de bons principes et patrons qui, autrement, prennent des années à maîtriser.
Le design émergent n'est donc pas une formule magique : c'est une discipline. On écrit des tests, on garde le filet vert, puis on nettoie sans peur — encore et encore. Au bout du chemin, on ne trouve pas le design qu'on avait dessiné à l'avance, mais souvent un meilleur : celui qui a émergé de la pression conjuguée de ces quatre règles.
À retenir
- Quatre règles, par ordre d'importance : tous les tests passent, zéro duplication, expressivité, minimalisme.
- La règle 1 prime : un système testable pousse vers de petites classes (SRP) et un faible couplage (DIP). Écrire des tests améliore le design.
- Les tests verts éliminent la peur du refactoring : c'est là qu'on applique les règles 2 à 4.
- La duplication est l'ennemie n°1 : extrayez méthodes et classes, et utilisez Template Method pour la duplication structurelle.
- Un code expressif révèle son intention par de bons noms, de petites unités, un vocabulaire de patrons standard et des tests-documentation.
- Minimisez le nombre d'unités sans dogmatisme — mais c'est la règle de plus faible priorité : ne sacrifiez jamais les trois premières pour elle.