Aller au contenu principal

Pattern Flyweight

Partagez l'état commun entre de nombreux objets similaires pour minimiser l'utilisation mémoire.


Le Problème

Vous construisez un éditeur de texte. Chaque caractère pourrait être un objet avec police, taille, couleur et position. Un million de caractères = un million d'objets = explosion mémoire.

L'approche naïve :

class Character {
constructor(
public char: string,
public font: string, // "Arial" repeated millions of times
public size: number, // 12 repeated millions of times
public color: string, // "#000000" repeated millions of times
public x: number,
public y: number
) {}
}

// Million characters = million copies of "Arial", 12, "#000000"

L'état intrinsèque (police, taille, couleur) est dupliqué. Gaspillage massif de mémoire.


La Solution

Partagez l'état intrinsèque. Ne stockez que l'état extrinsèque (position) par instance :

import { memoize } from "@pithos/core/arkhe";

// Flyweight factory - returns shared style objects
const getStyle = memoize((font: string, size: number, color: string) => ({
font,
size,
color,
}));

// Characters only store position + reference to shared style
interface Character {
char: string;
style: ReturnType<typeof getStyle>; // shared!
x: number;
y: number;
}

// Million characters, but only a few unique styles
const char1: Character = {
char: "H",
style: getStyle("Arial", 12, "#000000"), // shared
x: 0,
y: 0,
};

const char2: Character = {
char: "i",
style: getStyle("Arial", 12, "#000000"), // same reference — not a copy
x: 10,
y: 0,
};

char1.style === char2.style; // true — same object in memory

Les styles sont partagés via memoization. L'utilisation mémoire chute drastiquement.


Démo

Tapez du texte, changez les styles avec les presets, et observez les compteurs. Activez et désactivez le Flyweight — la barre mémoire montre la différence en temps réel.

Code
/**
* Flyweight pattern — style object pool via memoize.
*
* Same args = same object reference (shared intrinsic state).
* Without flyweight: every character gets its own copy.
*/

import { memoize } from "@pithos/core/eidos/flyweight/flyweight";
import type { CharStyle, EditorChar, EditorStats } from "./types";

/** Memoized style factory — same args = same object reference */
const memoizedCreateStyle = memoize(
(font: string, size: number, color: string): CharStyle => ({ font, size, color }),
(font: string, size: number, color: string) => `${font}|${size}|${color}`,
);

export function getSharedStyle(font: string, size: number, color: string): CharStyle {
return memoizedCreateStyle(font, size, color);
}

export function getCopiedStyle(font: string, size: number, color: string): CharStyle {
return { font, size, color };
}

export function resetPool(): void { memoizedCreateStyle.cache.clear(); }

const STYLE_OBJECT_SIZE = 64;

export function computeStats(chars: EditorChar[], useFlyweight: boolean): EditorStats {
const totalChars = chars.length;
if (totalChars === 0) return { totalChars: 0, styleObjects: 0, memoryBytes: 0, savedPercent: 0 };

if (useFlyweight) {
const refs = new Set(chars.map((c) => c.style));
const styleObjects = refs.size;
const memoryBytes = styleObjects * STYLE_OBJECT_SIZE;
const wouldBe = totalChars * STYLE_OBJECT_SIZE;
return { totalChars, styleObjects, memoryBytes, savedPercent: wouldBe > 0 ? Math.round(((wouldBe - memoryBytes) / wouldBe) * 100) : 0 };
}

return { totalChars, styleObjects: totalChars, memoryBytes: totalChars * STYLE_OBJECT_SIZE, savedPercent: 0 };
}

export function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
if (bytes < 1024) return `${bytes} B`;
return `${(bytes / 1024).toFixed(1)} KB`;
}
Result

Analogie

Un fichier de police. Votre ordinateur ne stocke pas une image "A" séparée pour chaque "A" à l'écran. Il stocke un seul glyphe "A" et le réutilise partout, en changeant juste la position et la taille.


Quand l'Utiliser

Si vous créez des milliers d'objets qui partagent la plupart de leurs données (entités de jeu, nœuds DOM, caractères de texte, tuiles de carte), extrayez la partie partagée dans une factory mémoïsée. Plus il y a de doublons, plus le gain est important.


Quand NE PAS l'Utiliser

Si chaque objet est unique sans état partagé, il n'y a rien à mutualiser. Flyweight ajoute un coût de lookup qui ne se justifie que quand de nombreux objets partagent les mêmes données intrinsèques.


En TypeScript Fonctionnel

Flyweight est essentiellement de la memoization. memoize d'Arkhe gère le pooling :

import { memoize } from "@pithos/core/arkhe";

// Any function that creates objects can become a flyweight factory
const getColor = memoize((r: number, g: number, b: number) => ({ r, g, b }));

// Same arguments = same object
getColor(255, 0, 0) === getColor(255, 0, 0); // true

API

  • memoize — Mettre en cache les résultats de fonctions, créant de fait un object pool