#11 / 23
S構造DIFF
フライウェイト
Flyweight
森に木を100万本生やしたいけど、そんなメモリは無い。── じゃあ『木の種』は1個だけ、座標だけ毎回渡そう。
Intent · 目的
オブジェクトの状態を『みんなで共有できる部分(intrinsic)』と『1つ1つ違う部分(extrinsic)』に分け、共有部分を1個のインスタンスで使い回すことでメモリを節約する。
Motivation · 動機
ゲームに森を作りたい。木が100万本ある。1本ごとに「テクスチャ・モデル・色設定・物理プロパティ」を持たせると、メモリが爆発する。でも実は『木の種類情報』は数十種類しか無く、変わるのは『どこに立っているか』だけ。なら『木の種類オブジェクト』は数十個だけ作って、座標は呼び出すたびに引数で渡せばいい。文字フォントのグリフ、ゲームの弾丸、絵文字、UIアイコン ── 大量に登場する似たもの達を、設計の力で薄く軽く描く呪文がフライウェイト。
適用場面
- 1アプリ内に同種のオブジェクトを大量に抱える(パーティクル、文字、ゲーム内の樹木・敵)。
- 2状態を内的(共有可)と外的(文脈依存)にスパッと切り分けられる場合。
- 3メモリ・GC コストがボトルネックになっている場合。
概念図
サンプルコード
class TreeKind {
constructor(public name: string, public texture: string) {}
render(x: number, y: number){ console.log(`${this.name}@${x},${y}`); }
}
class TreeKindFactory {
private pool = new Map<string, TreeKind>();
get(name: string, texture: string): TreeKind {
let k = this.pool.get(name);
if (!k) { k = new TreeKind(name, texture); this.pool.set(name, k); }
return k;
}
}
const factory = new TreeKindFactory();
for (let i = 0; i < 1_000_000; i++) {
const k = factory.get("oak", "oak.png");
if (i % 250000 === 0) k.render(i, i);
}Pros · メリット
- +メモリ使用量が劇的に減る。100万本の木が、数十個の種オブジェクト + 座標配列で済む。
- +GC 圧力が下がり、ガクッと止まる現象が減る。
- +共有部分が中央に集まるので、変更点も中央1か所で済む。
Cons · デメリット
- −状態の分割設計が肝で、内的/外的を取り違えると即バグ。誤って『座標』を共有側に置いたら、全ての木が同じ場所に重なる。
- −コードが2層構造になり、認知負荷が増える。
- −呼び出し側が外的状態を毎回渡すコストが付き纏う。
関連パターン
⚔ ready to test
理解度を確かめる時間だ
3 問のクイズで、 このパターンを身体に染み込ませよう。