Neuf habitudes au niveau du code qui rendent le travail des ingénieurs seniors plus fiable
Il explore neuf pratiques concrètes de codage — allant des clauses de protection au modélisation stricte des données — qui rendent le code plus résilient, plus lisible et plus facile à déboguer en situation de pression.
Pendant longtemps, il semblait que l’écart entre les ingénieurs les plus expérimentés d’une équipe et tous les autres tenait uniquement à leurs connaissances techniques — un truc obscur lié à un framework, une API cachée, un raccourci que personne d’autre n’avait encore découvert.
En observant de près le travail des ingénieurs expérimentés, lors de sessions en binôme, d’analyses de code ou d’appels liés à des incidents, on découvre quelque chose de moins glamour. Ils ne sont pas nécessairement plus intelligents. Ils écrivent simplement du code qui résiste aux pannes accidentelles, ce qui rend les réparations bien plus faciles en cas de problème.
Neuf habitudes spécifiques se révèlent suffisamment fréquentes pour mériter d’être adoptées délibérément.
1. Préférer les clauses de protection aux pyramides de la catastrophe
Une erreur courante au début est de superposer des logiques de validation jusqu’à former une sorte d’escalier en retraits :
function processWithdrawal(account, amount) {
if (account.isActive) {
if (amount > 0) {
if (account.balance >= amount) {
return debit(account, amount);
}
}
}
throw new Error("Withdrawal failed");
}
Cela fonctionne, mais le texte devient illisible après la troisième condition, et la logique réelle du retrait se retrouve enfouie à trois niveaux de profondeur. L’approche plus expérimentée inverse l’ordre : on rejette immédiatement les cas invalides, puis la vraie logique s’exécute au niveau le plus haut, sans indentation :
function processWithdrawal(account, amount) {
if (!account.isActive) throw new AccountInactiveError();
if (amount <= 0) throw new InvalidAmountError();
if (account.balance < amount) throw new InsufficientFundsError();
return debit(account, amount);
}
Rien de particulièrement ingénieux ici. Il s’agit simplement d’un texte lisible sous pression, ce qui est essentiel justement lorsque c’est nécessaire — pendant un incident, à une heure du matin, à moitié endormi, en essayant de déterminer laquelle des cinq conditions imbriquées vous induit en erreur.
2. Des noms qui décrivent l’activité métier, pas le type de données
Donner aux variables des noms basés sur leur forme plutôt que sur leur signification — data, res, obj, list — fonctionne bien dans un seul fichier. En revanche, cela devient extrêmement problématique lorsqu’une base de code compte quarante fichiers, chacun définissant data comme désignant quelque chose de différent.
data = fetch(order_id)
if data["status"] == "done":
process(data)
Comparé à :
order = fetch_order(order_id)
if order.is_fulfilled:
archive_order(order)
La deuxième version indique ce que représente l’objet et quelle condition est réellement importante, sans forcer à remonter jusqu’à la fonction d’où elle provient. Cela coûte quelques caractères supplémentaires, mais permet à la personne qui déboguera ce code d’éviter de parcourir trois fichiers sans rapport.
3. Une seule interface entre votre code et le monde extérieur
Les API de tiers sont des narrateurs peu fiables. Les noms des champs changent, les structures imbriquées évoluent, les champs optionnels deviennent obligatoires puis redeviennent optionnels. Permettre aux réponses brutes des API d’être intégrées directement dans la logique métier transforme chacun de ces changements en une véritable chasse au trésor à travers le code.
// scattered everywhere
const price = apiResponse.line_items[0].unit_price_cents / 100;
La meilleure approche consiste à traduire la réponse une seule fois, précisément à la frontière entre les deux systèmes :
function toLineItem(raw) {
return {
label: raw.description,
priceInDollars: raw.unit_price_cents / 100,
};
}
Si le fournisseur renomme plus tard unit_price_cents en price, une seule fonction doit être modifiée. Tout ce qui suit reste inchangé et ne remarque jamais la différence.
4. Modèles de données qui ne peuvent pas mentir
Un type composé de une douzaine de champs optionnels est un type qui a cessé d’essayer de représenter la réalité avec précision :
type Ticket = {
id?: string;
assignee?: string;
resolvedAt?: Date;
resolution?: string;
};
Cette forme vous permet de créer un ticket « résolu » sans solution associée, ou un ticket « attribué » sans destinataire — des états qui devraient être impossibles mais se compilent sans problème. En séparant le type en fonction de l’état réel, on élimine toute cette catégorie de bugs :
type OpenTicket = { id: string; assignee?: string };
type ResolvedTicket = { id: string; assignee: string; resolvedAt: Date; resolution: string };
Une fonction qui envoie un e-mail de résumé de résolution peut désormais exiger spécifiquement un argument de type ResolvedTicket, et le compilateur garantit qu’aucun élément inachevé n’atteindra jamais cette fonction.
5. Séparer la question de la commande
Lorsque les règles métier et les effets secondaires se mêlent, les tests deviennent difficiles — et une fois que les tests sont difficiles, les règles cessent complètement d’être testées :
def promote_employee(employee_id):
emp = get_employee(employee_id)
if emp.tenure_months < 12:
raise Error("Not eligible yet")
if emp.current_rating < 3:
raise Error("Rating too low")
give_raise(emp)
notify_hr(emp)
log_promotion(emp)
En extrayant la vérification de l’éligibilité dans une fonction indépendante, vous pouvez tester cette règle à l’aide d’un objet simple, sans avoir recours à une base de données ou à un service de messagerie :
def promotion_eligibility(emp):
if emp.tenure_months < 12:
return Ineligible("Not enough tenure")
if emp.current_rating < 3:
return Ineligible("Rating too low")
return Eligible()
Lorsque la vérification d’une règle devient rapide, les gens l’appliquent réellement, et les cas limites ne passent plus inaperçus des mois plus tard lorsque quelqu’un modifie les exigences de durée.
6. Les commentaires expliquent pourquoi, jamais ce qu’il y a.
// increment the counter
counter++;
// Retry once — the vendor's webhook occasionally arrives before
// the payment record finishes committing on their end.
retryOnce(processWebhook, payload);
Les ingénieurs expérimentés ont tendance à écrire nettement moins de commentaires que les plus jeunes — non par paresse, mais parce qu’ils ont compris que la plupart des commentaires servent à compenser un code qui ne s’explique pas de lui-même. Les commentaires qui restent sont ceux qui contiennent des informations que le code ne peut pas exprimer seul : une justification, un compromis, une mise en garde concernant quelque chose qui n’est pas évident à première vue.
7. Erreurs qui indiquent une piste utile
Un message d’erreur tel que "Entrée invalide" ne donne au développeur suivant aucun élément concret sur lequel travailler. Invalide en quoi ? Quelle entrée ? Une erreur utile contient suffisamment de détails pour que quelqu’un puisse réellement y agir :
{
"code": "INVALID_DATE_RANGE",
"message": "End date must be after start date.",
"field": "endDate"
}
Il n’est pas rare de trouver du code frontend qui examine le texte d’erreur pour déterminer le message à afficher — quelque chose comme if (err.message.includes("date")). Ce schéma est fragile par conception. Dès que quelqu’un reformule le message du backend, la logique de l’interface cesse silencieusement de fonctionner. Les codes existent pour permettre aux machines de prendre des décisions en fonction d’eux ; les messages existent pour que les humains puissent les lire. En les gardant séparés, aucun des deux n’est contraint de remplacer maladroitement l’autre.
8. Une demande de pull, une idée
Une demande de pull intitulée « fixer les problèmes de facturation » qui concerne une douzaine de fichiers et regroupe six modifications non liées est presque impossible à examiner. Celui qui la vérifie l’approuve sans vraiment la contrôler, ou passe une heure à essayer de comprendre quelle modification a provoqué quel effet.
L’approche disciplinée peut sembler presque excessivement prudente : renommer le champ dans une première demande de modification, introduire la nouvelle validation dans une deuxième, et l’intégrer au flux de travail dans une troisième. Écrire les choses de cette manière semble plus lent sur le moment. Mais c’est bien plus rapide pour faire des revues, et lorsque quelque chose ne fonctionne pas en production par la suite, git log vous donne une réponse concrète au lieu de vous forcer à examiner un diff de 400 lignes.
9. Considérer le premier brouillon comme tel
Cette dernière habitude a moins à voir avec le code lui-même et plus avec l’ego. Les développeurs au début de leur carrière ont souvent tendance à considérer la première version qui fonctionne comme le produit final — puisqu’elle marche, elle est déployée. Les ingénieurs plus expérimentés écrivent déjà cette première version en supposant qu’ils la reliront avec un œil critique avant même qu’elle ne soit mise en production.
C’est lors de cette deuxième passe que les conditions imbriquées sont transformées en clauses de protection, que les noms peu clairs sont renommés, et que ce qui semblait être un état « impossible » est détecté avant même que le client ne le rencontre. C’est une habitude simple — s’arrêter, relire, se demander si cela pourrait confondre quelqu’un qui n’a aucun contexte — mais c’est elle qui permet aux huit autres habitudes de se mettre réellement en pratique.
Le fil conducteur
Derrière tout cela se trouve en réalité une seule action répétée : prendre la complexité qui, sinon, resterait coincée dans l’esprit de quelqu’un d’autre plus tard, et la fixer quelque part où elle est visible immédiatement — dans un nom, une frontière, un type, ou une petite différence bien ciblée. Rien de tout cela ne requiert des compétences extraordinaires. Il suffit de décider systématiquement que quiconque lira ce code par la suite mérite vraiment une chance de le comprendre.
Lectures complémentaires
- Dix habitudes récurrentes de JavaScript qui sabotent secrètement votre codebase — Explique dix pièges courants en JavaScript et TypeScript, allant de l’égalité lâche à la mutation d’état, et présente des patterns plus sûrs pour remplacer chacun d’eux.
- Pieces-gages communs en JavaScript et TypeScript qui détruisent le code en secret — Explique des subtilités et pièges en JavaScript et TypeScript, allant des comparaisons avec NaN aux problèmes de synchronisation asynchrone et à la coercition de types, qui provoquent des bugs bien que le code paraisse correct.