MFormations
Modern Design Patterns

Chapitre 1

Chapitre 01 — Principes SOLID

Chapitre 01 — Principes SOLID

Cours 01 — Principes SOLID

Introduction

SOLID est un acronyme proposé par Robert C. Martin (Uncle Bob) au début des années 2000. Ces 5 principes guident la conception orientée objet pour produire un code maintenable, flexible et facile à tester.

Diagramme en cours de génération...

1. Single Responsibility Principle (SRP)

"Une classe doit avoir une seule raison de changer."

Problème

Une classe qui gère la persistance, l'affichage et les calculs est difficile à maintenir, tester et faire évoluer.

Violation

class Employee {
  constructor(
    public name: string,
    public salary: number
  ) {}

  calculateTax(): number {
    return this.salary * 0.2;
  }

  generateReport(): string {
    return `Employee: ${this.name}, Tax: ${this.calculateTax()}`;
  }

  saveToDatabase(): void {
    // SQL logic here
    console.log(`Saving ${this.name} to DB`);
  }

  sendEmail(): void {
    // Email logic
    console.log(`Sending email to ${this.name}`);
  }
}

Cette classe a 4 raisons de changer :

  1. Les règles de calcul des impôts
  2. Le format du rapport
  3. Le schéma de base de données
  4. Le service d'email

Solution

class Employee {
  constructor(
    public name: string,
    public salary: number
  ) {}
}

class TaxCalculator {
  calculate(employee: Employee): number {
    return employee.salary * 0.2;
  }
}

class ReportGenerator {
  generate(employee: Employee): string {
    return `Employee: ${employee.name}`;
  }
}

class EmployeeRepository {
  save(employee: Employee): void {
    console.log(`Saving ${employee.name} to DB`);
  }
}

class EmailService {
  send(employee: Employee, message: string): void {
    console.log(`Email to ${employee.name}: ${message}`);
  }
}

Bénéfices

  • Testabilité : chaque classe se teste indépendamment
  • Réutilisabilité : TaxCalculator peut être réutilisé ailleurs
  • Maintenabilité : un changement dans les taxes n'affecte que TaxCalculator

2. Open/Closed Principle (OCP)

"Les entités logicielles doivent être ouvertes à l'extension mais fermées à la modification."

Problème

Quand on ajoute une nouvelle fonctionnalité, on modifie du code existant, ce qui risque d'introduire des régressions.

Violation

class DiscountCalculator {
  calculate(price: number, customerType: string): number {
    if (customerType === "regular") {
      return price * 0.95;
    } else if (customerType === "premium") {
      return price * 0.90;
    } else if (customerType === "vip") {
      return price * 0.80;
    }
    return price;
  }
}

Pour ajouter un nouveau type de client, on modifie la classe → violation de l'OCP.

Solution

interface DiscountStrategy {
  apply(price: number): number;
}

class RegularDiscount implements DiscountStrategy {
  apply(price: number): number {
    return price * 0.95;
  }
}

class PremiumDiscount implements DiscountStrategy {
  apply(price: number): number {
    return price * 0.90;
  }
}

class VIPDiscount implements DiscountStrategy {
  apply(price: number): number {
    return price * 0.80;
  }
}

class DiscountCalculator {
  constructor(private strategy: DiscountStrategy) {}

  calculate(price: number): number {
    return this.strategy.apply(price);
  }
}

Pour ajouter SeasonalDiscount, on crée une nouvelle classe sans modifier l'existant :

class SeasonalDiscount implements DiscountStrategy {
  apply(price: number): number {
    return price * 0.75;
  }
}

3. Liskov Substitution Principle (LSP)

"Les objets d'une classe dérivée doivent pouvoir se substituer aux objets de la classe de base sans altérer le bon fonctionnement du programme."

Problème classique : le carré n'est pas un rectangle

Diagramme en cours de génération...

Violation

class Rectangle {
  constructor(
    protected width: number,
    protected height: number
  ) {}

  setWidth(width: number): void {
    this.width = width;
  }

  setHeight(height: number): void {
    this.height = height;
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width: number): void {
    this.width = width;
    this.height = width; // Maintient le carré
  }

  setHeight(height: number): void {
    this.width = height;
    this.height = height;
  }
}

// Client code qui suppose que Square est un Rectangle
function resize(rect: Rectangle): void {
  rect.setWidth(5);
  rect.setHeight(10);
  // Attend area = 50, mais si Square => 100
  console.log(rect.getArea());
}

resize(new Square(1, 1)); // 100 au lieu de 50

Solution

// Abstraire en Shape
interface Shape {
  getArea(): number;
}

class Rectangle implements Shape {
  constructor(
    private width: number,
    private height: number
  ) {}

  getArea(): number {
    return this.width * this.height;
  }
}

class Square implements Shape {
  constructor(private side: number) {}

  getArea(): number {
    return this.side * this.side;
  }
}

// Le client travaille avec Shape, pas avec Rectangle
function printArea(shape: Shape): void {
  console.log(`Area: ${shape.getArea()}`);
}

Règles de la substitution

  1. Préconditions ne peuvent pas être renforcées dans la sous-classe
  2. Postconditions ne peuvent pas être affaiblies
  3. Invariants de la classe de base doivent être préservés
  4. Contrainte d'historique (règle de Helling) : les nouvelles méthodes ne doivent pas modifier l'état d'une manière non prévue par la base

4. Interface Segregation Principle (ISP)

"Les clients ne doivent pas être forcés de dépendre d'interfaces qu'ils n'utilisent pas."

Problème

Une interface "grasse" oblige les implémentations à fournir des méthodes inutiles.

Violation

interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
}

class HumanWorker implements Worker {
  work(): void { console.log("Working"); }
  eat(): void { console.log("Eating"); }
  sleep(): void { console.log("Sleeping"); }
}

class RobotWorker implements Worker {
  work(): void { console.log("Working"); }
  eat(): void { throw new Error("Robots don't eat"); }
  sleep(): void { throw new Error("Robots don't sleep"); }
}

Solution

Diagramme en cours de génération...
interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

interface Sleepable {
  sleep(): void;
}

class HumanWorker implements Workable, Eatable, Sleepable {
  work(): void { console.log("Working"); }
  eat(): void { console.log("Eating"); }
  sleep(): void { console.log("Sleeping"); }
}

class RobotWorker implements Workable {
  work(): void { console.log("Working"); }
}

5. Dependency Inversion Principle (DIP)

"Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre des abstractions."

Problème

class MySQLDatabase {
  query(sql: string): any[] {
    console.log(`MySQL query: ${sql}`);
    return [];
  }
}

class UserService {
  private db: MySQLDatabase;

  constructor() {
    this.db = new MySQLDatabase(); // Couplage fort
  }

  getUsers(): any[] {
    return this.db.query("SELECT * FROM users");
  }
}

Solution

Diagramme en cours de génération...
interface Database {
  query(sql: string): any[];
}

class MySQLDatabase implements Database {
  query(sql: string): any[] {
    console.log(`MySQL: ${sql}`);
    return [];
  }
}

class PostgresDatabase implements Database {
  query(sql: string): any[] {
    console.log(`Postgres: ${sql}`);
    return [];
  }
}

class UserService {
  constructor(private db: Database) {} // Injection de dépendance

  getUsers(): any[] {
    return this.db.query("SELECT * FROM users");
  }
}

// Usage
const service = new UserService(new MySQLDatabase());
// ou
const service2 = new UserService(new PostgresDatabase());

Injection de dépendance

3 formes courantes :

FormeDescription
Constructor InjectionDépendances passées au constructeur
Setter InjectionDépendances passées via un setter
Interface InjectionDépendances passées via une méthode d'interface

Synthèse

Diagramme en cours de génération...

Bénéfices cumulatifs

  • SRP → code plus petit, plus ciblé
  • OCP → ajouts sans risque de régression
  • LSP → polymorphisme fiable
  • ISP → implémentations légères
  • DIP → découplage, testabilité