Concept #036
Design PatternsLe Design Pattern Prototype
- design pattern
- créationnel
- clonage
Le Design Pattern Prototype en une phrase
Prototype est un patron de conception créationnel qui permet de créer de nouveaux objets en clonant une instance existante, plutôt que de les instancier de zéro via un constructeur. L'objet cloné sert de modèle (prototype) que l'on peut ensuite personnaliser.
Fatigué de recréer des objets complexes de zéro juste pour une petite variante ? Le Prototype est la solution. Au lieu d'une nouvelle instanciation coûteuse, on clone un modèle existant. C'est plus rapide, plus efficace et idéal pour générer des familles d'objets similaires.
Le problème : l'instanciation coûteuse
Certains objets sont coûteux à créer. Leur construction implique des requêtes réseau, des calculs lourds, la lecture de fichiers de configuration, ou l'assemblage de nombreux composants imbriqués.
Imaginez un système de génération de documents :
class DocumentTemplate {
private styles: Map<string, CSSProperties>;
private fonts: Font[];
private header: HeaderConfig;
private footer: FooterConfig;
private watermark: WatermarkConfig;
private metadata: DocumentMetadata;
constructor(templatePath: string) {
// Lecture et parsing d'un fichier de template (lent)
const raw = fs.readFileSync(templatePath, "utf-8");
const parsed = parseTemplate(raw);
// Chargement des polices (accès disque)
this.fonts = parsed.fontPaths.map((p) => loadFont(p));
// Compilation des styles (calculs)
this.styles = compileStyles(parsed.styleSheet);
// Configuration des composants
this.header = buildHeader(parsed.header);
this.footer = buildFooter(parsed.footer);
this.watermark = buildWatermark(parsed.watermark);
this.metadata = parsed.metadata;
}
}Chaque instanciation lit le fichier, charge les polices et compile les styles. Si vous devez générer 100 documents à partir du même template avec des variantes mineures (nom du client, date, contenu), vous répétez tout ce travail 100 fois.
La solution : cloner le prototype
Au lieu de reconstruire, on clone un objet déjà construit et on modifie uniquement ce qui diffère.
L'interface Prototype
interface Prototype<T> {
clone(): T;
}Le document clonable
class DocumentTemplate implements Prototype<DocumentTemplate> {
private styles: Map<string, CSSProperties>;
private fonts: Font[];
private header: HeaderConfig;
private footer: FooterConfig;
private watermark: WatermarkConfig;
private metadata: DocumentMetadata;
private content: string;
constructor(templatePath: string) {
// ... construction coûteuse (identique à avant)
}
// Clone profond : copie indépendante
clone(): DocumentTemplate {
const cloned = Object.create(DocumentTemplate.prototype);
// Copie profonde des structures mutables
cloned.styles = new Map(this.styles);
cloned.fonts = [...this.fonts];
cloned.header = { ...this.header };
cloned.footer = { ...this.footer };
cloned.watermark = { ...this.watermark };
cloned.metadata = { ...this.metadata };
cloned.content = this.content;
return cloned;
}
// Méthodes pour personnaliser le clone
setContent(content: string): this {
this.content = content;
return this;
}
setMetadata(metadata: Partial<DocumentMetadata>): this {
this.metadata = { ...this.metadata, ...metadata };
return this;
}
}Utilisation
// Construction coûteuse : une seule fois
const invoicePrototype = new DocumentTemplate("templates/invoice.xml");
// Génération de 100 factures par clonage (rapide)
const invoices = customers.map((customer) => {
return invoicePrototype
.clone()
.setContent(generateInvoiceContent(customer))
.setMetadata({
title: `Facture ${customer.name}`,
date: new Date().toISOString(),
});
});Le template est construit une seule fois. Chaque facture est un clone personnalisé. Le gain de performance est proportionnel au coût de la construction initiale et au nombre de clones.
Clone superficiel vs clone profond
La distinction est cruciale et source de bugs subtils.
Clone superficiel (shallow clone) : copie les propriétés de premier niveau. Les objets et tableaux imbriqués restent partagés (même référence).
const original = {
name: "Alice",
address: { city: "Paris", zip: "75001" },
};
// Clone superficiel
const shallow = { ...original };
shallow.name = "Bob"; // OK, indépendant
shallow.address.city = "Lyon"; // ATTENTION : modifie aussi original !
console.log(original.address.city); // "Lyon" — l'original est corrompuClone profond (deep clone) : copie récursivement tous les niveaux d'imbrication. Les objets sont totalement indépendants.
// Méthode native (JavaScript moderne)
const deep = structuredClone(original);
deep.address.city = "Lyon";
console.log(original.address.city); // "Paris" — l'original est intactEn TypeScript/JavaScript, structuredClone() est la méthode recommandée pour le clonage profond. Elle gère les objets imbriqués, les tableaux, les Maps, les Sets, les Dates, et les références circulaires.
class GameCharacter implements Prototype<GameCharacter> {
constructor(
public name: string,
public stats: { health: number; attack: number; defense: number },
public inventory: Item[],
public skills: Map<string, Skill>,
) {}
clone(): GameCharacter {
// structuredClone gère la copie profonde de toute la structure
return structuredClone(this);
}
}Le registre de prototypes
Quand vous avez plusieurs prototypes à gérer, un registre centralise leur stockage et leur accès :
class PrototypeRegistry {
private prototypes = new Map<string, Prototype<unknown>>();
register(key: string, prototype: Prototype<unknown>): void {
this.prototypes.set(key, prototype);
}
get<T>(key: string): T {
const prototype = this.prototypes.get(key);
if (!prototype) {
throw new Error(`Prototype "${key}" non trouvé`);
}
return (prototype as Prototype<T>).clone();
}
}
// Configuration initiale
const registry = new PrototypeRegistry();
registry.register("invoice", new DocumentTemplate("templates/invoice.xml"));
registry.register("report", new DocumentTemplate("templates/report.xml"));
registry.register("contract", new DocumentTemplate("templates/contract.xml"));
// Utilisation : clonage par clé
const myInvoice = registry.get<DocumentTemplate>("invoice");
const myReport = registry.get<DocumentTemplate>("report");Le registre évite de garder des références directes aux prototypes dans tout le code. Il centralise la gestion des modèles et facilite l'ajout de nouveaux types.
Cas d'usage réels
Configuration d'environnements
const baseConfig: ServerConfig = {
port: 3000,
database: {
host: "localhost",
port: 5432,
pool: { min: 5, max: 20 },
},
logging: { level: "info", format: "json" },
cache: { ttl: 3600, maxSize: 1000 },
};
// Les environnements héritent du prototype de base
const productionConfig = {
...structuredClone(baseConfig),
database: {
...structuredClone(baseConfig.database),
host: "prod-db.internal",
pool: { min: 20, max: 100 },
},
logging: { level: "warn", format: "json" },
};
const stagingConfig = {
...structuredClone(baseConfig),
database: {
...structuredClone(baseConfig.database),
host: "staging-db.internal",
},
logging: { level: "debug", format: "text" },
};Tests : fixtures et objets de test
// Prototype de base pour les tests
const baseUser: User = {
id: "test-id",
name: "Test User",
email: "test@example.com",
role: "viewer",
preferences: { theme: "light", language: "fr", notifications: true },
createdAt: new Date("2025-01-01"),
};
// Chaque test clone et personnalise
it("devrait refuser l'accès aux viewers", () => {
const viewer = structuredClone(baseUser);
expect(canAccessAdmin(viewer)).toBe(false);
});
it("devrait autoriser l'accès aux admins", () => {
const admin = { ...structuredClone(baseUser), role: "admin" };
expect(canAccessAdmin(admin)).toBe(true);
});Prototype vs Factory : quand choisir ?
| Prototype | Factory | |
|---|---|---|
| Mécanisme | Clone un objet existant | Construit un objet de zéro |
| Quand l'utiliser | La construction est coûteuse et les variantes sont mineures | La construction est logique et les objets diffèrent significativement |
| Connaissance requise | Un prototype existant | Les paramètres de construction |
| Flexibilité | Cloner n'importe quel objet compatible | Types prédéfinis dans la factory |
Si vos objets sont des variations d'un même modèle, le Prototype est naturel. Si chaque objet est fondamentalement différent dans sa construction, la Factory est plus adaptée.
Résumé
Le pattern Prototype élimine le coût de la reconstruction répétitive. En clonant un modèle existant et en ne modifiant que les différences, on gagne en performance et en simplicité. La clé est de maîtriser la distinction entre clone superficiel et clone profond pour éviter les partages de références involontaires. Avec structuredClone() en JavaScript moderne, le clonage profond est devenu une opération native et fiable.