Clean code et refactoring - 10 - Couplage et cohesion

Composition vs héritage, injection de dépendances, et comment structurer des modules qui vieillissent bien

  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

10 - Couplage et cohesion -- composition vs héritage

Ce que tu vas apprendre

  • La différence entre couplage faible et couplage fort avec des exemples concrets
  • Ce qu'est la cohesion et pourquoi elle rend les modules fiables
  • Pourquoi la composition bat l'héritage dans la majorite des cas
  • L'injection de dépendances sans framework
  • Le découplage par événements

Prerequisites

Cet article fait suite a 09 - DRY, KISS, YAGNI. Les concepts de SOLID sont utiles ici, en particulier le D (Dependency Inversion).


Dans un projet e-commerce sur lequel j'ai travaille, le module de paiement importait directement le module utilisateur, qui importait le module de notification, qui importait le module de paiement. Un cercle parfait. Changer une ligne dans les notifications cassait les paiements. Personne ne comprenait pourquoi. Le couplage etait invisible et total.

Couplage : moins tu en sais, mieux tu te portes

Le couplage mesure a quel point un module depend des détails internes d'un autre. Un couplage fort signifie qu'un changement ici casse quelque chose la-bas.

typescript// Couplage fort : OrderService connait la structure interne de Database
class OrderService {
  createOrder(order: Order): void {
    const db = Database.getInstance();
    db.connection.pool.query(
      `INSERT INTO orders (id, user_id, total)
       VALUES ($1, $2, $3)`,
      [order.id, order.userId, order.total]
    );
  }
}

Ce service connaît le singleton, la connexion, le pool, et la syntaxe SQL. Si tu changes de base de donnees, tu reecris le service. Si tu changes la structure de la table, pareil.

typescript// Couplage faible : OrderService ne connait qu'une interface
interface OrderRepository {
  save(order: Order): Promise<void>;
}

class OrderService {
  constructor(private orders: OrderRepository) {}

  async createOrder(order: Order): Promise<void> {
    await this.orders.save(order);
  }
}

Le service sait qu'il peut sauvegarder une commande. Il ne sait pas comment. C'est exactement la bonne quantité d'ignorance.

Cohesion : un module fait une chose bien

La cohesion mesure a quel point les éléments d'un module sont lies entre eux. Un module cohesif regroupe des choses qui changent ensemble pour les memes raisons.

typescript// Faible cohesion : un fourre-tout
class UserManager {
  createUser(data: UserData): User { /* ... */ }
  sendWelcomeEmail(user: User): void { /* ... */ }
  generateInvoice(user: User): PDF { /* ... */ }
  updateProfilePicture(user: User, file: Buffer): void { /* ... */ }
  calculateLoyaltyPoints(user: User): number { /* ... */ }
}

// Forte cohesion : chaque classe a un role precis
class UserService {
  create(data: UserData): User { /* ... */ }
  update(id: string, data: Partial<UserData>): User { /* ... */ }
  deactivate(id: string): void { /* ... */ }
}

class LoyaltyService {
  calculatePoints(user: User): number { /* ... */ }
  applyReward(user: User, reward: Reward): void { /* ... */ }
}

class UserNotificationService {
  sendWelcome(user: User): void { /* ... */ }
  sendDeactivation(user: User): void { /* ... */ }
}

La regle est simple : si tu dois modifier UserManager pour quatre raisons différentes (changement de regles de fidelite, nouveau template d'email, nouvelle validation de profil, nouveau format de facture), la cohesion est mauvaise.

Le piège de l'héritage

L'héritage parait naturel. Un AdminUser est un User, donc AdminUser extends User, non ? En pratique, l'héritage créé du couplage vertical. Chaque sous-classe est couplee a l'implementation de son parent.

typescript// Heritage : ca commence bien
class Animal {
  constructor(protected name: string) {}
  move(): string { return `${this.name} bouge`; }
  eat(): string { return `${this.name} mange`; }
}

class Bird extends Animal {
  fly(): string { return `${this.name} vole`; }
}

// Puis ca deraille
class Penguin extends Bird {
  // Un pingouin ne vole pas. Que faire ?
  fly(): string {
    throw new Error("Les pingouins ne volent pas");
  }
}

On viole le principe de substitution de Liskov (voir article 08). L'héritage a force un pingouin dans une hiérarchie qui ne lui convient pas.

La composition resout le problème

Avec la composition, on assemble des comportements au lieu de les hériter.

typescript// Comportements comme fonctions
interface Movement {
  move(name: string): string;
}

const walking: Movement = {
  move: (name) => `${name} marche`,
};

const flying: Movement = {
  move: (name) => `${name} vole`,
};

const swimming: Movement = {
  move: (name) => `${name} nage`,
};

// Composition : on assemble
class Animal {
  constructor(
    private name: string,
    private movements: Movement[]
  ) {}

  move(): string[] {
    return this.movements.map((m) => m.move(this.name));
  }
}

const duck = new Animal("Canard", [walking, flying, swimming]);
const penguin = new Animal("Pingouin", [walking, swimming]);

// duck.move()    -> ["Canard marche", "Canard vole", "Canard nage"]
// penguin.move() -> ["Pingouin marche", "Pingouin nage"]

Pas de hiérarchie rigide. Pas de méthode qui lance une exception. Chaque animal a exactement les capacités dont il a besoin.

Injection de dépendances sans framework

Tu n'as pas besoin de NestJS ou d'InversifyJS pour faire de l'injection de dépendances. Un constructeur suffit.

typescript// Construction manuelle dans un fichier de composition
function createApp(): App {
  const config = loadConfig();
  const db = new PostgresDatabase(config.databaseUrl);
  const userRepo = new PostgresUserRepository(db);
  const emailService = new SmtpEmailService(config.smtp);
  const userService = new UserService(userRepo, emailService);
  const userController = new UserController(userService);

  return new App(userController);
}

Ce fichier est le seul endroit qui connaît les implementations concrètes. Tous les autres modules travaillent avec des interfaces. C'est le "composition root" -- un pattern simple et efficace.

Découplage par événements

Quand deux modules n'ont pas besoin de se connaître du tout, les événements sont la solution.

typescript// Un EventEmitter type
type EventMap = {
  "order:created": { orderId: string; userId: string };
  "order:paid": { orderId: string; amount: number };
  "user:registered": { userId: string; email: string };
};

class TypedEventBus {
  private handlers = new Map<string, Function[]>();

  on<K extends keyof EventMap>(
    event: K,
    handler: (data: EventMap[K]) => void
  ): void {
    const list = this.handlers.get(event as string) ?? [];
    list.push(handler);
    this.handlers.set(event as string, list);
  }

  emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
    const list = this.handlers.get(event as string) ?? [];
    list.forEach((handler) => handler(data));
  }
}

// Le module de commande emet, le module de notification ecoute
// Ils ne se connaissent pas
orderService.on("order:created", (data) => {
  notificationService.sendOrderConfirmation(data.userId, data.orderId);
});

Le module de commande ne sait pas que des notifications existent. Le module de notification ne sait pas comment les commandes sont créées. Chacun peut évoluer indépendamment.

Attention : les événements ajoutent de l'indirection. Un exces d'événements rend le flux d'exécution invisible. Comme pour tout, c'est une question d'équilibre. Plus de reflexions sur l'architecture découplé sur paltemps.fr.

Résumé

  • Le couplage faible fait que les modules ne dependent pas des détails internes des autres
  • La cohesion forte regroupe ce qui change ensemble pour les memes raisons
  • L'héritage créé du couplage vertical et des hierarchies rigides
  • La composition assemble des comportements -- plus flexible, plus testable
  • L'injection de dépendances n'a pas besoin de framework -- un constructeur suffit
  • Les événements decouplent complètement, mais ajoutent de l'indirection

Article précédent : 09 - DRY, KISS, YAGNI

Article suivant : 11 - Complexite cyclomatique

Sources

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