Code smells & heuristiques
Un catalogue d'odeurs de code et d'heuristiques concrètes pour reconnaître — et corriger — le code qui sent mauvais.
Le dernier chapitre de Clean Code ne ressemble à aucun autre. Ce n'est pas un exposé linéaire mais un catalogue : la liste, numérotée et classée par thème, des symptômes que Robert C. Martin a appris à repérer en relisant des milliers de lignes. Chaque entrée porte un code — C1, G5, N4, T9 — pour qu'on puisse y faire référence en revue de code, comme un médecin nomme un symptôme avant de poser un diagnostic.
Plus qu'une checklist, c'est une table des matières inversée : derrière chaque odeur se cache un principe défendu tout au long du livre. Plutôt que de réciter les soixante-six entrées une à une, regroupons-les par famille — commentaires, environnement, fonctions, règles générales, noms, tests — et rendons chacune tangible avec un avant/après en TypeScript. Le but n'est pas de mémoriser la liste, mais de développer le nez : sentir, en lisant, que quelque chose cloche.
Le code propre ne naît pas d'une liste de règles. On ne devient pas artisan en apprenant des heuristiques par cœur. Le professionnalisme vient de valeurs qui nourrissent une discipline.
Commentaires : ce qui pourrit en silence
Les commentaires vieillissent mal. Le livre consacre cinq entrées (C1 à C5) aux façons dont ils dérapent.
| Code | Odeur | Remède |
|---|---|---|
C1 | Information inappropriée (auteur, dates, historique) | La laisser au gestionnaire de versions |
C2 | Commentaire obsolète | Le mettre à jour ou le supprimer |
C3 | Commentaire redondant | Laisser parler le code |
C4 | Commentaire mal écrit | Soigner ou s'abstenir |
C5 | Code commenté | Le supprimer sans pitié |
L'information inappropriée (C1), c'est le commentaire qui héberge des métadonnées que d'autres outils gèrent mieux : historiques de changements, numéros de tickets, noms d'auteurs. Tout cela encombre le fichier alors que Git le retient déjà.
Le commentaire redondant (C3) répète ce que le code dit déjà, et le commentaire obsolète (C2) ment carrément, car il a dérivé loin du code qu'il décrivait.
// ❌ Avant — C3 : redondant + C2 : devenu faux
// Renvoie le prix TTC avec une TVA de 19,6 %
function prixTTC(prixHT: number): number {
return prixHT * 1.2; // le taux a changé, pas le commentaire
}
// ✅ Après — le code se suffit, la constante porte le sens
const TAUX_TVA = 0.2;
function prixTTC(prixHT: number): number {
return prixHT * (1 + TAUX_TVA);
} Piège courant
Le code commenté (C5) est qualifié d'« abomination » par Martin. Personne n'ose le supprimer, alors il pourrit : il appelle des fonctions disparues, suit des conventions mortes. Supprimez-le. Git s'en souvient si quelqu'un en a besoin un jour.
Environnement : tout doit tenir en une commande
Deux heuristiques (E1, E2) tiennent en une phrase : construire et tester doivent être triviaux.
E1— Cloner puis builder le projet doit se faire en une commande, pas en une chasse au trésor de fichiers de config et de scripts ésotériques.E2— Lancer la suite de tests doit se faire en une commande, idéalement un clic dans l'IDE.
# ✅ L'idéal : deux commandes, et c'est tout
git clone git@github.com:acme/boutique.git
cd boutique && npm ci && npm test Si lancer les tests demande de configurer une base, d'éditer trois variables d'environnement et de prier, alors les tests ne seront pas lancés. Un test qu'on ne lance pas ne protège rien.
Fonctions : signatures qui mentent
Les quatre heuristiques sur les fonctions (F1 à F4) reviennent toutes à la même exigence : une signature doit être honnête et minimale.
F1Trop d'arguments — zéro est l'idéal, puis un, deux, trois ; au-delà, c'est très suspect.F2Arguments de sortie — un argument qu'on modifie pour renvoyer un résultat prend le lecteur à contre-pied.F3Arguments drapeau — un booléen en paramètre crie que la fonction fait deux choses.F4Fonction morte — une fonction jamais appelée doit être supprimée.
// ❌ Avant — F1 + F3 : trop d'arguments, dont un drapeau
function creerCommande(
client: Client,
lignes: Ligne[],
express: boolean,
cadeau: boolean,
): Commande { /* ... */ }
creerCommande(client, lignes, true, false); // que veut dire true ? // ✅ Après — un objet d'options nommées, intentions claires
type OptionsLivraison = { express?: boolean; cadeau?: boolean };
function creerCommande(
client: Client,
lignes: Ligne[],
options: OptionsLivraison = {},
): Commande { /* ... */ }
creerCommande(client, lignes, { express: true }); Pour F2, le réflexe est clair : si une fonction doit modifier un état, qu'elle modifie l'objet sur lequel elle est appelée, plutôt qu'un paramètre transformé en variable de sortie.
Règles générales : le cœur du catalogue
La section G est la plus riche (trente-six entrées). Voici les plus marquantes, regroupées par intention.
Éliminer la duplication (G5)
Chaque duplication dans le code est une occasion manquée d'abstraction.
C'est, selon Martin, l'une des règles les plus importantes du livre — le principe DRY (Don't Repeat Yourself). La duplication évidente se replie dans une fonction ; la chaîne if/else répétée se remplace par du polymorphisme ; les algorithmes jumeaux se factorisent via un patron (Template Method, Strategy).
// ❌ Avant — la même validation copiée-collée
function inscrire(email: string) {
if (!email.includes("@")) throw new Error("email invalide");
// ...
}
function abonnerNewsletter(email: string) {
if (!email.includes("@")) throw new Error("email invalide");
// ...
}
// ✅ Après — une seule source de vérité
function assertEmailValide(email: string): void {
if (!email.includes("@")) throw new Error("email invalide");
} Ranger au bon niveau (G6, G8)
Le code au mauvais niveau d'abstraction (G6) mélange les concepts généraux et les détails. L'exemple du livre : une interface Stack qui expose percentFull(), alors que toutes les piles n'ont pas de notion de capacité. Cette méthode appartient à une interface dérivée BoundedStack.
Le « trop d'information » (G8) en est le cousin : les bons modules ont des interfaces minces. Moins une classe expose de méthodes, plus le couplage reste faible.
// ❌ Avant — G6 : percentFull ne concerne pas toutes les piles
interface Pile<T> {
empiler(x: T): void;
depiler(): T;
pourcentageRempli(): number; // n'a de sens que si bornée
}
// ✅ Après — concept de capacité isolé dans une interface dédiée
interface Pile<T> {
empiler(x: T): void;
depiler(): T;
}
interface PileBornee<T> extends Pile<T> {
pourcentageRempli(): number;
} Supprimer le mort et l'incohérent (G9, G11)
Le code mort (G9) — branche d'un if impossible, catch qui ne se déclenche jamais — finit par sentir car il n'évolue plus avec le reste. On lui offre un enterrement décent : la suppression.
L'incohérence (G11) trahit le principe de moindre surprise : si vous nommez une méthode traiterDemandeVerification, nommez sa voisine traiterDemandeSuppression, pas deletionHandler. Faites une chose d'une certaine façon, puis faites toutes les choses similaires de la même façon.
Replacer le comportement où sont les données (G14)
L'envie de fonctionnalité (feature envy) est l'un des smells de Martin Fowler. Une méthode qui passe son temps à lire les accesseurs d'un autre objet « envie » la classe de cet objet : elle aimerait y vivre.
// ❌ Avant — feature envy : tout vient de l'employe
class CalculateurPaie {
payeHebdo(employe: Employe): number {
const taux = employe.getTauxHoraire();
const heures = employe.getHeuresTravaillees();
const sup = Math.max(0, heures - 35);
return (heures - sup) * taux + sup * taux * 1.5;
}
}
// ✅ Après — le calcul vit là où sont les données
class Employe {
payeHebdo(): number {
const sup = Math.max(0, this.heures - 35);
return (this.heures - sup) * this.taux + sup * this.taux * 1.5;
}
} Bannir les arguments sélecteurs (G15)
Cousin du drapeau (F3), l'argument sélecteur (G15) est ce false pendouillant en fin d'appel qui ne dit rien. Il fusionne plusieurs fonctions en une seule, par paresse. Mieux vaut plusieurs fonctions courtes au nom explicite.
// ❌ Avant — calculerPaie(false) : que sélectionne ce booléen ?
function calculerPaie(heuresSup: boolean): number { /* ... */ }
// ✅ Après — deux fonctions au nom parlant
function payeNormale(): number { /* ... */ }
function payeAvecHeuresSup(): number { /* ... */ } Rendre l'intention visible (G19, G20, G16)
Les variables explicatives (G19) découpent un calcul opaque en valeurs intermédiaires nommées. Et les noms de fonctions doivent dire ce qu'elles font (G20) : date.add(5) est ambigu — cinq jours ? heures ? la date est-elle mutée ? dateApresJours(5) répond avant même qu'on lise le corps.
// ❌ Avant — G19 : que sont m[1] et m[2] ?
const m = enTete.exec(ligne);
if (m) entetes.set(m[1].toLowerCase(), m[2]);
// ✅ Après — des noms rendent le module transparent
const correspondance = enTete.exec(ligne);
if (correspondance) {
const [, cle, valeur] = correspondance;
entetes.set(cle.toLowerCase(), valeur);
} Préférer le polymorphisme aux switch (G23)
Il ne peut y avoir plus d'un
switchpar type de sélection.
La plupart des switch existent parce qu'ils sont la solution de force brute, pas la bonne. La règle « ONE SWITCH » : chaque switch devrait être suspect, et ses cas devraient produire des objets polymorphes qui remplacent tous les autres switch du système.
// ❌ Avant — switch dispersé dans tout le système
function fraisLivraison(type: string): number {
switch (type) {
case "standard": return 4.9;
case "express": return 9.9;
case "retrait": return 0;
default: throw new Error("type inconnu");
}
}
// ✅ Après — polymorphisme : un seul point de sélection
interface ModeLivraison {
frais(): number;
}
class Standard implements ModeLivraison { frais() { return 4.9; } }
class Express implements ModeLivraison { frais() { return 9.9; } }
class Retrait implements ModeLivraison { frais() { return 0; } } Nommer les nombres magiques (G25)
Une des plus vieilles règles du métier : cacher les nombres bruts derrière des constantes nommées. 86400 devient SECONDES_PAR_JOUR. Et « nombre magique » vaut aussi pour les chaînes : "John Doe" dans un test gagne à devenir NOM_EMPLOYE_HORAIRE.
// ❌ Avant — que veut dire 86400 ?
if (Date.now() - creePar > 86400 * 1000) expirer();
// ✅ Après
const SECONDES_PAR_JOUR = 86_400;
const MS_PAR_SECONDE = 1_000;
if (Date.now() - creePar > SECONDES_PAR_JOUR * MS_PAR_SECONDE) expirer(); Note
Tous les nombres ne méritent pas une constante. circonference = rayon * Math.PI * 2 se lit très bien ; introduire une constante DEUX serait absurde. Le critère, c'est la lisibilité, pas le dogme.
Encapsuler les conditions (G28, G29, G33)
Trois heuristiques visent les conditions illisibles.
G28— Extrayez les conditions dans une fonction qui explique l'intention.G29— Préférez les conditions positives aux négatives, plus dures à lire.G33— Encapsulez les conditions limites, ces+1/-1éparpillés.
// ❌ Avant — G28 + G29 : logique brute et négation
if (!compte.estBloque() && compte.solde > 0 && !compte.estClos) {
// ...
}
// ✅ Après — la condition porte un nom, formulée positivement
if (compte.peutEtreDebite()) {
// ...
} // ❌ Avant — G33 : level + 1 répété, condition limite éparpillée
if (niveau + 1 < balises.length) {
analyser(corps, balises, niveau + 1, decalage);
}
// ✅ Après — la limite est encapsulée dans une variable
const niveauSuivant = niveau + 1;
if (niveauSuivant < balises.length) {
analyser(corps, balises, niveauSuivant, decalage);
} Une fonction = une chose (G30)
Une fonction qui enchaîne plusieurs « sections » fait plusieurs choses. On la découpe jusqu'à ce que chaque fonction ne fasse qu'une chose, à un seul niveau d'abstraction.
// ❌ Avant — boucle + condition + paiement, trois choses
function payer(employes: Employe[]): void {
for (const e of employes) {
if (e.estJourDePaie()) {
const montant = e.calculerPaie();
e.verserPaie(montant);
}
}
}
// ✅ Après — chaque fonction fait une chose
function payer(employes: Employe[]): void {
for (const e of employes) payerSiNecessaire(e);
}
function payerSiNecessaire(e: Employe): void {
if (e.estJourDePaie()) e.verserPaie(e.calculerPaie());
} Exposer les couplages temporels (G31)
Un couplage temporel caché (G31) survient quand l'ordre d'appel compte mais que rien ne l'impose. On le rend explicite par une « chaîne de seaux » : chaque fonction produit ce dont la suivante a besoin.
// ❌ Avant — rien n'empêche d'inverser l'ordre
class Importateur {
importer(): void {
this.validerSchema();
this.transformer(); // dépend de validerSchema, mais rien ne l'oblige
this.charger();
}
}
// ✅ Après — l'ordre est imposé par les paramètres
class Importateur {
importer(): void {
const schema = this.validerSchema();
const donnees = this.transformer(schema);
this.charger(donnees);
}
} Config au sommet, navigation courte (G35, G36)
La configuration (G35) — valeurs par défaut, ports, chemins — doit vivre au plus haut niveau d'abstraction, puis descendre par arguments. On ne l'enterre pas dans une fonction de bas niveau où if (port === 0) // 80 par défaut se cache.
La navigation transitive (G36), c'est la loi de Déméter : si A collabore avec B et B avec C, le code de A ne doit pas connaître C. On évite a.getB().getC().faire().
// ❌ Avant — G36 : navigation transitive, couplage à toute la chaîne
const ville = commande.getClient().getAdresse().getVille();
// ✅ Après — le collaborateur direct offre le service
const ville = commande.villeDeLivraison(); Astuce
La loi de Déméter se résume à « parlez à vos amis, pas aux amis de vos amis ». Si vous enchaînez les .getX().getY().getZ(), vous figez l'architecture : interposer un maillon obligera à modifier chaque appelant.
Noms : la moitié de la lisibilité
Quatre entrées de la section N méritent l'attention.
N1Noms descriptifs — les noms font 90 % de la lisibilité ; ne les choisissez pas à la hâte.N2Bon niveau d'abstraction —getConnectedPhoneNumber()enferme une interfaceModemdans la téléphonie ;getConnectedLocator()reste ouvert aux autres connexions.N4Noms non ambigus —doRename()ne dit rien ;renamePageAndOptionallyAllReferences()est long mais explicite.N7Décrire les effets de bord —getOos()qui crée l'objet s'il manque devrait s'appelercreateOrReturnOos().
// ❌ Avant — N7 : le nom cache un effet de bord (création)
function getConnexion(): Connexion {
if (!this.connexion) this.connexion = ouvrir();
return this.connexion;
}
// ✅ Après — le nom avoue ce qu'il fait vraiment
function obtenirOuCreerConnexion(): Connexion {
if (!this.connexion) this.connexion = ouvrir();
return this.connexion;
} À retenir
Un nom doit décrire tout ce qu'une fonction est ou fait. N'utilisez pas un verbe simple pour une action qui en cache une autre. Le lecteur doit pouvoir faire confiance au nom sans lire le corps.
Tests : insuffisants tue silencieusement
La section T rappelle que des tests médiocres sont presque pires que pas de tests, car ils donnent une fausse assurance.
| Code | Heuristique |
|---|---|
T1 | Tests insuffisants : testez tout ce qui peut casser |
T2 | Utilisez un outil de couverture pour voir les trous |
T3 | Ne sautez pas les tests triviaux : leur valeur documentaire dépasse leur coût |
T5 | Testez les conditions limites : on rate souvent les bords |
T9 | Les tests doivent être rapides — un test lent finit par être abandonné |
Le critère de suffisance (T1) n'est pas « ça semble assez », mais « toute condition est explorée, tout calcul validé ». La couverture (T2) rend les trous visibles. Et les conditions limites (T5) sont le terrain où l'on se trompe le plus.
// ✅ T5 : on teste les bords, pas seulement le cas nominal
test("remise volume aux conditions limites", () => {
expect(remise(0)).toBe(0); // panier vide
expect(remise(1)).toBe(0); // juste en dessous du seuil
expect(remise(10)).toBe(0.05); // pile au seuil
expect(remise(11)).toBe(0.05); // juste au-dessus
}); Attention
Un test lent est un test qu'on ne lance pas (T9). Quand le délai presse, ce sont les tests lents qu'on retire de la suite — et la protection s'évanouit au pire moment. Gardez vos tests rapides coûte que coûte.
À retenir
- Ce chapitre est un catalogue de diagnostic : chaque code (
G5,N4,T9…) nomme une odeur pour en parler en revue, mais l'objectif est de développer le nez, pas de réciter la liste. - La duplication (
G5) est l'ennemi numéro un : chaque copie est une abstraction manquée. DRY, polymorphisme, patrons — éliminez-la partout. - Rendez l'intention visible : noms honnêtes (
N1,N7), variables explicatives (G19), conditions encapsulées et positives (G28,G29), constantes nommées (G25). - Faites une seule chose par fonction (
G30), avec peu d'arguments (F1), sans drapeau ni sélecteur (F3,G15). - Limitez le couplage : interfaces minces (
G8), config au sommet (G35), pas de navigation transitive (G36, loi de Déméter), couplages temporels rendus explicites (G31). - Des tests suffisants, rapides et axés sur les bords (
T1,T5,T9) ; le code mort et commenté (G9,C5), supprimé sans regret — Git s'en souvient.