DPDesign Pattern Quest
#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 問のクイズで、 このパターンを身体に染み込ませよう。

挑戦する →