Aller au contenu
  1. Exemples/

Le pattern Builder - construire des objets complexes étape par étape

·18 mins· loading · loading · · ·
Conception Back-End
Adrien D'acunto
Auteur
Adrien D’acunto
Sommaire

Le pattern Builder : construire des objets complexes étape par étape
#

Introduction
#

Le pattern Builder est un design pattern créationnel qui permet de construire des objets complexes étape par étape. Il sépare la construction d’un objet de sa représentation, permettant ainsi de créer différentes représentations avec le même processus de construction. Dans cet article, nous explorerons ce pattern à travers un exemple concret : la construction d’une promotion étudiante avec validation des données.

Qu’est-ce que le Builder ?
#

Définition
#

Le pattern Builder permet de :

  • Construire des objets complexes étape par étape
  • Produire différents types d’objets avec le même processus
  • Isoler le code de construction de la logique métier
  • Valider les données avant la création de l’objet final

Structure UML
#

classDiagram
    class BuilderInterface~T~ {
        <>
        +build() T
    }
    
    class ConcreteBuilder {
        -field1
        -field2
        +method1() ConcreteBuilder
        +method2() ConcreteBuilder
        +build() Product
    }
    
    class Product {
        +property1
        +property2
    }
    
    BuilderInterface <|.. ConcreteBuilder : implements
    ConcreteBuilder ..> Product : creates

Caractéristiques clés
#

Caractéristique Description
Fluent Interface Chaînage des méthodes (method chaining)
Validation Vérification des données avant construction
Immutabilité L’objet final peut être immutable
Séparation Construction séparée de la représentation

Problématique sans Builder
#

Le problème du constructeur télescopique
#

// Sans Builder : Constructeurs multiples (anti-pattern)
class Promo {
    constructor(name: string) { /* ... */ }
}

class Promo {
    constructor(name: string, beginAt: Date) { /* ... */ }
}

class Promo {
    constructor(name: string, beginAt: Date, endAt: Date) { /* ... */ }
}

// Utilisation confuse
const promo1 = new Promo("Dev 2025");
const promo2 = new Promo("Dev 2025", new Date());
const promo3 = new Promo("Dev 2025", new Date(), new Date());

Le problème de l’objet partiellement construit
#

// Sans Builder : Objet invalide temporairement
const promo = new Promo();
// Promo existe mais n'est pas valide
promo.setName("Dev 2025");
// Toujours pas valide
promo.setBeginAt(new Date());
// Enfin valide
promo.setEndAt(new Date());

Conséquences :

  • Objets dans un état invalide pendant la construction
  • Pas de validation centralisée
  • Code verbeux et répétitif
  • Difficile de maintenir la cohérence

Implémentation TypeScript
#

Architecture complète
#

classDiagram
    class BuilderInterface~T~ {
        <>
        +build() T
    }
    
    class PromoBuilder {
        -_name: string
        -_beginAt: Date
        -_endAt: Date
        +name(name: string) PromoBuilder
        +beginAt(date: Date) PromoBuilder
        +endAt(date: Date) PromoBuilder
        +build() Promo
    }
    
    class Promo {
        -name: string
        -beginAt: Date
        -endAt: Date
        -students: CollectionInterface~Student~
        +getName() string
        +setName(name: string) void
        +getBeginAt() Date
        +setBeginAt(beginAt: Date) void
        +getEndAt() Date
        +setEndAt(endAt: Date) void
        +addStudent(student: Student) void
        +removeStudent(student: Student) void
        +getStudentsNumber() number
    }
    
    class CollectionInterface~T~ {
        <>
        +add(item: T) void
        +remove(item: T) void
        +getLength() number
    }
    
    class StudentCollection {
        +add(student: Student) void
        +remove(student: Student) void
        +getLength() number
    }
    
    BuilderInterface <|.. PromoBuilder : implements
    PromoBuilder ..> Promo : creates
    Promo --> CollectionInterface : uses
    CollectionInterface <|.. StudentCollection : implements

Interface Builder générique
#

export interface BuilderInterface<T> {
    build(): T
}

Cette interface générique définit le contrat de base :

  • Paramètre de type T : le type d’objet à construire
  • Méthode build() : retourne l’instance finale construite

Classe Promo
#

import type { CollectionInterface } from "../core/collection/collection-interface";
import type { Student } from "./student";
import { StudentCollection } from "./student-collection";

export class Promo {
    private name: string = ''
    private beginAt: Date = new Date()
    private endAt: Date = new Date()
    private students: CollectionInterface<Student> = new StudentCollection()

    public getName(): string {
        return this.name;
    }

    public setName(name: string): void {
        if (this.name === '')
            this.name = name;
    }

    public getBeginAt(): Date {
        return this.beginAt;
    }

    public setBeginAt(beginAt: Date): void {
        this.beginAt = beginAt;
    }

    public getEndAt(): Date {
        return this.endAt;
    }

    public setEndAt(endAt: Date): void {
        this.endAt = endAt;
    }

    public addStudent(student: Student): void {
        this.students.add(student)
    }

    public removeStudent(student: Student): void {
        this.students.remove(student)
    }

    public getStudentsNumber(): number {
        return this.students.getLength()
    }
}

Points clés :

  • Attributs privés avec valeurs par défaut
  • Protection contre la modification du nom (immutabilité partielle)
  • Gestion d’une collection d’étudiants
  • Interface simple et claire

PromoBuilder
#

import type { BuilderInterface } from "../core/builder/builder.interface";
import { Promo } from "./promo";

export class PromoBuilder implements BuilderInterface<Promo> {
    private _name: string = ''
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date()
    
    public name(name: string): PromoBuilder {
        this._name = name
        return this
    }

    public beginAt(date: Date): PromoBuilder {
        this._beginAt = date
        return this
    }

    public endAt(date: Date): PromoBuilder {
        this._endAt = date
        return this
    }

    build(): Promo {
        if (this._name === '') {
            throw new Error('Promo name is required')
        }

        if (this._beginAt > this._endAt) {
            throw new Error("Incorrect dates");
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)

        return promo
    }
}

Caractéristiques du builder :

  • Attributs privés préfixés par _ (convention)
  • Chaque méthode retourne this (fluent interface)
  • Validation dans build() avant création
  • Séparation claire construction/validation

Analyse du code
#

Diagramme de séquence : construction d’une promo
#

sequenceDiagram
    participant Client
    participant Builder as PromoBuilder
    participant Promo
    
    Client->>Builder: new PromoBuilder()
    Client->>Builder: name("Dev 2025")
    Builder-->>Client: this
    
    Client->>Builder: beginAt(2025-01-15)
    Builder-->>Client: this
    
    Client->>Builder: endAt(2025-12-20)
    Builder-->>Client: this
    
    Client->>Builder: build()
    
    alt Validation OK
        Builder->>Builder: Vérifier name
        Builder->>Builder: Vérifier dates
        Builder->>Promo: new Promo()
        Builder->>Promo: setName()
        Builder->>Promo: setBeginAt()
        Builder->>Promo: setEndAt()
        Promo-->>Builder: promo
        Builder-->>Client: promo
    else Validation échouée
        Builder-->>Client: throw Error
    end

Fluent Interface : le chaînage de méthodes
#

public name(name: string): PromoBuilder {
    this._name = name
    return this  // Retourne l'instance courante
}

Ce pattern permet le chaînage :

const promo = new PromoBuilder()
    .name("Dev 2025")
    .beginAt(new Date("2025-01-15"))
    .endAt(new Date("2025-12-20"))
    .build();

Avantages :

  • Code lisible et expressif
  • Ordre des appels flexible
  • Construction en une seule expression

Validation Centralisée
#

build(): Promo {
    // Validation 1 : Nom obligatoire
    if (this._name === '') {
        throw new Error('Promo name is required')
    }

    // Validation 2 : Dates cohérentes
    if (this._beginAt > this._endAt) {
        throw new Error("Incorrect dates");
    }
    
    // Construction uniquement si validations OK
    const promo = new Promo()
    promo.setName(this._name)
    promo.setBeginAt(this._beginAt)
    promo.setEndAt(this._endAt)

    return promo
}

Bénéfices :

  • Toutes les validations en un seul endroit
  • Objet final toujours valide
  • Erreurs explicites avant création

Séparation des responsabilités
#

Classe Responsabilité
Promo Représenter une promotion (logique métier)
PromoBuilder Construire et valider une promotion
BuilderInterface Définir le contrat de construction

Utilisation pratique
#

Exemple complet
#

// main.ts
import { PromoBuilder } from './promo-builder';
import { Student } from './student';

// Construction simple
const promo = new PromoBuilder()
    .name("Développement Web 2025")
    .beginAt(new Date("2025-01-15"))
    .endAt(new Date("2025-12-20"))
    .build();

console.log(promo.getName()); // "Développement Web 2025"
console.log(promo.getStudentsNumber()); // 0

// Ajout d'étudiants
const alice = new Student("Alice", "Martin");
const bob = new Student("Bob", "Dupont");

promo.addStudent(alice);
promo.addStudent(bob);

console.log(promo.getStudentsNumber()); // 2

Gestion des erreurs
#

// Erreur : Nom manquant
try {
    const promo = new PromoBuilder()
        .beginAt(new Date("2025-01-15"))
        .endAt(new Date("2025-12-20"))
        .build();
} catch (error) {
    console.error(error.message); // "Promo name is required"
}

// Erreur : Dates incohérentes
try {
    const promo = new PromoBuilder()
        .name("Dev 2025")
        .beginAt(new Date("2025-12-20"))
        .endAt(new Date("2025-01-15"))
        .build();
} catch (error) {
    console.error(error.message); // "Incorrect dates"
}

Tests unitaires
#

// promo-builder.test.ts
import { describe, it, expect } from 'vitest';
import { PromoBuilder } from './promo-builder';

describe('PromoBuilder', () => {
    
    it('devrait construire une promo valide', () => {
        const promo = new PromoBuilder()
            .name("Dev 2025")
            .beginAt(new Date("2025-01-15"))
            .endAt(new Date("2025-12-20"))
            .build();
        
        expect(promo.getName()).toBe("Dev 2025");
        expect(promo.getBeginAt()).toEqual(new Date("2025-01-15"));
        expect(promo.getEndAt()).toEqual(new Date("2025-12-20"));
    });
    
    it('devrait rejeter un nom vide', () => {
        expect(() => {
            new PromoBuilder()
                .beginAt(new Date("2025-01-15"))
                .endAt(new Date("2025-12-20"))
                .build();
        }).toThrow("Promo name is required");
    });
    
    it('devrait rejeter des dates incohérentes', () => {
        expect(() => {
            new PromoBuilder()
                .name("Dev 2025")
                .beginAt(new Date("2025-12-20"))
                .endAt(new Date("2025-01-15"))
                .build();
        }).toThrow("Incorrect dates");
    });
    
    it('devrait permettre le chaînage de méthodes', () => {
        const builder = new PromoBuilder();
        
        const result1 = builder.name("Dev 2025");
        const result2 = result1.beginAt(new Date());
        
        expect(result1).toBe(builder);
        expect(result2).toBe(builder);
    });
});

Avantages et inconvénients
#

Avantages
#

Avantage Description Exemple
Code lisible Syntaxe fluide et expressive .name("Dev").beginAt(date)
Validation centralisée Toutes les vérifications en un lieu Méthode build()
Construction atomique Erreur avant création -
Flexibilité Ordre des paramètres libre Commence par n’importe quel setter
Réutilisabilité Builder réutilisable Créer plusieurs objets similaires
Immutabilité possible Objet final peut être immutable Pas de setters publics

Inconvénients
#

Inconvénient Description Impact
Code verbeux Classe builder supplémentaire Plus de fichiers
Duplication Attributs répétés (classe + builder) Maintenance accrue
Complexité Pattern lourd pour objets simples Overhead inutile
Mémoire Instance builder temporaire Garbage collection

Quand utiliser le builder ?
#

Utilisez le Builder pour :

// Objet avec nombreux paramètres
class Configuration {
    // 15+ paramètres
}
const config = new ConfigurationBuilder()
    .host("localhost")
    .port(3000)
    .timeout(5000)
    .retries(3)
    // ...
    .build();

// Objets avec validations complexes
class User {
    // Validations multiples interdépendantes
}
const user = new UserBuilder()
    .email("test@example.com")
    .password("secure123")
    .confirmPassword("secure123")
    .age(25)
    .build();

// Création d'objets immutables
class Invoice {
    // Readonly après construction
}
const invoice = new InvoiceBuilder()
    .customer(customer)
    .items(items)
    .build(); // Objet immutable

N’utilisez PAS le Builder pour :

// Objets simples
class Point {
    constructor(public x: number, public y: number) {}
}
// new Point(10, 20) suffit

// Objets avec peu de paramètres
class Color {
    constructor(public r: number, public g: number, public b: number) {}
}
// Pas besoin de ColorBuilder

// Objets sans validation
class Label {
    constructor(public text: string) {}
}
// Trop simple pour un builder

Variantes du pattern
#

Builder avec valeurs par défaut
#

export class PromoBuilderWithDefaults implements BuilderInterface<Promo> {
    private _name: string = 'Promo Sans Nom'
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // +1 an
    
    public name(name: string): PromoBuilderWithDefaults {
        this._name = name
        return this
    }

    public beginAt(date: Date): PromoBuilderWithDefaults {
        this._beginAt = date
        return this
    }

    public endAt(date: Date): PromoBuilderWithDefaults {
        this._endAt = date
        return this
    }

    build(): Promo {
        // Pas d'erreur si nom manquant (valeur par défaut)
        if (this._beginAt > this._endAt) {
            throw new Error("Incorrect dates");
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)

        return promo
    }
}

// Utilisation
const promo = new PromoBuilderWithDefaults().build(); // Utilise les défauts

Builder avec étapes obligatoires (Step Builder)
#

// Étape 1 : Nom obligatoire
interface INameStep {
    name(name: string): IBeginDateStep;
}

// Étape 2 : Date de début obligatoire
interface IBeginDateStep {
    beginAt(date: Date): IEndDateStep;
}

// Étape 3 : Date de fin obligatoire
interface IEndDateStep {
    endAt(date: Date): IBuildStep;
}

// Étape 4 : Construction finale
interface IBuildStep {
    build(): Promo;
}

export class StepPromoBuilder implements INameStep, IBeginDateStep, IEndDateStep, IBuildStep {
    private _name: string = ''
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date()
    
    public name(name: string): IBeginDateStep {
        this._name = name
        return this
    }

    public beginAt(date: Date): IEndDateStep {
        this._beginAt = date
        return this
    }

    public endAt(date: Date): IBuildStep {
        this._endAt = date
        return this
    }

    build(): Promo {
        if (this._beginAt > this._endAt) {
            throw new Error("Incorrect dates");
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)

        return promo
    }
}

// Utilisation : Ordre forcé par TypeScript
const promo = new StepPromoBuilder()
    .name("Dev 2025")        // Doit être appelé en premier
    .beginAt(new Date())     // Puis celui-ci
    .endAt(new Date())       // Puis celui-ci
    .build();                // Enfin build()

Builder avec reset
#

export class ReusablePromoBuilder implements BuilderInterface<Promo> {
    private _name: string = ''
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date()
    
    public name(name: string): ReusablePromoBuilder {
        this._name = name
        return this
    }

    public beginAt(date: Date): ReusablePromoBuilder {
        this._beginAt = date
        return this
    }

    public endAt(date: Date): ReusablePromoBuilder {
        this._endAt = date
        return this
    }

    // Méthode pour réinitialiser le builder
    public reset(): ReusablePromoBuilder {
        this._name = ''
        this._beginAt = new Date()
        this._endAt = new Date()
        return this
    }

    build(): Promo {
        if (this._name === '') {
            throw new Error('Promo name is required')
        }

        if (this._beginAt > this._endAt) {
            throw new Error("Incorrect dates");
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)

        return promo
    }
}

// Utilisation : Réutilisation du même builder
const builder = new ReusablePromoBuilder();

const promo1 = builder
    .name("Promo A")
    .beginAt(new Date("2025-01-01"))
    .endAt(new Date("2025-06-30"))
    .build();

const promo2 = builder
    .reset() // Réinitialisation
    .name("Promo B")
    .beginAt(new Date("2025-07-01"))
    .endAt(new Date("2025-12-31"))
    .build();

Builder avec director (Pattern Complet)
#

// Director : encapsule des constructions prédéfinies
export class PromoDirector {
    constructor(private builder: PromoBuilder) {}
    
    // Construction standard
    public buildStandardPromo(name: string, year: number): Promo {
        return this.builder
            .name(name)
            .beginAt(new Date(`${year}-01-15`))
            .endAt(new Date(`${year}-12-20`))
            .build();
    }
    
    // Construction pour stage d'été
    public buildSummerPromo(name: string, year: number): Promo {
        return this.builder
            .name(`${name} - Été`)
            .beginAt(new Date(`${year}-06-01`))
            .endAt(new Date(`${year}-08-31`))
            .build();
    }
    
    // Construction intensive (bootcamp)
    public buildBootcamp(name: string): Promo {
        const start = new Date();
        const end = new Date();
        end.setDate(end.getDate() + 90); // 3 mois
        
        return this.builder
            .name(`Bootcamp ${name}`)
            .beginAt(start)
            .endAt(end)
            .build();
    }
}

// Utilisation
const builder = new PromoBuilder();
const director = new PromoDirector(builder);

const promo1 = director.buildStandardPromo("Dev Web", 2025);
const promo2 = director.buildSummerPromo("Dev Mobile", 2025);
const promo3 = director.buildBootcamp("Full Stack");

Bonnes pratiques
#

Nommage cohérent
#

// BON : Noms clairs et cohérents
class PromoBuilder {
    name(name: string): PromoBuilder { /* ... */ }
    beginAt(date: Date): PromoBuilder { /* ... */ }
    endAt(date: Date): PromoBuilder { /* ... */ }
}

// MAUVAIS : Noms incohérents
class PromoBuilder {
    setName(name: string): PromoBuilder { /* ... */ }
    withBeginDate(date: Date): PromoBuilder { /* ... */ }
    endingAt(date: Date): PromoBuilder { /* ... */ }
}

Validation complète
#

build(): Promo {
    // Toutes les validations en un bloc
    const errors: string[] = [];
    
    if (this._name === '') {
        errors.push('Promo name is required');
    }
    
    if (this._name.length < 3) {
        errors.push('Promo name must be at least 3 characters');
    }
    
    if (this._beginAt > this._endAt) {
        errors.push('Begin date must be before end date');
    }
    
    if (this._endAt < new Date()) {
        errors.push('End date must be in the future');
    }
    
    // Lever toutes les erreurs d'un coup
    if (errors.length > 0) {
        throw new Error(`Validation failed:\n- ${errors.join('\n- ')}`);
    }
    
    // Construction
    const promo = new Promo()
    promo.setName(this._name)
    promo.setBeginAt(this._beginAt)
    promo.setEndAt(this._endAt)

    return promo
}

Documentation claire
#

/**
 * Builder pour la création de promotions étudiantes
 * 
 * @example
 * ```typescript
 * const promo = new PromoBuilder()
 *     .name("Dev Web 2025")
 *     .beginAt(new Date("2025-01-15"))
 *     .endAt(new Date("2025-12-20"))
 *     .build();
 * ```
 * 
 * @throws {Error} Si le nom est vide
 * @throws {Error} Si les dates sont incohérentes
 */
export class PromoBuilder implements BuilderInterface<Promo> {
    
    /**
     * Définit le nom de la promotion
     * 
     * @param name - Nom de la promotion (minimum 3 caractères)
     * @returns L'instance du builder pour chaînage
     */
    public name(name: string): PromoBuilder {
        this._name = name
        return this
    }
    
    // ...
}

Types de retour explicites
#

// BON : Type de retour explicite
public name(name: string): PromoBuilder {
    this._name = name
    return this
}

// MAUVAIS : Type inféré (moins clair)
public name(name: string) {
    this._name = name
    return this
}

Méthodes optionnelles
#

export class PromoBuilder implements BuilderInterface<Promo> {
    private _name: string = ''
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date()
    private _maxStudents?: number // Optionnel
    private _description?: string // Optionnel
    
    // Méthodes obligatoires
    public name(name: string): PromoBuilder {
        this._name = name
        return this
    }
    
    // Méthodes optionnelles
    public maxStudents(max: number): PromoBuilder {
        this._maxStudents = max
        return this
    }
    
    public description(desc: string): PromoBuilder {
        this._description = desc
        return this
    }

    build(): Promo {
        // Validations sur champs obligatoires uniquement
        if (this._name === '') {
            throw new Error('Promo name is required')
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)
        
        // Champs optionnels
        if (this._maxStudents !== undefined) {
            promo.setMaxStudents(this._maxStudents)
        }
        
        if (this._description !== undefined) {
            promo.setDescription(this._description)
        }

        return promo
    }
}

Pattern Builder avec TypeScript avancé
#

Builder générique réutilisable
#

// Builder générique pour n'importe quelle classe
export abstract class GenericBuilder<T> implements BuilderInterface<T> {
    protected properties: Partial<T> = {}
    
    protected set<K extends keyof T>(key: K, value: T[K]): this {
        this.properties[key] = value
        return this
    }
    
    protected validate(): void {
        // À surcharger dans les sous-classes
    }
    
    abstract build(): T
}

// Utilisation spécifique
class PromoGenericBuilder extends GenericBuilder<Promo> {
    name(name: string): this {
        return this.set('name' as any, name)
    }
    
    beginAt(date: Date): this {
        return this.set('beginAt' as any, date)
    }
    
    endAt(date: Date): this {
        return this.set('endAt' as any, date)
    }
    
    protected validate(): void {
        if (!this.properties.name) {
            throw new Error('Name is required')
        }
    }
    
    build(): Promo {
        this.validate()
        
        const promo = new Promo()
        promo.setName(this.properties.name as string)
        promo.setBeginAt(this.properties.beginAt as Date)
        promo.setEndAt(this.properties.endAt as Date)
        
        return promo
    }
}

Builder avec types conditionnels
#

// Types pour forcer les étapes obligatoires
type RequiredFields<T> = {
    [K in keyof T]-?: T[K]
}

type OptionalFields<T> = {
    [K in keyof T]?: T[K]
}

// Builder typé strictement
class TypedPromoBuilder {
    private data: Partial<{
        name: string
        beginAt: Date
        endAt: Date
        maxStudents: number
    }> = {}
    
    name(name: string): Omit<TypedPromoBuilder, 'name'> {
        this.data.name = name
        return this
    }
    
    beginAt(date: Date): Omit<TypedPromoBuilder, 'beginAt'> {
        this.data.beginAt = date
        return this
    }
    
    endAt(date: Date): Omit<TypedPromoBuilder, 'endAt'> {
        this.data.endAt = date
        return this
    }
    
    maxStudents(max: number): this {
        this.data.maxStudents = max
        return this
    }
    
    build(): Promo {
        // TypeScript garantit que toutes les méthodes ont été appelées
        const promo = new Promo()
        promo.setName(this.data.name!)
        promo.setBeginAt(this.data.beginAt!)
        promo.setEndAt(this.data.endAt!)
        
        return promo
    }
}

Builder avec validation progressive
#

class ValidatingPromoBuilder {
    private _name: string = ''
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date()
    private errors: Map<string, string> = new Map()
    
    name(name: string): ValidatingPromoBuilder {
        this._name = name
        
        // Validation immédiate
        if (name.length < 3) {
            this.errors.set('name', 'Name must be at least 3 characters')
        } else {
            this.errors.delete('name')
        }
        
        return this
    }
    
    beginAt(date: Date): ValidatingPromoBuilder {
        this._beginAt = date
        
        // Validation de cohérence avec endAt
        if (this._endAt && date > this._endAt) {
            this.errors.set('dates', 'Begin date must be before end date')
        } else {
            this.errors.delete('dates')
        }
        
        return this
    }
    
    endAt(date: Date): ValidatingPromoBuilder {
        this._endAt = date
        
        // Validation de cohérence avec beginAt
        if (this._beginAt && this._beginAt > date) {
            this.errors.set('dates', 'End date must be after begin date')
        } else {
            this.errors.delete('dates')
        }
        
        return this
    }
    
    // Vérifier les erreurs à tout moment
    hasErrors(): boolean {
        return this.errors.size > 0
    }
    
    getErrors(): string[] {
        return Array.from(this.errors.values())
    }
    
    build(): Promo {
        if (this.hasErrors()) {
            throw new Error(`Validation errors:\n- ${this.getErrors().join('\n- ')}`)
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)
        
        return promo
    }
}

// Utilisation avec feedback progressif
const builder = new ValidatingPromoBuilder()
    .name('Ab') // Trop court
    .beginAt(new Date('2025-12-31'))
    .endAt(new Date('2025-01-01')); // Incohérent

console.log(builder.hasErrors()); // true
console.log(builder.getErrors()); 
// ['Name must be at least 3 characters', 'End date must be after begin date']

Anti-Patterns à éviter
#

Builder trop complexe
#

// MAUVAIS : Builder qui fait trop de choses
class GodPromoBuilder {
    private promo: Promo
    
    constructor() {
        this.promo = new Promo()
    }
    
    name(name: string): this {
        this.promo.setName(name)
        // Logique métier dans le builder (MAUVAIS)
        this.notifyAdmins(`New promo: ${name}`)
        this.createDatabaseEntry()
        this.generateReports()
        return this
    }
    
    // Trop de responsabilités
    private notifyAdmins(message: string): void { /* ... */ }
    private createDatabaseEntry(): void { /* ... */ }
    private generateReports(): void { /* ... */ }
    
    build(): Promo {
        return this.promo
    }
}

// BON : Builder simple, logique séparée
class SimplePromoBuilder {
    private _name: string = ''
    
    name(name: string): this {
        this._name = name
        return this
    }
    
    build(): Promo {
        const promo = new Promo()
        promo.setName(this._name)
        return promo
    }
}

// Logique métier ailleurs
class PromoService {
    create(promo: Promo): void {
        this.notifyAdmins(promo)
        this.saveToDB(promo)
        this.generateReports(promo)
    }
}

Modification de l’objet après construction
#

// MAUVAIS : Builder qui modifie l'objet construit
class MutablePromoBuilder {
    private promo: Promo = new Promo()
    
    name(name: string): this {
        this.promo.setName(name)
        return this
    }
    
    build(): Promo {
        // Retourne la référence directe (MAUVAIS)
        return this.promo
    }
}

const builder = new MutablePromoBuilder()
const promo1 = builder.name('Promo A').build()
const promo2 = builder.name('Promo B').build() // Modifie aussi promo1 !

console.log(promo1.getName()) // "Promo B" (PROBLÈME)

// BON : Créer une nouvelle instance à chaque build()
class ImmutablePromoBuilder {
    private _name: string = ''
    
    name(name: string): this {
        this._name = name
        return this
    }
    
    build(): Promo {
        // Nouvelle instance à chaque fois
        const promo = new Promo()
        promo.setName(this._name)
        return promo
    }
}

Validation insuffisante
#

// MAUVAIS : Pas de validation
class UnsafePromoBuilder {
    private _name: string = ''
    
    name(name: string): this {
        this._name = name
        return this
    }
    
    build(): Promo {
        // Aucune validation (MAUVAIS)
        const promo = new Promo()
        promo.setName(this._name)
        return promo
    }
}

// Permet de créer des objets invalides
const promo = new UnsafePromoBuilder().build() // Nom vide !

// BON : Validation complète
class SafePromoBuilder {
    private _name: string = ''
    
    name(name: string): this {
        this._name = name
        return this
    }
    
    build(): Promo {
        if (this._name === '') {
            throw new Error('Name is required')
        }
        
        if (this._name.length < 3) {
            throw new Error('Name must be at least 3 characters')
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        return promo
    }
}

Tests avancés
#

Tests de validation
#

describe('PromoBuilder - Validation', () => {
    
    it('devrait valider le nom minimum', () => {
        expect(() => {
            new PromoBuilder()
                .name('AB') // Trop court
                .beginAt(new Date())
                .endAt(new Date())
                .build()
        }).toThrow('at least 3 characters')
    })
    
    it('devrait valider les dates futures', () => {
        const pastDate = new Date('2020-01-01')
        
        expect(() => {
            new PromoBuilder()
                .name('Dev 2025')
                .beginAt(pastDate)
                .endAt(pastDate)
                .build()
        }).toThrow('must be in the future')
    })
    
    it('devrait valider la durée minimale', () => {
        const start = new Date('2025-01-01')
        const end = new Date('2025-01-02') // Trop court
        
        expect(() => {
            new PromoBuilder()
                .name('Dev 2025')
                .beginAt(start)
                .endAt(end)
                .build()
        }).toThrow('minimum duration')
    })
})

Tests d’immutabilité
#

describe('PromoBuilder - Immutabilité', () => {
    
    it('ne devrait pas affecter les builds précédents', () => {
        const builder = new PromoBuilder()
        
        const promo1 = builder
            .name('Promo A')
            .beginAt(new Date('2025-01-01'))
            .endAt(new Date('2025-06-30'))
            .build()
        
        const promo2 = builder
            .name('Promo B')
            .beginAt(new Date('2025-07-01'))
            .endAt(new Date('2025-12-31'))
            .build()
        
        expect(promo1.getName()).toBe('Promo A')
        expect(promo2.getName()).toBe('Promo B')
        expect(promo1).not.toBe(promo2)
    })
})

Tests de performance
#

describe('PromoBuilder - Performance', () => {
    
    it('devrait construire rapidement 1000 promos', () => {
        const start = Date.now()
        
        for (let i = 0; i < 1000; i++) {
            new PromoBuilder()
                .name(`Promo ${i}`)
                .beginAt(new Date())
                .endAt(new Date())
                .build()
        }
        
        const duration = Date.now() - start
        expect(duration).toBeLessThan(1000) // < 1 seconde
    })
})

Conclusion
#

Récapitulatif
#

Le pattern Builder est idéal pour construire des objets complexes avec validation :

Avantages Inconvénients
Code lisible et expressif Code plus verbeux
Validation centralisée Classe supplémentaire
Objets toujours valides Duplication des attributs
Flexibilité totale Complexité accrue
Immutabilité possible Overhead mémoire

Quand l’utiliser ?
#

Utilisez le Builder pour :

  • Objets avec nombreux paramètres (plus de 4-5)
  • Validation complexe avant construction
  • Objets immutables après création
  • Construction en plusieurs étapes logiques
  • API publiques nécessitant une bonne ergonomie

Évitez le Builder pour :

  • Objets simples avec peu de paramètres
  • Objets sans validation
  • Performance critique (overhead du builder)
  • Objets internes non exposés

Articles connexes

Le pattern Singleton - garantir une instance unique
·12 mins· loading · loading
Conception Back-End
Single Responsibility Principle (SRP) - Refactoring d'une Classe Invoice
·14 mins· loading · loading
Conception Back-End
Les Interfaces en Programmation Orientée Objet - Principe SOLID et Exemple Pratique
·7 mins· loading · loading
Conception Back-End
Modélisation de base de données - Le cas Langlois
·7 mins· loading · loading
Conception Back-End
Les patrons de conception
·7 mins· loading · loading
Conception
Documentation et Google Workspace - Guide Pratique pour Développeurs
·13 mins· loading · loading
Documentation