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ère | Builder | Prototype |
|---|---|---|
| Objectif | Construction complexe | Clonage d'objets |
| Quand | Objet avec plusieurs étapes de construction | Objet coûteux à créer ou à initialiser |
| Variation | Directeur + Builder différent | Clone + modifications |
| Immutabilité | Peut produire des objets immutables | Dépend de l'implémentation (deep/shallow) |
| Performance | Moyenne | Élevée (pas de création "from scratch") |
| Complexité | Moyenne | Faible à moyenne |
Comment choisir ?
Diagramme en cours de génération...