La lumière froide du scanner balayait le document original. Une facture type, avec son en-tête complexe, ses mentions légales, sa mise en page sophistiquée. Créer ce modèle avait coûté cher en ressources, en requêtes à la base, en calcul de mise en forme. Maintenant, le client en voulait dix copies, avec juste le numéro et le nom du client qui changeaient. Relancer tout le processus de création pour chaque copie ? C’était un gaspillage. Un crime. Il fallait une méthode plus rapide, plus économique : le clonage. Il fallait un Prototype.
Clonage vs instanciation
La différence est fondamentale, une question de coût.
- Instanciation (
new Classe()) : Exécuter un constructeur, qui peut impliquer des opérations lourdes (chargement depuis une base de données, calculs complexes, appels réseau, parsing de fichiers). C’est repartir de zéro. - Clonage : Copier l’état d’un objet existant, un prototype, et éventuellement y apporter quelques modifications mineures. C’est dupliquer un état déjà calculé.
Le pattern Prototype permet de créer de nouveaux objets en copiant un prototype existant, plutôt qu’en passant par une création coûteuse. L’idée est de déléguer le processus de clonage à l’objet lui-même, via une méthode comme clone().
Implémentation de base
// L'interface Prototype (contrat de clonage)
interface Prototype<T> {
clone(): T;
}
// Un produit concret pouvant être cloné
class FactureComplexe implements Prototype<FactureComplexe> {
constructor(
public numero: string,
public client: string,
public montantTotal: number,
public lignes: string[], // Tableau d'objets complexes
public enTetePersonalise: EnTete // Objet imbriqué
) {
// Simulation d'une initialisation TRÈS coûteuse
console.log(`⚠️ Coût élevé : Création de la facture ${numero} (chargement templates, calculs...)`);
}
// La méthode clone est l'essence du pattern.
clone(): FactureComplexe {
// ICI se joue tout l'enjeu : copie superficielle ou profonde ?
// Version naïve (et DANGEREUSE) : copie superficielle.
return Object.create(this);
// Ou bien, en utilisant un constructeur de copie (plus sûr) :
// return new FactureComplexe(this.numero, this.client, ...);
}
}
Copie superficielle / profonde
C’est le point de bascule entre un pattern utile et un piège à bugs. Le diable est dans les détails… des références.
Copie superficielle (Shallow Copy) : Seule l’instance de l’objet principal est dupliquée. Les objets imbriqués (tableaux, objets propriétés) sont partagés entre l’original et le clone. Modifier le tableau du clone modifie le tableau de l’original. C’est dangereux.
Copie profonde (Deep Copy) : L’objet principal ET tous les objets qu’il référence sont récursivement dupliqués. Le clone est totalement indépendant. C’est ce qu’on veut souvent, mais c’est plus coûteux et parfois complexe à mettre en œuvre (cycles de références, fonctions, etc.).
class EnTete {
constructor(public logo: string, public mentionsLegales: string) {}
}
class Facture implements Prototype<Facture> {
constructor(
public numero: string,
public lignes: string[], // Référence à un tableau
public enTete: EnTete // Référence à un objet
) {}
// 1. COPIE SUPERFICIELLE DANGEREUSE
cloneSuperficiel(): Facture {
return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
// Ou simplement : return { ...this }; // Spread operator (superficiel)
}
// 2. COPIE PROFONDE (MANUELLE - pour petits objets)
clone(): Facture {
// On duplique récursivement les structures imbriquées.
return new Facture(
this.numero,
[...this.lignes], // Copie du tableau
new EnTete(this.enTete.logo, this.enTete.mentionsLegales) // Copie de l'objet
);
}
// 3. COPIE PROFONDE GÉNÉRIQUE (JSON - avec limites)
cloneViaJSON(): Facture {
// ATTENTION : Perd les méthodes, les fonctions, les dates mal formatées, les undefined.
// Ne gère PAS les références circulaires.
return JSON.parse(JSON.stringify(this));
}
}
// --- SCÉNARIO CATASTROPHE (Copie superficielle) ---
const original = new Facture('FAC-001', ['Ligne1', 'Ligne2'], new EnTete('logo.png', 'Mentions A'));
const clone = original.cloneSuperficiel();
clone.numero = 'FAC-002'; // OK, propriété primitive.
clone.lignes.push('Ligne3 ajoutée au clone'); // CATASTROPHE : Affecte aussi 'original' !
clone.enTete.mentionsLegales = 'MENTIONS MODIFIÉES'; // CATASTROPHE : Affecte aussi l'original !
console.log(original.lignes); // ['Ligne1', 'Ligne2', 'Ligne3 ajoutée au clone'] → INATTENDU !
console.log(original.enTete.mentionsLegales); // 'MENTIONS MODIFIÉES' → INATTENDU !
// --- SCÉNARIO SAIN (Copie profonde manuelle) ---
const original2 = new Facture('FAC-003', ['LigneA'], new EnTete('logo2.png', 'Mentions B'));
const clone2 = original2.clone(); // Utilise la méthode clone profonde
clone2.lignes.push('LigneB ajoutée au clone');
clone2.enTete.mentionsLegales = 'Nouvelles mentions';
console.log(original2.lignes); // ['LigneA'] → Inchangé. OK.
console.log(original2.enTete.mentionsLegales); // 'Mentions B' → Inchangé. OK.
Cas limites et pièges
- Références circulaires : Si l’objet A référence B, et B référence A, une copie profonde naïve va entrer dans une boucle infinie ou planter. Il faut une logique de suivi des objets déjà copiés (un
Mappour mémoriser les correspondances original -> clone). - Fonctions et méthodes : Une copie profonde générique (comme le hack JSON) perd les fonctions attachées à l’objet. La copie manuelle ou l’utilisation de librairies spécialisées (
lodash.cloneDeep) est nécessaire. - Objets spéciaux :
Date,RegExp,Map,Setne sont pas clonés correctement par des méthodes simples. Ils nécessitent un traitement spécifique. - Prototype et héritage : Si votre objet a un prototype complexe (héritage), une copie superficielle avec
Object.createpeut le préserver, mais une copie profonde manuelle peut nécessiter de connaître la chaîne de prototypes. - Performance : Une copie profonde complète peut être aussi coûteuse que la création originale si l’objet est très grand et imbriqué. Il faut évaluer le gain. Parfois, une copie partielle (seulement les parties qui changent) est préférable.
- Le registre de prototypes : Une variation avancée du pattern introduit un registre (un
Mapou un objet) qui stocke des prototypes nommés. Au lieu de cloner un objet qu’on a déjà en main, on demande au registre de nous donner une copie d’un prototype prédéfini (ex:prototypeRegistry.get('factureTypeA').clone()).
Quand l’utiliser ?
- Quand la création d’un objet est plus coûteuse que sa copie.
- Quand votre système doit produire de nouvelles instances qui ne diffèrent que par quelques attributs d’un état initial complexe (ex: documents, configurations, personnages de jeu avec des stats de base).
- Quand vous voulez cacher la complexité de la création au client, qui a juste besoin de dire « donne-moi un truc comme celui-là ».
- Quand les classes à instancier sont déterminées à l’exécution (comme pour la Factory), et que vous avez déjà une instance « modèle » sous la main.
J’éteignis le scanner. Le prototype était un pattern de l’ombre. Moins glorieux que le Builder, moins structurant que l’Abstract Factory. Mais dans un monde où la performance et la ressource comptent, savoir cloner efficacement était une compétence de survivant.
C’était le dernier pattern de la famille Créationnelle. On avait couvert le spectre : du contrôle absolu (Singleton) à la délégation polymorphique (Factory), de la construction complexe (Builder) à la duplication économique (Prototype).
Ils répondaient tous à la même question fondamentale : comment naissent les objets dans un système flexible et maintenable ?
La prochaine famille, la Structurelle, aborderait une autre question tout aussi cruciale : comment ces objets s’assemblent-ils pour former des structures plus grandes sans s’étouffer mutuellement ?
Le premier de cette série serait l’Adapter. L’art de faire coexister des incompatibles. Une compétence essentielle dans le monde réel, où tout n’est jamais conçu pour s’emboîter parfaitement.