Polar Code 🎭

Command Palette

Search for a command to run...

18
Pièce N°18

CHAPITRE 17 – FLYWEIGHT

La pièce sentait le moisi et le papier. Des dossiers empilés jusqu’au plafond. Des milliers de rapports d’enquête. Chaque page partageait le même en-tête, le même logo, les mêmes mentions légales. Le système de rendu original durait une éternité et consommait toute la mémoire. Il créait un nouvel objet complet pour chaque page. La répétition tuait la performance. Il fallait extraire l’invariant, le partager. Il fallait un Flyweight.

Partage d’état

Le Flyweight repose sur une distinction fondamentale :

  • État intrinsèque : Les données partagées, immuables, indépendantes du contexte. C’est l’essence de l’objet, ce qui le rend semblable à d’autres. (Ex: le glyphe d’un caractère, la texture d’un arbre, l’en-tête d’un document).
  • État extrinsèque : Les données uniques, variables, dépendantes du contexte. C’est ce qui rend chaque instance différente. (Ex: la position du caractère, les coordonnées de l’arbre, le numéro de page).

Le pattern consiste à :

  1. Extraire l’état intrinsèque dans un objet Flyweight unique par combinaison de valeurs.
  2. Stocker cet objet Flyweight dans une fabrique qui garantit le partage.
  3. Faire en sorte que les objets contextuels (appelés contexte) contiennent l’état extrinsèque.
  4. Lors d’une opération, passer l’état extrinsèque en paramètre au Flyweight partagé.

Optimisation mémoire

L’économie est radicale. Au lieu de N objets complets (intrinsèque + extrinsèque), on a :

  • K objets Flyweight (un par combinaison unique d’état intrinsèque, où K est souvent bien plus petit que N).
  • N objets légers de contexte (ou simplement des structures de données) qui contiennent l’état extrinsèque et une référence au Flyweight.

Le gain est énorme quand l’état intrinsèque est lourd (textures, polices, modèles 3D) et qu’il y a de nombreuses instances.

Cas massifs

L’exemple canonique : un traitement de texte ou un jeu vidéo.

// --- ÉTAT INTRINSÈQUE (Partagé, immuable) ---
// La classe Flyweight. Coûteuse à créer, mais partagée.
class GlypheFlyweight {
    // Données intrinsèques (lourdes)
    constructor(
        private readonly caractere: string,
        private readonly police: string,
        private readonly taille: number,
        private readonly couleurIntrinseque: string // Couleur par défaut
    ) {
        console.log(`🔄 Création COÛTEUSE du glyphe pour '${caractere}' (${police}, ${taille}pt)`);
    }

    // L'opération utilise l'état intrinsèque (this) et l'état extrinsèque (passé en paramètre)
    afficher(positionX: number, positionY: number, couleurExtrinseque?: string): void {
        const couleurFinale = couleurExtrinseque || this.couleurIntrinseque;
        console.log(`Dessiner '${this.caractere}' en ${this.police} ${this.taille}pt, ` +
                    `couleur ${couleurFinale} à la position (${positionX}, ${positionY})`);
    }
}

// --- FABRIQUE DE FLYWEIGHTS (Garantit le partage) ---
class FabriqueGlyphes {
    private flyweights: { [cle: string]: GlypheFlyweight } = {};

    // Clé basée sur l'état intrinsèque
    private getCle(caractere: string, police: string, taille: number, couleurIntrinseque: string): string {
        return `${caractere}-${police}-${taille}-${couleurIntrinseque}`;
    }

    obtenirGlyphe(caractere: string, police: string, taille: number, couleurIntrinseque: string = 'noir'): GlypheFlyweight {
        const cle = this.getCle(caractere, police, taille, couleurIntrinseque);
        if (!this.flyweights[cle]) {
            this.flyweights[cle] = new GlypheFlyweight(caractere, police, taille, couleurIntrinseque);
        } else {
            console.log(`✅ Réutilisation du glyphe existant pour '${caractere}'`);
        }
        return this.flyweights[cle];
    }

    compterFlyweights(): number {
        return Object.keys(this.flyweights).length;
    }
}

// --- ÉTAT EXTRINSÈQUE (Contenu dans le contexte) ---
class CaractereContexte {
    // Référence au Flyweight partagé
    constructor(
        private flyweight: GlypheFlyweight,
        // État extrinsèque, unique à cette instance
        private positionX: number,
        private positionY: number,
        private couleurSupplementaire?: string // Peut surcharger la couleur intrinsèque
    ) {}

    afficher(): void {
        // On délègue au Flyweight en lui passant l'état extrinsèque
        this.flyweight.afficher(this.positionX, this.positionY, this.couleurSupplementaire);
    }
}

// --- CLIENT (Crée un "document" massif) ---
function simulerDocumentMassif() {
    const fabrique = new FabriqueGlyphes();
    const document: CaractereContexte[] = [];
    const texte = "LE POLAR NOIR"; // Imaginez un roman entier ici
    const police = "Courier New";
    const taille = 12;

    console.log("=== Construction du document (optimisé Flyweight) ===");
    for (let i = 0; i < texte.length; i++) {
        const caractere = texte[i];
        // 1. On obtient (ou récupère) le Flyweight partagé pour ce caractère/style.
        const glyphePartage = fabrique.obtenirGlyphe(caractere, police, taille);
        // 2. On crée un contexte léger avec la position unique.
        const contexte = new CaractereContexte(glyphePartage, i * 10, 0);
        document.push(contexte);
    }

    // Ajoutons quelques caractères spéciaux avec une couleur différente (intrinsèque)
    const glypheSpecial = fabrique.obtenirGlyphe('!', police, taille, 'rouge');
    document.push(new CaractereContexte(glypheSpecial, texte.length * 10, 0));

    console.log(`\nDocument de ${document.length} caractères créé.`);
    console.log(`Nombre de Flyweights UNIQUES créés : ${fabrique.compterFlyweights()} (au lieu de ${document.length})`);
    console.log(`Économie mémoire : ~${Math.round((1 - fabrique.compterFlyweights() / document.length) * 100)}%`);

    console.log("\n=== Affichage du document ===");
    document.forEach(c => c.afficher());
}

simulerDocumentMassif();

Cas réels

  1. Jeux vidéo : Forêts (arbres), armées (soldats), particules. La géométrie et la texture sont partagées (Flyweight), seules la position et l’orientation sont individuelles.
  2. UI / CSS : Dans un navigateur, les styles CSS calculés pour des éléments identiques (même classe) sont partagés (conceptuellement un Flyweight).
  3. Traitement de données : Quand on traite des millions d’enregistrements avec des catégories répétitives, l’objet « Catégorie » est un candidat parfait pour le Flyweight.
  4. Réseau : Dans des protocoles, les en-têtes de paquets souvent identiques pourraient être représentés par des Flyweights.

Mises en garde

  1. Complexité accrue : Le code devient plus complexe. Il faut gérer deux types d’objets (Flyweight et Contexte) et une fabrique.
  2. Trouver l’état extrinsèque : Il n’est pas toujours évident de séparer proprement les deux états. Si trop de données deviennent extrinsèques, l’overhead de les passer en paramètre annule les gains.
  3. Accès concurrentiel : Les Flyweights sont partagés, donc immuables. Si un Flyweight doit être modifié, il faut en créer un nouveau (et la fabrique doit le gérer correctement, potentiellement avec de l’éviction).
  4. Prématuré : Comme toute optimisation, il ne faut l’appliquer que quand le problème de mémoire est avéré et identifié par un profiler.

Je regardai les piles de dossiers. Le Flyweight était le pattern de l’austérité. Il ne brillait pas par son élégance conceptuelle comme le Composite ou le Decorator. Il brillait par son efficacité brute, son refus du gaspillage.

C’était le dernier pattern Structurel. La famille était complète.

La prochaine étape serait le passage aux Patterns Comportementaux. On quittait la question « comment les objets sont-ils structurés ? » pour « comment interagissent-ils et se répartissent-ils les responsabilités ? ».

Le premier de cette série serait Chain of Responsibility. La chaîne de transmission, où une requête erre jusqu’à trouver un gestionnaire qui veuille bien la prendre. Une métaphore parfaite pour un polar.