Clean code et refactoring - 09 - DRY, KISS, YAGNI

Quand abstraire, quand dupliquer, et pourquoi la simplicité bat toujours l'intelligence

  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

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

Sources

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