09 - DRY, KISS, YAGNI -- quand abstraire et quand dupliquer
Ce que tu vas apprendre
- Ce que DRY veut vraiment dire (indice : ce n'est pas "zero duplication de code")
- Pourquoi la duplication est parfois meilleure qu'une mauvaise abstraction
- KISS applique a des exemples concrets TypeScript
- YAGNI et la regle des trois pour décider quand abstraire
Prerequisites
Cet article fait suite a 08 - SOLID en pratique. On y a vu les principes de conception orientee objet. Ici, on passe aux heuristiques de simplicité.
Un collegue m'a montre un jour un helper formatData utilise dans sept fichiers différents. Le problème : chaque fichier avait besoin d'un formatage legerement différent. Le helper avait accumule huit paramètres booleens pour gerer tous les cas. La duplication qu'il voulait éviter etait devenue un monstre bien pire. C'est l'histoire de beaucoup de projets.
DRY ne veut pas dire zero duplication
DRY signifie "Don't Repeat Yourself", mais Andy Hunt et Dave Thomas (les auteurs du Pragmatic Programmer) parlent de duplication de connaissance, pas de duplication de code.
typescript// Ces deux fonctions se ressemblent, mais representent
// deux connaissances differentes
function calculateShippingTax(amount: number): number {
return amount * 0.2;
}
function calculateServiceTax(amount: number): number {
return amount * 0.2;
}
Aujourd'hui le taux est le meme. Demain, le taux de TVA sur les services peut changer indépendamment de celui sur le shipping. Fusionner ces deux fonctions en une seule calculateTax creerait un couplage artificiel. Deux morceaux de code qui se ressemblent ne sont pas forcement une duplication.
La vraie duplication, c'est quand une regle métier est exprimee a deux endroits :
typescript// Duplication reelle : la regle "un utilisateur premium
// a plus de 1000 points" est a deux endroits
// Dans le service
if (user.points > 1000) {
applyDiscount(order);
}
// Dans le controller, 200 lignes plus loin
if (user.points > 1000) {
showPremiumBadge(user);
}
// Fix : une seule source de verite
function isPremium(user: User): boolean {
return user.points > 1000;
}
Si le seuil passe a 1500, je change un seul endroit. Ca, c'est DRY.
Quand la duplication bat l'abstraction
Sandi Metz a une phrase celebre : "duplication is far cheaper than the wrong abstraction." J'y crois profondement.
typescript// Mauvaise abstraction : un helper generique qui essaie de tout faire
function processEntity(
entity: unknown,
type: "user" | "product" | "order",
options: {
validate?: boolean;
transform?: boolean;
notify?: boolean;
format?: "json" | "xml" | "csv";
}
): unknown {
if (type === "user") {
// 30 lignes specifiques aux users
} else if (type === "product") {
// 30 lignes specifiques aux products
} else if (type === "order") {
// 30 lignes specifiques aux orders
}
// ...
}
// Mieux : trois fonctions simples et explicites
function processUser(user: User): ProcessedUser {
validate(user);
return transformUser(user);
}
function processProduct(product: Product): ProcessedProduct {
return transformProduct(product);
}
function processOrder(order: Order): ProcessedOrder {
validate(order);
notifyWarehouse(order);
return transformOrder(order);
}
Les trois fonctions ont quelques lignes en commun. Et alors ? Chacune est lisible, testable, et peut évoluer sans impacter les autres.
KISS -- la simplicité n'est pas la facilite
KISS (Keep It Simple, Stupid) ne veut pas dire "ecris du code basique". Ca veut dire : choisis la solution la plus simple qui resout le problème. Le code clever impressionne pendant la code review. Il fait pleurer pendant le debug a 3h du matin.
typescript// Clever : enchainement de reduce, flatMap, et destructuring
const result = data
.reduce((acc, { items, ...rest }) =>
[...acc, ...items.flatMap(({ tags, ...item }) =>
tags.map(tag => ({ ...rest, ...item, tag }))
)], [] as Flattened[]);
// Simple : deux boucles imbriquees, lisible par tout le monde
const result: Flattened[] = [];
for (const entry of data) {
for (const item of entry.items) {
for (const tag of item.tags) {
result.push({
name: entry.name,
category: entry.category,
itemId: item.id,
tag,
});
}
}
}
La version "simple" fait 12 lignes au lieu de 5. Elle est aussi trois fois plus facile a debugger, a modifier et a comprendre pour quelqu'un qui ne l'a pas écrite.
Un autre piège KISS classique : les generics inutiles.
typescript// Sur-generique pour rien
class Repository<T extends BaseEntity, K extends keyof T> {
async findBy(key: K, value: T[K]): Promise<T[]> { /* ... */ }
}
// Si tu n'as que des Users et des Products,
// deux classes simples font le meme travail en etant plus claires
class UserRepository {
async findByEmail(email: string): Promise<User[]> { /* ... */ }
}
YAGNI -- tu n'en auras pas besoin
YAGNI (You Ain't Gonna Need It) vient de l'Extreme Programming. Le principe est simple : ne construis pas une fonctionnalité tant que tu n'en as pas besoin maintenant.
J'ai vu un dev passer trois jours a construire un système de plugins pour une app qui n'avait qu'un seul plugin. "Au cas ou." Deux ans plus tard, il y a toujours un seul plugin, et le système de plugins complique chaque modification.
typescript// YAGNI violation : support multi-base de donnees "au cas ou"
interface DatabaseAdapter {
connect(): Promise<void>;
query(sql: string): Promise<unknown>;
disconnect(): Promise<void>;
}
class PostgresAdapter implements DatabaseAdapter { /* ... */ }
class MySQLAdapter implements DatabaseAdapter { /* ... */ } // jamais utilise
class SQLiteAdapter implements DatabaseAdapter { /* ... */ } // jamais utilise
// YAGNI : juste ce dont tu as besoin maintenant
class Database {
private pool: Pool;
async query(sql: string, params: unknown[]): Promise<QueryResult> {
return this.pool.query(sql, params);
}
}
Si un jour tu migres vers MySQL, tu refactoreras. Le refactoring sera plus facile parce que le code est simple, pas parce qu'il a une abstraction prematuree. On en reparle en détail dans l'article sur les abstractions prematurees.
La regle des trois
C'est mon heuristique favorite. La première fois que tu ecris un bout de code, ecris-le. La deuxieme fois que tu vois un pattern similaire, note-le mais duplique. La troisieme fois, abstrait.
Pourquoi ? Parce qu'avec deux occurrences, tu ne sais pas encore quelle est la bonne abstraction. Avec trois, le pattern se dessine.
typescript// Occurrence 1 : dans le service utilisateur
const userAge = Math.floor(
(Date.now() - user.birthDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000)
);
// Occurrence 2 : dans le service employe -- tu dupliques
const employeeAge = Math.floor(
(Date.now() - employee.birthDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000)
);
// Occurrence 3 : dans le service patient -- maintenant tu abstrais
function calculateAge(birthDate: Date): number {
return Math.floor(
(Date.now() - birthDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000)
);
}
Trois occurrences confirmees, une abstraction justifiee. Plus de reflexions sur quand abstraire sur paltemps.fr.
L'abstraction prematuree est pire que la duplication
Je le répété parce que c'est le piège numero un. Une abstraction prematuree :
- Ajoute de l'indirection (plus de fichiers a ouvrir pour comprendre)
- Cree du couplage (les consommateurs dependent de l'abstraction)
- Resiste au changement (modifier l'abstraction impacte tout le monde)
- Masque la complexité réelle derrière une fausse simplicité
La duplication, elle, a un coût visible et previsible. Tu vois le code en double. Tu sais ou il est. Tu peux le fusionner quand le bon pattern emerge.
Résumé
- DRY concerne la duplication de connaissance, pas la duplication de code
- Deux blocs de code identiques peuvent representer deux concepts différents
- KISS : la solution la plus simple qui marche est la meilleure
- Le code clever est l'ennemi du code maintenable
- YAGNI : ne construis que ce dont tu as besoin maintenant
- La regle des trois : duplique deux fois, abstrait a la troisieme
- Une mauvaise abstraction coûte plus cher que la duplication
Article précédent : 08 - SOLID en pratique
Article suivant : 10 - Couplage et cohesion