MFormations
Modern Design Patterns

Chapitre 4

Chapitre 04 — Builder et Prototype

Chapitre 04 — Builder et Prototype

Cours 04 — Builder et Prototype

Partie 1 : Builder

1.1 Définition

Le Builder sépare la construction d'un objet complexe de sa représentation, permettant au même processus de construction de créer différentes représentations.

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

1.2 Problème résolu

Certains objets nécessitent une construction complexe :

  • Objets avec de nombreux paramètres optionnels
  • Construction multi-étapes avec validation intermédiaire
  • Différentes représentations du même processus
// Sans Builder : constructeur monstrueux
const pizza = new Pizza("large", "thin", true, true, false, true, false, "cheddar");

// Avec Builder : lisible et flexible
const pizza = new PizzaBuilder()
  .setSize("large")
  .setCrust("thin")
  .addCheese()
  .addPepperoni()
  .addMushrooms()
  .setCheeseType("cheddar")
  .build();

1.3 Fluent Interface

Le Fluent Interface est un style d'API où les méthodes retournent this pour permettre le chaînage :

class QueryBuilder {
  private query: string = "";

  select(...fields: string[]): this {
    this.query = `SELECT ${fields.join(", ")}`;
    return this;
  }

  from(table: string): this {
    this.query += ` FROM ${table}`;
    return this;
  }

  where(condition: string): this {
    this.query += ` WHERE ${condition}`;
    return this;
  }

  orderBy(field: string, direction: "ASC" | "DESC" = "ASC"): this {
    this.query += ` ORDER BY ${field} ${direction}`;
    return this;
  }

  limit(n: number): this {
    this.query += ` LIMIT ${n}`;
    return this;
  }

  build(): string {
    return this.query + ";";
  }
}

// Usage fluent
const query = new QueryBuilder()
  .select("id", "name", "email")
  .from("users")
  .where("active = true")
  .orderBy("name")
  .limit(10)
  .build();

1.4 Builder avec Director

Quand les étapes de construction sont complexes et doivent être réutilisées :

// Director
class PizzaDirector {
  constructor(private builder: PizzaBuilder) {}

  makeMargherita(): Pizza {
    return this.builder
      .setSize("large")
      .setCrust("thin")
      .addCheese()
      .addTomato()
      .addBasil()
      .build();
  }

  makePepperoni(): Pizza {
    return this.builder
      .setSize("large")
      .setCrust("thick")
      .addCheese()
      .addPepperoni()
      .build();
  }
}

// Usage
const director = new PizzaDirector(new PizzaBuilder());
const margherita = director.makeMargherita();
const pepperoni = director.makePepperoni();

1.5 Builder Immutable

Pour construire des objets immutables :

class User {
  readonly id: string;
  readonly name: string;
  readonly email: string;
  readonly role: string;

  private constructor(builder: UserBuilder) {
    this.id = builder["id"];
    this.name = builder["name"];
    this.email = builder["email"];
    this.role = builder["role"];
  }

  static builder(): UserBuilder {
    return new UserBuilder();
  }
}

class UserBuilder {
  private id: string = "";
  private name: string = "";
  private email: string = "";
  private role: string = "user";

  setId(id: string): this {
    this.id = id;
    return this;
  }

  setName(name: string): this {
    this.name = name;
    return this;
  }

  setEmail(email: string): this {
    this.email = email;
    return this;
  }

  setRole(role: string): this {
    this.role = role;
    return this;
  }

  build(): User {
    this.validate();
    return new User(this);
  }

  private validate(): void {
    if (!this.name) throw new Error("Name required");
    if (!this.email?.includes("@")) throw new Error("Invalid email");
  }
}

// Usage
const user = User.builder()
  .setId("123")
  .setName("Alice")
  .setEmail("alice@example.com")
  .setRole("admin")
  .build();

1.6 StringBuilder

Builder natif dans de nombreux langages :

// TypeScript
const sb: string[] = [];
sb.push("Hello");
sb.push(" ");
sb.push("World");
const result = sb.join("");

// Java
// StringBuilder sb = new StringBuilder();
// sb.append("Hello");
// sb.append(" World");
// String result = sb.toString();

Partie 2 : Prototype

2.1 Définition

Le Prototype spécifie les types d'objets à créer en utilisant une instance prototype, et crée de nouveaux objets en copiant ce prototype.

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

2.2 Problème résolu

  • Éviter le coût de création d'objets complexes (base de données, fichiers)
  • Éviter de créer des classes de factory pour chaque type d'objet
  • Créer des copies avec des variations mineures

2.3 Shallow Copy vs Deep Copy

class Address {
  constructor(
    public street: string,
    public city: string
  ) {}
}

class Employee {
  constructor(
    public name: string,
    public address: Address,
    public skills: string[]
  ) {}

  // Shallow copy : les références sont partagées
  shallowClone(): Employee {
    return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
  }

  // Deep copy : tout est dupliqué
  deepClone(): Employee {
    return new Employee(
      this.name,
      new Address(this.address.street, this.address.city),
      [...this.skills]
    );
  }
}

// Problème du shallow copy
const original = new Employee("Alice", new Address("123 Main St", "Paris"), ["JS"]);
const shallow = original.shallowClone();
shallow.address.street = "456 Oak St";
console.log(original.address.street); // "456 Oak St" — modifié !

// Deep copy résout le problème
const deep = original.deepClone();
deep.address.street = "789 Pine St";
console.log(original.address.street); // "456 Oak St" — inchangé

2.4 Implémentation avec interface Cloneable

interface Cloneable<T> {
  clone(): T;
}

class Shape implements Cloneable<Shape> {
  constructor(
    public x: number,
    public y: number,
    public color: string
  ) {}

  clone(): Shape {
    return new Shape(this.x, this.y, this.color);
  }
}

class Circle extends Shape {
  constructor(
    x: number, y: number, color: string,
    public radius: number
  ) {
    super(x, y, color);
  }

  clone(): Circle {
    return new Circle(this.x, this.y, this.color, this.radius);
  }
}

// Registry de prototypes
class ShapeRegistry {
  private shapes: Map<string, Shape> = new Map();

  addPrototype(id: string, shape: Shape): void {
    this.shapes.set(id, shape);
  }

  create(id: string): Shape | undefined {
    const prototype = this.shapes.get(id);
    return prototype?.clone();
  }
}

// Usage
const registry = new ShapeRegistry();
registry.addPrototype("circle_blue", new Circle(0, 0, "blue", 10));

const circle1 = registry.create("circle_blue")!;
const circle2 = registry.create("circle_blue")!;
console.log(circle1 === circle2); // false — objets distincts

2.5 Prototype pour la performance

class DatabaseRecord {
  constructor(
    public tableName: string,
    public columns: string[],
    public data: Map<string, any>
  ) {
    // Simulation d'une opération coûteuse
    this.loadSchema();
  }

  private loadSchema(): void {
    // Lourd : requête à la base de données
    console.log(`Loading schema for ${this.tableName}...`);
  }

  clone(): DatabaseRecord {
    // Évite de recharger le schéma
    return new DatabaseRecord(
      this.tableName,
      [...this.columns],
      new Map(this.data)
    );
  }
}

// Sans Prototype : le schéma est rechargé à chaque fois
const user1 = new DatabaseRecord("users", ["id", "name"], new Map());
const user2 = new DatabaseRecord("users", ["id", "name"], new Map()); // Reload!

// Avec Prototype : le schéma est chargé une fois
const baseRecord = new DatabaseRecord("users", ["id", "name"], new Map());
const user1Clone = baseRecord.clone();
const user2Clone = baseRecord.clone(); // Pas de reload !

Partie 3 : Comparaison Builder vs Prototype

CritèreBuilderPrototype
ObjectifConstruction complexeClonage d'objets
QuandObjet avec plusieurs étapes de constructionObjet coûteux à créer ou à initialiser
VariationDirecteur + Builder différentClone + modifications
ImmutabilitéPeut produire des objets immutablesDépend de l'implémentation (deep/shallow)
PerformanceMoyenneÉlevée (pas de création "from scratch")
ComplexitéMoyenneFaible à moyenne

Comment choisir ?

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