Les patrons de création
Cinq façons de créer des objets avec souplesse : Fabrique, Fabrique abstraite, Monteur, Prototype et Singleton.
Créer un objet, c'est trivial : on appelle new et l'affaire est réglée. Sauf que ce petit mot, répété partout dans le code, tisse un réseau de dépendances invisibles. Chaque new Camion() cloue votre logique métier à une classe concrète précise. Le jour où il faut livrer par bateau plutôt que par camion, ce sont des dizaines de fichiers qu'il faut rouvrir, parsemés de conditions qui aiguillent le comportement selon le type d'objet manipulé.
Les patrons de création (creational patterns) répondent tous à ce problème, sous des angles différents : ils prennent en charge la mécanique d'instanciation pour la rendre plus souple et découplée du reste du code. Au lieu de laisser le client décider quelle classe instancier et comment, ils déplacent cette décision dans un endroit dédié et remplaçable. Ce chapitre couvre les cinq patrons de création du catalogue : la fabrique (Factory Method), la fabrique abstraite (Abstract Factory), le monteur (Builder), le prototype (Prototype) et le singleton (Singleton).
Note
Beaucoup de conceptions commencent par la fabrique — moins compliquée, personnalisable par sous-classes — puis évoluent vers la fabrique abstraite, le prototype ou le monteur : plus flexibles, mais plus complexes. Inutile de dégainer le plus lourd dès le départ.
La fabrique (Factory Method)
Intention
Définir une méthode pour créer des objets dans une classe parente, tout en laissant les sous-classes décider de la classe concrète à instancier.
Le problème
Imaginez que vous développez une application de gestion logistique. La première version ne gère que le transport par camions : l'essentiel du code vit donc dans la classe Camion. L'application devient populaire et, chaque jour, des compagnies de transport maritime vous réclament la prise en charge des bateaux.
Excellente nouvelle pour le business. Mais côté code, c'est la douche froide : presque tout est couplé à la classe Camion. Ajouter les bateaux exigerait de toucher à l'ensemble de la base de code. Et si demain vous ajoutez l'avion ou le train, il faudra recommencer. Vous finiriez avec un code horrible, truffé de conditions qui changent le comportement de l'application selon la classe de transport manipulée.
La solution
La fabrique suggère de remplacer les appels directs au constructeur (avec new) par des appels à une méthode-fabrique spéciale, redéfinissable. Les objets sont toujours créés via new, mais à l'intérieur de cette méthode. Les objets qu'elle renvoie sont appelés des « produits ».
À première vue, on a juste déplacé l'appel au constructeur. Mais la bascule est là : on peut désormais redéfinir la méthode-fabrique dans une sous-classe et changer la classe des produits créés. Une seule limite : les produits renvoyés doivent partager une interface commune, et la méthode-fabrique de la classe de base doit déclarer ce type d'interface comme valeur de retour.
Ainsi, Camion et Bateau implémentent tous deux l'interface Transport (avec une méthode livrer). La fabrique de LogistiqueRoutiere renvoie des camions, celle de LogistiqueMaritime renvoie des bateaux. Le code client ne voit pas la différence : il traite tout comme un Transport abstrait.
// Le produit : interface commune à tous les transports.
interface Transport {
livrer(): string;
}
class Camion implements Transport {
livrer(): string {
return "Livraison par la route, dans une caisse.";
}
}
class Bateau implements Transport {
livrer(): string {
return "Livraison par la mer, dans un conteneur.";
}
}
// Le créateur : il contient la logique métier et délègue
// la création du produit à la méthode-fabrique.
abstract class Logistique {
// La méthode-fabrique, redéfinie par les sous-classes.
abstract creerTransport(): Transport;
// La logique métier s'appuie sur le produit, sans
// connaître sa classe concrète.
planifierLivraison(): string {
const transport = this.creerTransport();
return `Planification : ${transport.livrer()}`;
}
}
class LogistiqueRoutiere extends Logistique {
creerTransport(): Transport {
return new Camion();
}
}
class LogistiqueMaritime extends Logistique {
creerTransport(): Transport {
return new Bateau();
}
}
// Le client choisit la sous-classe selon la configuration,
// puis travaille avec elle via l'interface de base.
function lancer(logistique: Logistique): void {
console.log(logistique.planifierLivraison());
}
lancer(new LogistiqueRoutiere());
lancer(new LogistiqueMaritime()); Notez que la création de produits n'est pas la responsabilité première du créateur. La classe Logistique a déjà une logique métier centrale (planifierLivraison) ; la méthode-fabrique sert juste à la découpler des classes de produits concrètes. L'analogie de Shvets : une grande entreprise de logiciels peut avoir un service de formation pour ses programmeurs, mais sa fonction première reste d'écrire du code, pas de produire des programmeurs.
Quand l'utiliser
- Quand vous ne connaissez pas à l'avance les types et dépendances exacts des objets que votre code devra manipuler. Pour ajouter un nouveau produit, il suffira de créer une nouvelle sous-classe de créateur.
- Quand vous voulez offrir aux utilisateurs de votre bibliothèque ou framework un moyen d'en étendre les composants internes : ils redéfinissent la méthode-fabrique pour substituer leur propre composant au composant par défaut.
- Quand vous voulez économiser des ressources en réutilisant des objets existants (connexions à une base, fichiers) plutôt que de les recréer. Un constructeur renvoie toujours un objet neuf ; une méthode-fabrique, elle, peut renvoyer un objet d'un cache ou d'un pool.
Piège courant
Le code peut se complexifier, car le patron impose d'introduire beaucoup de nouvelles sous-classes. Le scénario idéal est d'introduire la fabrique dans une hiérarchie de créateurs déjà existante, sans en inventer une de toutes pièces.
La fabrique abstraite (Abstract Factory)
Intention
Créer des familles d'objets liés et cohérents entre eux, sans préciser leurs classes concrètes.
Le problème
Vous codez un simulateur de magasin de meubles. Votre code manipule une famille de produits liés — Chaise, Canape, TableBasse — disponibles en plusieurs variantes : Moderne, Victorien, ArtDeco.
Il vous faut un moyen de créer des objets individuels qui s'accordent avec les autres objets de la même famille : les clients deviennent fous quand ils reçoivent du mobilier dépareillé. Un canapé moderne ne va pas avec des chaises victoriennes. Par ailleurs, les fournisseurs mettent souvent à jour leur catalogue, et vous ne voulez pas réécrire le code central à chaque ajout de produit ou de famille.
La solution
La fabrique abstraite commence par déclarer une interface pour chaque produit distinct de la famille (Chaise, Canape, TableBasse). Toutes les variantes implémentent l'interface correspondante.
Ensuite, on déclare la fabrique abstraite : une interface dotée d'une méthode de création par produit (creerChaise, creerCanape…), chacune renvoyant le type abstrait du produit. Pour chaque variante de la famille, on crée alors une fabrique concrète distincte : la FabriqueModerne ne produit que des ChaiseModerne, CanapeModerne, etc. Le client travaille uniquement avec les interfaces abstraites des fabriques et des produits : on peut donc changer la fabrique injectée (et donc la variante reçue) sans casser le code client. Et quelle que soit la variante, les produits s'accorderont toujours entre eux.
// Produits abstraits : un type par produit de la famille.
interface Chaise {
surface(): string;
}
interface Canape {
longueur(): string;
}
// Variante moderne des produits.
class ChaiseModerne implements Chaise {
surface(): string {
return "chaise aux lignes épurées";
}
}
class CanapeModerne implements Canape {
longueur(): string {
return "canapé minimaliste deux places";
}
}
// Variante victorienne des produits.
class ChaiseVictorienne implements Chaise {
surface(): string {
return "chaise sculptée en bois massif";
}
}
class CanapeVictorien implements Canape {
longueur(): string {
return "canapé capitonné à pieds tournés";
}
}
// La fabrique abstraite : une méthode par produit.
interface FabriqueMobilier {
creerChaise(): Chaise;
creerCanape(): Canape;
}
// Chaque fabrique concrète couvre une variante entière.
class FabriqueModerne implements FabriqueMobilier {
creerChaise(): Chaise {
return new ChaiseModerne();
}
creerCanape(): Canape {
return new CanapeModerne();
}
}
class FabriqueVictorienne implements FabriqueMobilier {
creerChaise(): Chaise {
return new ChaiseVictorienne();
}
creerCanape(): Canape {
return new CanapeVictorien();
}
}
// Le client ne connaît que les interfaces abstraites.
// Les produits qu'il reçoit sont toujours assortis.
class Salon {
private chaise: Chaise;
private canape: Canape;
constructor(fabrique: FabriqueMobilier) {
this.chaise = fabrique.creerChaise();
this.canape = fabrique.creerCanape();
}
decrire(): string {
return `${this.chaise.surface()} + ${this.canape.longueur()}`;
}
}
// L'application choisit la fabrique selon la configuration,
// généralement au démarrage.
const style = "moderne";
const fabrique: FabriqueMobilier =
style === "moderne" ? new FabriqueModerne() : new FabriqueVictorienne();
console.log(new Salon(fabrique).decrire()); Quand l'utiliser
- Quand votre code doit travailler avec diverses familles de produits liés, sans dépendre de leurs classes concrètes (inconnues à l'avance ou amenées à évoluer). Tant que vous créez les objets via l'interface, impossible de produire une variante incohérente.
- Quand une classe accumule un ensemble de méthodes-fabriques qui brouillent sa responsabilité première : il est temps d'extraire ces méthodes dans une vraie fabrique abstraite.
Astuce
La fabrique abstraite repose souvent sur un ensemble de fabriques. Comparée au monteur, elle retourne le produit immédiatement, alors que le monteur permet d'exécuter des étapes de construction supplémentaires avant de récupérer le résultat. Le revers de la médaille : beaucoup de nouvelles interfaces et classes apparaissent, et le code peut devenir plus compliqué qu'il ne le faudrait.
Le monteur (Builder)
Intention
Construire un objet complexe pas à pas, le même processus de construction pouvant produire des représentations différentes.
Le problème
Imaginez un objet complexe dont l'initialisation exige de remplir laborieusement de nombreux champs et objets imbriqués. Ce code se retrouve d'ordinaire enfoui dans un constructeur monstrueux à rallonge de paramètres — ou pire, éparpillé dans le code client.
Prenez la construction d'une Maison. Une maison simple, c'est quatre murs, un sol, une porte, deux fenêtres, un toit. Mais que faire pour une maison plus grande, avec arrière-cour, chauffage, plomberie, et pourquoi pas une piscine et un garage ? Première option : étendre Maison en une foule de sous-classes pour couvrir chaque combinaison — la hiérarchie explose. Deuxième option : un constructeur géant avec tous les paramètres possibles. Cela élimine les sous-classes, mais la plupart du temps la plupart des paramètres seront inutilisés, rendant les appels au constructeur hideux (la piscine ne concerne qu'une maison sur dix).
La solution
Le monteur extrait le code de construction de l'objet et le déplace dans des objets séparés appelés monteurs. Il organise la construction en une série d'étapes (construireMurs, construirePorte…). Pour créer un objet, on exécute la suite d'étapes voulue sur un monteur — et l'on n'appelle que les étapes nécessaires à la configuration désirée.
Quand certaines étapes exigent une implémentation différente selon la représentation (les murs d'un chalet en bois, ceux d'un château en pierre), on crée plusieurs monteurs concrets qui implémentent le même jeu d'étapes, mais différemment. On peut enfin extraire la séquence d'appels dans une classe directeur (Director), optionnelle : elle définit l'ordre des étapes pour des configurations réutilisables, tandis que le monteur en fournit l'implémentation.
// Le produit complexe à construire.
class Maison {
murs = 0;
toit = false;
garage = false;
piscine = false;
decrire(): string {
const options = [
this.garage ? "garage" : null,
this.piscine ? "piscine" : null,
].filter(Boolean);
return `Maison de ${this.murs} murs` +
`${this.toit ? " avec toit" : ""}` +
`${options.length ? ", " + options.join(" et ") : ""}.`;
}
}
// L'interface du monteur : les étapes communes.
interface MonteurMaison {
reinitialiser(): void;
construireMurs(nombre: number): void;
construireToit(): void;
construireGarage(): void;
construirePiscine(): void;
}
// Un monteur concret, qui assemble la maison étape par étape.
class MonteurMaisonStandard implements MonteurMaison {
private maison = new Maison();
reinitialiser(): void {
this.maison = new Maison();
}
construireMurs(nombre: number): void {
this.maison.murs = nombre;
}
construireToit(): void {
this.maison.toit = true;
}
construireGarage(): void {
this.maison.garage = true;
}
construirePiscine(): void {
this.maison.piscine = true;
}
// La récupération du résultat ne figure pas dans
// l'interface : des monteurs différents peuvent produire
// des objets sans interface commune.
recupererResultat(): Maison {
const produit = this.maison;
this.reinitialiser();
return produit;
}
}
// Le directeur : il connaît l'ordre des étapes pour des
// configurations courantes. Il est optionnel.
class Directeur {
construireMaisonMinimale(monteur: MonteurMaison): void {
monteur.reinitialiser();
monteur.construireMurs(4);
monteur.construireToit();
}
construireMaisonDeLuxe(monteur: MonteurMaison): void {
monteur.reinitialiser();
monteur.construireMurs(8);
monteur.construireToit();
monteur.construireGarage();
monteur.construirePiscine();
}
}
// Le client associe un monteur au directeur, lance la
// construction, puis récupère le résultat du monteur.
const directeur = new Directeur();
const monteur = new MonteurMaisonStandard();
directeur.construireMaisonDeLuxe(monteur);
console.log(monteur.recupererResultat().decrire());
// Pour une configuration sur mesure, on appelle les étapes
// directement, sans directeur.
monteur.reinitialiser();
monteur.construireMurs(6);
monteur.construirePiscine();
console.log(monteur.recupererResultat().decrire()); Le monteur ne donne jamais accès au produit inachevé pendant la construction, ce qui empêche le client de récupérer un résultat incomplet. Et contrairement aux autres patrons de création, il peut produire des objets sans interface commune (Shvets cite l'exemple d'une Voiture et de son Manuel papier, construits par le même processus mais radicalement différents) : c'est pourquoi la méthode de récupération du résultat ne figure pas dans l'interface du monteur.
Quand l'utiliser
- Pour vous débarrasser d'un « constructeur télescopique » — ces constructeurs surchargés à dix paramètres optionnels. Le monteur construit l'objet étape par étape, en n'utilisant que les étapes vraiment nécessaires.
- Quand votre code doit créer différentes représentations d'un produit (maisons en pierre ou en bois) à partir d'étapes de construction similaires qui ne diffèrent que par les détails.
- Pour construire des arbres Composite ou d'autres objets complexes : les étapes peuvent être différées, voire appelées récursivement pour bâtir un arbre.
À retenir
Le directeur n'est pas obligatoire : le client peut piloter le monteur directement. N'introduisez le directeur que lorsqu'il existe des routines de construction à réutiliser à plusieurs endroits, ou pour masquer totalement les détails de construction au client. La complexité globale augmente, puisque le patron exige plusieurs classes nouvelles.
Le prototype (Prototype)
Intention
Cloner des objets existants via une méthode cloner(), sans coupler votre code à leurs classes concrètes ni à leur état interne.
Le problème
Vous voulez créer une copie exacte d'un objet. La méthode naïve : créer un nouvel objet de la même classe, puis recopier un à un les champs de l'original. Deux pièges. D'abord, certains champs peuvent être privés et invisibles depuis l'extérieur de l'objet. Ensuite, copier « de l'extérieur » vous force à connaître la classe de l'objet : votre code en devient dépendant. Or parfois vous ne connaissez que l'interface que l'objet respecte, pas sa classe concrète — par exemple quand un paramètre de méthode accepte n'importe quel objet suivant une interface donnée.
La solution
Le prototype délègue le clonage à l'objet lui-même. On déclare une interface commune à tous les objets clonables, contenant en général une unique méthode cloner(). Cette méthode crée un objet de la classe courante et y reporte les valeurs de tous les champs de l'original — y compris les champs privés, car la plupart des langages autorisent un objet à accéder aux champs privés d'un autre objet de la même classe.
Un objet qui supporte le clonage est un prototype. Quand vos objets ont des dizaines de champs et des centaines de configurations possibles, cloner un prototype pré-configuré devient une alternative au sous-classage : on prépare un jeu d'objets configurés de diverses façons, et quand on a besoin de l'un d'eux, on le clone au lieu de le reconstruire de zéro.
Note
L'analogie de Shvets est la division cellulaire (la mitose) : après division, deux cellules identiques se forment. La cellule d'origine joue le rôle de prototype et prend un rôle actif dans la création de sa copie — contrairement à un prototype industriel passif.
// L'interface prototype : une seule méthode de clonage.
interface Forme {
x: number;
y: number;
couleur: string;
cloner(): Forme;
}
abstract class FormeBase implements Forme {
x = 0;
y = 0;
couleur = "noir";
// Le constructeur de copie reporte les champs partagés.
// Une sous-classe l'appelle avant de copier les siens.
constructor(source?: Forme) {
if (source) {
this.x = source.x;
this.y = source.y;
this.couleur = source.couleur;
}
}
abstract cloner(): Forme;
}
class Rectangle extends FormeBase {
largeur = 0;
hauteur = 0;
constructor(source?: Rectangle) {
super(source);
if (source) {
this.largeur = source.largeur;
this.hauteur = source.hauteur;
}
}
cloner(): Forme {
return new Rectangle(this);
}
}
class Cercle extends FormeBase {
rayon = 0;
constructor(source?: Cercle) {
super(source);
if (source) {
this.rayon = source.rayon;
}
}
cloner(): Forme {
return new Cercle(this);
}
}
// Le client clone sans connaître la classe réelle :
// le polymorphisme appelle le bon `cloner`.
const formes: Forme[] = [];
const cercle = new Cercle();
cercle.x = 10;
cercle.rayon = 20;
formes.push(cercle);
formes.push(cercle.cloner()); // copie exacte du cercle
const rectangle = new Rectangle();
rectangle.largeur = 30;
rectangle.hauteur = 15;
formes.push(rectangle);
const copies = formes.map((forme) => forme.cloner());
console.log(copies.length); // 3 clones, types préservés Tant qu'on appelle cloner() sur une Forme, le programme vérifie sa classe réelle et exécute la méthode de clonage appropriée : on obtient de vrais clones (Cercle, Rectangle) et non de simples objets Forme. Notez que chaque classe doit explicitement redéfinir cloner() avec son propre nom de classe, sinon le clonage produirait un objet de la classe parente.
Quand l'utiliser
- Quand votre code ne doit pas dépendre des classes concrètes des objets à copier — fréquent lorsque les objets vous arrivent via une interface depuis du code tiers, dont vous ignorez les classes.
- Quand vous voulez réduire le nombre de sous-classes qui ne diffèrent que par la façon dont elles initialisent leurs objets. Plutôt qu'instancier la sous-classe correspondant à une configuration, le client cherche le prototype adéquat et le clone.
On peut centraliser les prototypes fréquents dans un registre de prototypes (un dictionnaire nom → prototype), pratique pour retrouver et cloner un objet par étiquette. Attention toutefois : cloner des objets aux références circulaires peut s'avérer très délicat.
Le singleton (Singleton)
Intention
Garantir qu'une classe n'a qu'une seule instance, tout en fournissant un point d'accès global à cette instance.
Le problème
Le singleton résout deux problèmes à la fois — ce qui, en soi, viole le principe de responsabilité unique.
Premièrement, garantir une instance unique. Pourquoi vouloir contrôler le nombre d'instances ? Le plus souvent pour contrôler l'accès à une ressource partagée — une base de données, un fichier. Le comportement attendu : si vous demandez un second objet, vous recevez celui que vous aviez déjà créé. Un constructeur classique ne peut pas faire cela, puisqu'il doit par définition renvoyer un objet neuf.
Deuxièmement, fournir un point d'accès global. Comme les variables globales, le singleton laisse accéder à un objet depuis n'importe où — mais il protège cette instance d'un écrasement par du code tiers, ce qu'une simple variable globale ne fait pas.
La solution
Toutes les implémentations partagent deux étapes : rendre le constructeur par défaut privé (pour empêcher tout new extérieur), et créer une méthode statique de création qui fait office de constructeur. Sous le capot, cette méthode appelle le constructeur privé, met l'objet en cache dans un champ statique, et renvoie l'objet en cache à tous les appels suivants (initialisation paresseuse).
// La connexion à la base, en singleton.
class BaseDeDonnees {
// Champ statique stockant l'unique instance.
private static instance: BaseDeDonnees | null = null;
// Constructeur privé : aucun `new` depuis l'extérieur.
private constructor() {
// Initialisation : connexion au serveur, etc.
}
// Le seul point d'accès. Crée l'instance au premier
// appel, puis renvoie toujours la même.
static obtenirInstance(): BaseDeDonnees {
if (BaseDeDonnees.instance === null) {
BaseDeDonnees.instance = new BaseDeDonnees();
}
return BaseDeDonnees.instance;
}
// La logique métier du singleton.
requete(sql: string): void {
console.log(`Exécution : ${sql}`);
}
}
const foo = BaseDeDonnees.obtenirInstance();
foo.requete("SELECT ...");
const bar = BaseDeDonnees.obtenirInstance();
// `bar` est exactement le même objet que `foo`.
console.log(foo === bar); // true L'analogie de Shvets : un gouvernement. Quelles que soient les personnes qui le composent, « le gouvernement de tel pays » est un point d'accès global unique qui désigne le groupe au pouvoir.
Quand l'utiliser
- Quand une classe doit avoir une seule instance accessible à tous les clients (un objet base de données partagé).
- Quand vous avez besoin d'un contrôle plus strict que les variables globales : rien, hormis la classe singleton elle-même, ne peut remplacer l'instance en cache.
Les critiques
Le singleton est sans doute le patron le plus controversé. Outre la violation du principe de responsabilité unique, il pose plusieurs problèmes sérieux.
Attention
Le singleton peut masquer une mauvaise conception, en autorisant des composants à trop se connaître les uns les autres. C'est de l'état global déguisé.
Il exige un traitement particulier en environnement multithread, sous peine de voir plusieurs threads créer l'instance plusieurs fois (d'où le verrouillage à double vérification).
Il complique les tests unitaires : beaucoup de frameworks de mock reposent sur l'héritage, or le constructeur est privé et les méthodes statiques ne se redéfinissent pas. Shvets le formule crûment : trouvez un moyen créatif de simuler le singleton, ou n'écrivez pas les tests, ou… n'utilisez pas le singleton.
Tableau récapitulatif
| Patron | Intention | Quand l'utiliser |
|---|---|---|
| Fabrique (Factory Method) | Déléguer aux sous-classes le choix de la classe concrète à instancier. | Types des objets inconnus à l'avance ; offrir un point d'extension dans un framework ; réutiliser des objets via un pool. |
| Fabrique abstraite (Abstract Factory) | Créer des familles d'objets liés et cohérents sans préciser leurs classes. | Plusieurs familles de produits assortis (styles, OS) à garder cohérentes et extensibles. |
| Monteur (Builder) | Construire pas à pas un objet complexe, plusieurs représentations possibles. | Éviter un constructeur télescopique ; produire diverses représentations par étapes ; bâtir des arbres. |
| Prototype (Prototype) | Cloner des objets existants sans coupler le code à leurs classes. | Copier des objets dont on ignore la classe ; remplacer des sous-classes qui ne diffèrent que par leur configuration. |
| Singleton (Singleton) | Garantir une instance unique avec un point d'accès global. | Ressource partagée unique (base, configuration) ; contrôle strict sur une « variable globale ». |
À retenir
- Les patrons de création remplacent les
newéparpillés par des points de création dédiés et remplaçables, qui découplent le code client des classes concrètes. - La fabrique délègue le choix de la classe aux sous-classes ; la fabrique abstraite généralise l'idée à des familles cohérentes de produits assortis.
- Le monteur construit pas à pas et n'expose jamais l'objet inachevé : c'est le remède au constructeur à dix paramètres ; le directeur y est optionnel.
- Le prototype clone via
cloner()sans dépendre des classes ni accéder à l'état interne de l'extérieur — utile face aux objets tiers et aux configurations multiples. - Le singleton garantit une instance unique, mais c'est de l'état global masqué : il viole le principe de responsabilité unique, complique les tests et la concurrence. À n'utiliser qu'à bon escient.
- Commencez simple (souvent la fabrique) et n'évoluez vers les patrons plus lourds que lorsque le besoin de flexibilité le justifie réellement, pour éviter la sur-ingénierie.