Aller au contenu principal

Concept #036

Design Patterns

Le Design Pattern Prototype

#0366 min de lecture
  • design pattern
  • créationnel
  • clonage
Prototype : cloner plutôt que reconstruire

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 corrompu

Clone 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 intact

En 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 ?

PrototypeFactory
MécanismeClone un objet existantConstruit un objet de zéro
Quand l'utiliserLa construction est coûteuse et les variantes sont mineuresLa construction est logique et les objets diffèrent significativement
Connaissance requiseUn prototype existantLes paramètres de construction
FlexibilitéCloner n'importe quel objet compatibleTypes 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.