Le code parfait n’existe pas
Et c’est ok.
Le problème n’est pas le perfectionnisme en soi — c’est le perfectionnisme qui bloque la livraison. Un dev avec impact sait décider ce qui est “suffisamment bon” à un instant T, livre, et itère.
“Shipping is a feature. A product that doesn’t work but exists is infinitely more valuable than a perfect product that doesn’t exist.” — Joel Spolsky
Qu’est-ce que “production-ready” ?
“Production-ready” ne veut pas dire parfait. Ça veut dire :
- Fonctionnel — ça fait ce que ça doit faire dans les cas nominaux
- Testable — il existe un moyen de vérifier que ça marche
- Observable — tu peux savoir quand ça casse (logs, erreurs explicites)
- Maintenable — quelqu’un d’autre (ou toi dans 6 mois) peut le comprendre
Écrire du code testable
Le code testable n’est pas une question de tests en premier. C’est une question de design.
Un code difficile à tester révèle souvent un design problématique : couplage fort, responsabilités multiples, dépendances cachées.
Les 3 patterns qui rendent le code testable :
1. Injection de dépendances
// ❌ Difficile à tester — dépendance cachée
async function getUser(id: string) {
const db = new Database(); // couplage fort
return db.query(`SELECT * FROM users WHERE id = $1`, [id]);
}
// ✅ Facile à tester — dépendance injectée
async function getUser(id: string, db: Database) {
return db.query(`SELECT * FROM users WHERE id = $1`, [id]);
}
2. Fonctions pures là où c’est possible
// ❌ Impure — résultat dépend d'un état externe
function formatPrice(price: number) {
return `${price} ${config.currency}`; // config = global state
}
// ✅ Pure — même input → même output, toujours
function formatPrice(price: number, currency: string) {
return `${price} ${currency}`;
}
3. Séparation I/O et logique
// ❌ Mélange I/O et logique métier
async function processOrder(orderId: string) {
const order = await db.getOrder(orderId); // I/O
const total = order.items.reduce((s, i) => s + i.price * i.qty, 0); // logique
await db.updateOrder(orderId, { total }); // I/O
}
// ✅ Logique extraite, pure, testable sans DB
function calculateTotal(items: OrderItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
async function processOrder(orderId: string) {
const order = await db.getOrder(orderId);
const total = calculateTotal(order.items); // logique pure, testable
await db.updateOrder(orderId, { total });
}
Le framework de décision “suffisamment bon”
Quand tu te demandes si ton code est prêt, utilise ce filtre en 3 niveaux :
| Niveau | Question | Si non |
|---|---|---|
| Bloquant | Est-ce que ça casse des cas nominaux ? | Ne livre pas |
| Important | Est-ce que les erreurs sont gérées ? | Fix dans la même PR |
| Amélioration | Est-ce que c’est optimal / élégant ? | Issue ou TODO — pas un blocage |
Rendre le code maintenable
Le code maintenable, c’est du code qui communique son intention.
Nommage explicite :
// ❌ Qu'est-ce que c veut dire ici ?
const c = items.filter(i => i.s === 'active' && i.p > 0);
// ✅ Intention claire
const availableProducts = items.filter(
item => item.status === 'active' && item.price > 0
);
Fonctions courtes avec une seule responsabilité :
Si tu ne peux pas résumer ce que fait ta fonction en une phrase sans le mot “et”, c’est qu’elle fait trop de choses.
Exercice pratique
Prends une fonctionnalité que tu as livrée récemment et réponds honnêtement :
- Est-ce qu’il y a un test qui prouve que ça fonctionne ?
- Si ça crashe en prod, est-ce que tu le saurais en moins de 5 minutes ?
- Est-ce qu’un autre dev peut comprendre le code sans te demander d’explications ?
Si la réponse à l’une de ces questions est “non”, note ce que tu aurais changé. C’est ça, la progression.