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
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
- Robert C. Martin, "Clean Code", Chapter 4: Comments - https://www.oreilly.com/library/view/clean-code-a/9780136083238/
- JSDoc Référence - https://jsdoc.app/
- TypeScript Documentation on JSDoc - https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html