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.
- flyweight.ts
- Usage
/**
* 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`;
}
import { useState, useCallback, useMemo, useRef } from "react";
import { getSharedStyle, getCopiedStyle, computeStats, resetPool } from "@/lib/flyweight";
import { useContentEditable } from "./useContentEditable";
import { PRESETS } from "@/data/presets";
import type { EditorChar, StylePreset, EditorMode } from "@/lib/types";
export function useTextEditor() {
const [mode, setMode] = useState<EditorMode>("flyweight");
const [chars, setChars] = useState<EditorChar[]>([]);
const [activePreset, setActivePreset] = useState<StylePreset>(PRESETS[0]);
const presetRef = useRef(activePreset);
presetRef.current = activePreset;
const modeRef = useRef(mode);
modeRef.current = mode;
const { editorRef, handleBlur, restoreFocus, readInput, clearEditor } = useContentEditable(chars);
const stats = useMemo(() => computeStats(chars, mode === "flyweight"), [chars, mode]);
const noFlyweightStats = useMemo(() => (mode === "flyweight" ? computeStats(chars, false) : null), [chars, mode]);
const changePreset = useCallback((preset: StylePreset) => {
setActivePreset(preset);
requestAnimationFrame(() => restoreFocus());
}, [restoreFocus]);
const handleInput = useCallback(() => {
const { text, cursorPos } = readInput();
const { font, size, color } = presetRef.current;
const getStyle = modeRef.current === "flyweight" ? getSharedStyle : getCopiedStyle;
setChars((prev) => {
const oldLen = prev.length;
const newLen = text.length;
if (newLen > oldLen) {
const inserted = newLen - oldLen;
const insertPos = cursorPos - inserted;
const nc: EditorChar[] = [];
for (let i = 0; i < insertPos && i < oldLen; i++) nc.push({ ...prev[i], char: text[i] });
for (let i = insertPos; i < insertPos + inserted; i++) nc.push({ char: text[i], style: getStyle(font, size, color), index: i });
for (let i = insertPos; i < oldLen; i++) nc.push({ ...prev[i], char: text[i + inserted] });
return nc;
} else if (newLen < oldLen) {
const deleted = oldLen - newLen;
const nc: EditorChar[] = [];
for (let i = 0; i < cursorPos; i++) nc.push({ ...prev[i], char: text[i] });
for (let i = cursorPos + deleted; i < oldLen; i++) nc.push({ ...prev[i], char: text[i - deleted] });
return nc;
}
return prev.map((c, i) => ({ ...c, char: text[i] }));
});
}, [readInput]);
const handleReset = useCallback(() => { setChars([]); resetPool(); clearEditor(); }, [clearEditor]);
const handleModeSwitch = useCallback((newMode: EditorMode) => {
const getStyle = newMode === "flyweight" ? getSharedStyle : getCopiedStyle;
setChars((prev) => prev.map((c) => ({ ...c, style: getStyle(c.style.font, c.style.size, c.style.color) })));
setMode(newMode);
}, []);
return {
mode, activePreset, changePreset, stats, noFlyweightStats,
editorRef, handleInput, handleBlur, handleReset, handleModeSwitch,
};
}
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