Clean code et refactoring - 04 - Commentaires et documentation : quand le code ne suffit pas

Les bons commentaires expliquent le pourquoi, pas le quoi. JSDoc, TODO, types comme documentation : guide pratique pour commenter juste.

  1. 01 Clean code et refactoring - 00 - Pourquoi le clean code est un investissement, pas un luxe
  2. 02 Clean code et refactoring - 01 - Nommage : la competence la plus sous-estimee
  3. 03 Clean code et refactoring - 02 - Fonctions : courtes, claires, responsables
  4. 04 Clean code et refactoring - 03 - Conditions et lisibilité : sortir de la pyramide
  5. 05 Clean code et refactoring - 04 - Commentaires et documentation : quand le code ne suffit pas
  6. 06 Clean code et refactoring - 05 - Immutabilite et effets de bord : moins de surprises, moins de bugs
  7. 07 Clean code et refactoring - 06 - Gestion des erreurs propre : fail fast, fail loud
  8. 08 Clean code et refactoring - 07 - Programmation defensive vs offensive : valider aux frontieres, faire confiance a l'intérieur
  9. 09 Clean code et refactoring - 08 - SOLID en pratique avec TypeScript
  10. 10 Clean code et refactoring - 09 - DRY, KISS, YAGNI
  11. 11 Clean code et refactoring - 10 - Couplage et cohesion
  12. 12 Clean code et refactoring - 11 - Complexite cyclomatique
  13. 13 Clean code et refactoring - 12 - Abstractions prematurees vs tardives
  14. 14 Clean code et refactoring - 13 - Code smells
  15. 15 Clean code et refactoring - 14 - Techniques de refactoring
  16. 16 Clean code et refactoring - 15 - Refactoring legacy sans tout casser
  17. 17 Clean code et refactoring - 16 - Tests comme filet de sécurité pour le refactoring
  18. 18 Clean code et refactoring - 17 - Structurer un projet — feature-based vs layer-based
  19. 19 Clean code et refactoring - 18 - Constantes, configuration et magic numbers
  20. 20 Clean code et refactoring - 19 - Linting et formatting — ESLint, Biome, automatiser la qualité
  21. 21 Clean code et refactoring - 20 - Conventions d'équipe et ADR
  22. 22 Clean code et refactoring - 21 - Dette technique — quand elle est acceptable, quand elle tue le projet
  23. 23 Clean code et refactoring - 22 - Code review — donner et recevoir du feedback
  24. 24 Clean code et refactoring - 23 - Glossaire — tous les termes de la serie

04 - Commentaires et documentation : quand le code ne suffit pas

Ce que tu vas apprendre

  • Pourquoi "le bon code se documente tout seul" est un demi-mensonge
  • Quand un commentaire est nécessaire (et quand il est du bruit)
  • Les conventions TODO, FIXME, HACK
  • JSDoc pour les API publiques
  • Les types TypeScript comme forme de documentation

Prerequisites

03 - Conditions et lisibilité


Il y a un commentaire que je n'oublierai jamais. C'etait dans un service de paiement, ligne 847 :

typescript// Ne pas supprimer ce sleep. Sans lui, Stripe renvoie une erreur
// 429 parce qu'on appelle refund() trop vite apres charge().
// J'ai passe 3 jours a debugger ca en 2023. - Thomas
await sleep(500);

Ce commentaire est parfait. Il explique pourquoi ce sleep(500) est la. Sans lui, le prochain développeur qui passe voit un sleep suspect et le supprime pour "nettoyer le code". Et trois jours de debugging recommencent.

Le problème n'est pas les commentaires. C'est les mauvais commentaires.

"Le bon code se documente tout seul" : un demi-mensonge

Cette phrase contient du vrai. Si tes variables et fonctions sont bien nommees (voir l'article sur le nommage), beaucoup de commentaires deviennent inutiles :

typescript// Mauvais - le commentaire repete le code
// Verifie si l'utilisateur est actif
if (user.isActive) { ... }

// Verifie si le panier contient des articles
if (cart.items.length > 0) { ... }

// Calcule le total TTC
const totalTTC = totalHT * (1 + taxRate);

Ces commentaires n'apportent rien. Le code dit deja la meme chose. Les supprimer rend le fichier plus lisible.

Mais le code ne peut pas tout dire. Il dit le "quoi" et le "comment". Il ne dit jamais le "pourquoi". Pourquoi ce sleep(500) ? Pourquoi cette limite a 100 et pas 50 ? Pourquoi on n'utilise pas la méthode standard ? Le code ne répond pas a ces questions.

Les commentaires qui valent de l'or

Le "pourquoi" :

typescript// On trie par date de creation descendante plutot que par ID
// parce que les IDs ne sont pas sequentiels depuis la migration
// vers les UUIDs en janvier 2024.
const sorted = orders.sort((a, b) =>
  b.createdAt.getTime() - a.createdAt.getTime()
);

Le compromis technique :

typescript// On utilise une requete SQL brute ici au lieu de l'ORM parce que
// Prisma ne supporte pas les window functions. Ticket ouvert :
// https://github.com/prisma/prisma/issues/5602
const result = await db.$queryRaw`
  SELECT *, ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY score DESC) as rank
  FROM products
`;

L'avertissement :

typescript// ATTENTION : cette fonction est appelee 10 000 fois par seconde
// en pic de trafic. Toute allocation memoire ici impacte le GC.
// Profiler avant de modifier.
function matchRoute(path: string): Route | null {
  // ...
}

La référencé externe :

typescript// Algorithme de Luhn pour la validation de carte bancaire
// Ref: https://en.wikipedia.org/wiki/Luhn_algorithm
function isValidCardNumber(number: string): boolean {
  // ...
}

Les commentaires toxiques

Le commentaire qui ment :

typescript// Retourne la liste des utilisateurs actifs
function getUsers(): User[] {
  // En realite, retourne TOUS les utilisateurs
  // quelqu'un a change la query mais pas le commentaire
  return db.query("SELECT * FROM users");
}

Un commentaire faux est pire que pas de commentaire. Quand le code change, le commentaire doit changer. En pratique, ca n'arrive presque jamais. C'est pour ca que les commentaires qui paraphrasent le code sont dangereux : ils deviennent faux au premier refactoring.

Le commentaire bavard :

typescript/**
 * Cette methode est utilisee pour obtenir le nom complet de
 * l'utilisateur. Elle concatene le prenom et le nom de famille
 * en les separant par un espace. Si le prenom ou le nom de
 * famille est null ou undefined, une chaine vide est utilisee
 * a la place pour eviter d'afficher "null" ou "undefined" dans
 * l'interface utilisateur.
 */
function getFullName(user: User): string {
  return `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim();
}

Le code fait une ligne. Le commentaire en fait huit. Et il ne dit rien que le code ne dit pas deja.

Le code commente :

typescriptfunction calculatePrice(product: Product): number {
  // const oldPrice = product.basePrice * 1.1;
  // const discount = getSeasonalDiscount(product);
  // return oldPrice - discount;
  const price = product.basePrice * product.multiplier;
  // if (product.category === "electronics") {
  //   price *= 0.95;
  // }
  return price;
}

Du code commente, c'est du bruit. Tu as Git. Si tu as besoin de l'ancien code, il est dans l'historique. Supprime-le. Si ca te fait peur, fais un commit avant de supprimer. Mais supprime-le.

TODO, FIXME, HACK : les conventions qui marchent

typescript// TODO: implementer la pagination quand on aura plus de 1000 produits
// TODO(@nicolas): ajouter le support multi-devises - ticket PROJ-456

// FIXME: cette requete N+1 ralentit la page produits (voir monitoring)
// FIXME: race condition quand deux utilisateurs modifient le meme panier

// HACK: contournement du bug Chrome 118 avec les dates en timezone UTC+13
// HACK: Stripe API renvoie le montant en centimes sauf pour le JPY

Les regles :

  • TODO : fonctionnalité manquante, a faire plus tard. Inclus un ticket si possible.
  • FIXME : bug connu, a corriger. Plus urgent qu'un TODO.
  • HACK : solution temporaire qui marche mais qui est moche. Explique pourquoi.

Configure ton linter pour détecter les TODO sans ticket. Un TODO sans contexte ni proprietaire vieillit mal. Dans six mois, personne ne saura pourquoi il est la ni si c'est encore pertinent.

JSDoc pour les API publiques

Si tu ecris une librairie, un SDK, ou un module utilise par d'autres équipes, JSDoc est indispensable :

typescript/**
 * Formate un montant en devise lisible.
 *
 * @param amount - Montant en centimes (ex: 1999 pour 19.99 EUR)
 * @param currency - Code ISO 4217 de la devise (ex: "EUR", "USD")
 * @param locale - Locale BCP 47 pour le formatage (defaut: "fr-FR")
 * @returns Chaine formatee (ex: "19,99 EUR")
 *
 * @example
 * formatCurrency(1999, "EUR") // "19,99 EUR"
 * formatCurrency(1999, "USD", "en-US") // "$19.99"
 *
 * @throws {RangeError} Si la devise n'est pas un code ISO 4217 valide
 */
function formatCurrency(
  amount: number,
  currency: string,
  locale: string = "fr-FR"
): string {
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  }).format(amount / 100);
}

Pour le code interne, JSDoc est optionnel. Si les types TypeScript et les noms de paramètres sont explicites, ca suffit souvent. Ne fais pas du JSDoc pour faire du JSDoc.

Les types TypeScript comme documentation

Les types sont la meilleure forme de documentation : ils ne mentent pas (le compilateur le garantit) et ils sont toujours à jour.

typescript// Sans types - besoin d'un commentaire pour comprendre la structure
// config contient host (string), port (number), ssl (boolean),
// retries (number, optionnel, defaut 3)
function connect(config: any) { ... }

// Avec types - le commentaire est inutile
type DatabaseConfig = {
  host: string;
  port: number;
  ssl: boolean;
  retries?: number; // defaut: 3
};

function connect(config: DatabaseConfig) { ... }

Les unions discriminees documentent les états possibles :

typescripttype ApiResponse<T> =
  | { status: "success"; data: T }
  | { status: "error"; error: string; code: number }
  | { status: "loading" };

Pas besoin de commentaire pour expliquer les trois états possibles. Le type le fait. J'utilise beaucoup ce pattern dans mes projets, et j'en parle sur paltemps.fr dans le contexte de la gestion d'état.

Dead code vs code commente

Regle simple : le code mort doit mourir.

typescript// Mauvais - fonction jamais appelee
function legacyCalculation(items: Item[]): number {
  // 40 lignes de code que personne n'appelle
}

// Mauvais - import inutilise
import { oldHelper } from "./deprecated-utils";

// Mauvais - branche jamais atteinte
if (false) {
  migrateOldData();
}

Supprime tout ca. Git garde l'historique. ESLint avec no-unused-vars et no-unreachable attrape la plupart de ces cas. Active ces regles et ne les désactivé jamais.

Résumé

  • Les bons commentaires expliquent le "pourquoi", pas le "quoi"
  • Un commentaire qui paraphrase le code est du bruit
  • Un commentaire faux est pire que pas de commentaire
  • TODO/FIXME/HACK avec un ticket et un contexte
  • JSDoc pour les API publiques, optionnel pour le code interne
  • Les types TypeScript sont la meilleure documentation : toujours à jour, jamais faux
  • Le code mort et le code commente doivent etre supprimes

Article précédent : 03 - Conditions et lisibilité

Article suivant : 05 - Immutabilite et effets de bord

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.