// ───────────────────────────────────────────────────────────────────────────── // FLYWEIGHT [Structural] // ───────────────────────────────────────────────────────────────────────────── // INTENT : Use sharing to support large numbers of fine-grained objects efficiently. // PROBLEM : A document has 1M characters, each with font/size/weight. One style object per character burns // memory — yet only ~20 distinct styles exist. // SAY : Participants: Flyweight (shared, immutable INTRINSIC state), ConcreteFlyweight, FlyweightFactory // (caches & hands out instances by key), Client (supplies EXTRINSIC state — position, char — at // call time). Pitch: "intern the shared part; pass the varying part in." // SEEN IN : `Symbol.for(key)` global registry & V8 string interning; cached `Intl.NumberFormat` / // `Intl.DateTimeFormat` instances (construction is expensive — share per locale+options); // Immutable.js / Immer structural sharing; browser glyph/font caches; game sprite atlases; // ORM identity maps (one object per row id) — flyweight-ish. // VS : Singleton = exactly ONE instance; Flyweight = one instance PER KEY, many keys. Prototype copies // objects; Flyweight deliberately avoids copies. Object pool recycles MUTABLE objects; // flyweights are immutable. // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── Classic shape (classes, so the participant names are visible) class TextStyle { // ConcreteFlyweight — intrinsic state only, frozen constructor(font, size, bold) { Object.assign(this, { font, size, bold }); Object.freeze(this); } render(char, x, y) { // operation receives EXTRINSIC state return `${char}@${x},${y}:${this.font}/${this.size}${this.bold ? 'b' : ''}`; } } class TextStyleFactory { // FlyweightFactory — one instance per key #cache = new Map(); get(font, size, bold = false) { const key = `${font}|${size}|${bold}`; if (!this.#cache.has(key)) this.#cache.set(key, new TextStyle(font, size, bold)); return this.#cache.get(key); } get count() { return this.#cache.size; } } class Document { // Client — stores (char, styleRef, position) constructor(factory) { this.factory = factory; this.glyphs = []; } write(text, { font = 'Inter', size = 12, bold = false } = {}) { const style = this.factory.get(font, size, bold); for (const char of text) this.glyphs.push({ char, style, x: this.glyphs.length, y: 0 }); return this; } render() { return this.glyphs.map((g) => g.style.render(g.char, g.x, g.y)); } } const factory = new TextStyleFactory(); const doc = new Document(factory) .write('Hello ', { size: 12 }) .write('World', { size: 12, bold: true }) .write('!!!!!!!!!!', { size: 12 }); assert.equal(doc.glyphs.length, 21); assert.equal(factory.count, 2); // 21 glyphs, 2 style objects assert.equal(doc.glyphs[0].style, doc.glyphs[20].style); // identity shared across the document assert.notEqual(doc.glyphs[0].style, doc.glyphs[6].style); // different intrinsic state → different object assert.equal(factory.get('Inter', 12), factory.get('Inter', 12, false)); assert.throws(() => { factory.get('Inter', 12).size = 99; }, TypeError); // immutable (strict mode throws) assert.equal(doc.render()[6], 'W@6,0:Inter/12b'); // ── Idiomatic JS shape: memoized factory over a Map — Intl formatters are the textbook real-world case const intern = (make) => { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); if (!cache.has(key)) cache.set(key, make(...args)); return cache.get(key); }; }; const getFormatter = intern((locale, currency) => new Intl.NumberFormat(locale, { style: 'currency', currency })); const a = getFormatter('en-US', 'USD'); const b = getFormatter('en-US', 'USD'); const c = getFormatter('de-DE', 'EUR'); assert.equal(a, b); // shared instance (construction skipped) assert.notEqual(a, c); assert.equal(a.format(1234.5), '$1,234.50'); assert.equal(c.format(1234.5).replace(/\u00a0/g, ' '), '1.234,50 €'); // Symbol.for is a built-in flyweight registry: same key → same symbol, across modules and realms assert.equal(Symbol.for('app.user'), Symbol.for('app.user')); assert.notEqual(Symbol('app.user'), Symbol('app.user')); // Extrinsic vs intrinsic in one line: 10k map markers, 3 icon objects const icon = intern((kind) => Object.freeze({ kind, svg: `` })); const markers = Array.from({ length: 10_000 }, (_, i) => ({ lat: i, lng: -i, // extrinsic — per marker icon: icon(['cafe', 'atm', 'park'][i % 3]), // intrinsic — shared })); assert.equal(markers.length, 10_000); assert.equal(new Set(markers.map((m) => m.icon)).size, 3); assert.equal(markers[0].icon, markers[3].icon); console.log('ok: design-patterns/structural/flyweight.js');