Le dossier était épais. Des photos d'un objet sous tous les angles. Pas n'importe lequel : une Commande. Client, adresses, lignes, promotions, paiement, statut. Le rapport initial disait : "Cause de la rigidité : Constructeur à 15 paramètres." Ils avaient essayé de tout fournir d'un coup. Résultat : un cadavre logiciel, impossible à créer partiellement, inextensible, illisible. Ils avaient besoin de quelqu'un pour orchestrer la construction, étape par étape. Ils avaient besoin d'un Builder.
Construction étape par étape
Certains objets sont complexes. Leur construction n'est pas un acte instantané (new), c'est un processus. Il implique des étapes, des validations, une logique. Le pattern Builder sépare la construction d'un objet complexe de sa représentation, afin que le même processus de construction puisse créer différentes représentations.
L'idée est simple : au lieu d'avoir un constructeur monstrueux ou plusieurs constructeurs surchargés, on a un objet dédié, le Builder, qui accumule les paramètres étape par étape, via des méthodes fluides et claires, et qui finalement, produit l'objet désiré.
Objets complexes
Une Commande est le parfait suspect.
- Elle a de nombreuses parties : en-tête (client, date), corps (lignes), pied (total, promotions).
- Certaines parties sont optionnelles (promotion, adresse de livraison spéciale).
- La logique de construction peut être complexe : calcul du total, application des règles métier.
- On peut vouloir des variantes : une commande simple, une commande express, une commande avec acompte.
Le constructeur classique est une impasse :
// L'ANTI-PATTERN : Le constructeur télescope.
class Commande {
constructor(
public clientId: string,
public items: LigneCommande[],
public adresseLivraison: Adresse,
public adresseFacturation: Adresse | null,
public modePaiement: string,
public codePromo: string | null,
public estExpress: boolean,
// ... 8 autres paramètres ...
) {
// La validation devient un enfer. L'ordre des paramètres est crucial.
}
}
// Création illisible et sujette aux erreurs.
const c = new Commande('client123', items, adrLiv, adrFac, 'CARD', null, true, ...);
Le Builder en action
// 1. Le Produit Complexe
class Commande {
// Notez : les propriétés peuvent être privées et finales (immutables).
constructor(
public readonly clientId: string,
public readonly items: readonly LigneCommande[],
public readonly adresseLivraison: Adresse,
public readonly adresseFacturation: Adresse,
public readonly modePaiement: string,
public readonly codePromo?: string,
public readonly estExpress: boolean = false
) {}
}
// 2. L'Interface Builder (contrat de construction étape par étape)
interface CommandeBuilder {
setClient(clientId: string): this;
ajouterItem(produitId: string, quantite: number): this;
setAdresseLivraison(adresse: Adresse): this;
setAdresseFacturation(adresse: Adresse): this;
utiliserModePaiement(mode: string): this;
appliquerCodePromo(code: string): this;
marquerExpress(): this;
build(): Commande; // L'étape finale qui produit l'objet.
}
// 3. Le Concrete Builder (implémentation par défaut)
class ConcreteCommandeBuilder implements CommandeBuilder {
private clientId?: string;
private items: LigneCommande[] = [];
private adresseLivraison?: Adresse;
private adresseFacturation?: Adresse;
private modePaiement: string = 'CARTE';
private codePromo?: string;
private estExpress: boolean = false;
setClient(clientId: string): this {
// Validation possible
if (!clientId) throw new Error('Client invalide');
this.clientId = clientId;
return this; // Retourne `this` pour le chaînage fluide.
}
ajouterItem(produitId: string, quantite: number): this {
this.items.push(new LigneCommande(produitId, quantite));
return this;
}
setAdresseLivraison(adresse: Adresse): this {
this.adresseLivraison = adresse;
// Si l'adresse de facturation n'est pas encore définie, on la copie.
if (!this.adresseFacturation) {
this.adresseFacturation = adresse;
}
return this;
}
setAdresseFacturation(adresse: Adresse): this {
this.adresseFacturation = adresse;
return this;
}
utiliserModePaiement(mode: string): this {
this.modePaiement = mode;
return this;
}
appliquerCodePromo(code: string): this {
this.codePromo = code;
return this;
}
marquerExpress(): this {
this.estExpress = true;
return this;
}
// Méthode finale qui valide et construit l'objet immuable.
build(): Commande {
// VALIDATION CENTRALISÉE
if (!this.clientId) throw new Error('Client manquant');
if (this.items.length === 0) throw new Error('Panier vide');
if (!this.adresseLivraison) throw new Error('Adresse manquante');
// Logique métier complexe peut être ici (calcul de réduction, etc.)
return new Commande(
this.clientId,
[...this.items], // Copie pour éviter les modifications externes
this.adresseLivraison,
this.adresseFacturation!,
this.modePaiement,
this.codePromo,
this.estExpress
);
}
}
// 4. Le Directeur (Optionnel - pour encapsuler des recettes de construction communes)
class DirecteurCommandes {
static construireCommandeExpress(clientId: string, itemPrincipal: string): Commande {
return new ConcreteCommandeBuilder()
.setClient(clientId)
.ajouterItem(itemPrincipal, 1)
.marquerExpress()
.setAdresseLivraison(Adresse.USINE) // Adresse prédéfinie
.utiliserModePaiement('EXPRESS_PAY')
.build();
}
static construireCommandeStandard(clientId: string): CommandeBuilder {
// Peut aussi retourner le builder pour personnalisation ultérieure.
return new ConcreteCommandeBuilder().setClient(clientId);
}
}
// 5. UTILISATION PAR LE CLIENT
// Méthode fluide, claire, étape par étape.
const commande1: Commande = new ConcreteCommandeBuilder()
.setClient('client_789')
.ajouterItem('livre_456', 2)
.ajouterItem('cafe_123', 1)
.setAdresseLivraison(new Adresse('123 Rue Principale', 'Paris'))
.appliquerCodePromo('ETE2024')
.build();
console.log(commande1); // Commande immuable et valide.
// Utilisation du Directeur pour une recette prédéfinie.
const commandeExpress = DirecteurCommandes.construireCommandeExpress('client_555', 'urgent_001');
console.log(commandeExpress);
// Construction personnalisée avec le Directeur qui retourne un Builder.
const builder = DirecteurCommandes.construireCommandeStandard('client_999');
const commande2 = builder
.ajouterItem('produit_x', 5)
.setAdresseFacturation(new Adresse('Siège Social', 'Lyon'))
.build();
Immutabilité
Le Builder est souvent le compagnon idéal des objets immuables. L'objet final (Commande) peut être déclaré avec des propriétés readonly. Le Builder, lui, est mutable et accumule l'état. Au moment du build(), il fige tout dans un objet immuable. C'est le meilleur des deux mondes : une construction flexible et un produit final sûr, thread-safe (en théorie JS) et prévisible.
Différences avec Factory
C'est une question cruciale. Factory et Builder résolvent tous deux des problèmes de création, mais pas les mêmes.
| Aspect | Factory (Méthode/Abstraite) | Builder |
|---|---|---|
| Intention | Créer un objet (souvent en une étape). Cacher quelle classe concrète est instanciée. | Construire un objet complexe étape par étape. Offrir un contrôle fin sur le processus de construction. |
| Processus | Généralement un appel unique (createProduct()). | Une séquence d'appels (setA().setB().setC().build()). |
| Focus | Quel objet créer ? (Le type du produit). | Comment construire un objet spécifique ? (L'assemblage des parties). |
| Complexité | Pour des objets dont la construction est simple ou standardisée. | Pour des objets avec de nombreuses parties, une construction paramétrable, des étapes de validation. |
| Produit final | Peut retourner n'importe quel sous-type d'une hiérarchie. | Construit généralement un type d'objet spécifique, mais peut en configurer différentes représentations. |
| Analogie | Un recruteur qui vous envoie le bon candidat (un développeur) sans vous dire comment il l'a trouvé. | Un architecte qui supervise la construction d'une maison, pièce par pièce, selon vos spécifications. |
En résumé :
- Besoin de choisir une implémentation parmi plusieurs, de façon polymorphique ? → Factory.
- Besoin de configurer méticuleusement un objet complexe, avec de nombreuses options, de manière lisible et robuste ? → Builder.
Je rangeai les photos de la Commande. Le Builder n'était pas une simple technique de code. C'était une discipline. Il forçait à penser la construction comme un processus, à centraliser la validation, à rendre le code client lisible et intentionnel.
Il transformait un acte opaque (new Commande(...)) en une narration claire : "Pour ce client, ajoute ces articles, livre là-bas, facture ici, avec ce code promo." Chaque étape était compréhensible.
Le prochain pattern serait le dernier de la famille Créationnelle. Le Prototype. Pour quand la copie est moins chère que la création. Quand le clonage est l'opération primordiale.
Mais le Builder, lui, restait comme l'artisan méticuleux. Celui qui savait que pour monter un dossier solide, il faut y aller pièce par pièce, preuve après preuve, jusqu'à ce que l'ensemble tienne debout, immuable et incontestable.