DPDesign Pattern Quest
#08 / 23
S構造DIFF

コンポジット

Composite

ファイルもフォルダも、まとめて『中身のサイズが分かるもの』として扱う。個と集合の境界を消す。

Intent · 目的

オブジェクトを木構造に組み立て、葉(個別)とコンテナ(合成)を区別せず同じインタフェースで扱えるようにする。再帰的な構造を、利用側の `if フォルダなら…` を撲滅して、自然に書けるようにするパターン。

Motivation · 動機

「フォルダのサイズを表示してください」── ナイーブに書くと、ファイルなら `size`、フォルダなら子要素を全部足す、と分岐したくなる。さらに『ショートカット』も出てきたら if が増える。Composite は『葉も合成も、`size()` を呼べば正しい数字を返す』という1つの約束で統一する。フォルダは中の子に再帰的に聞くだけで仕事完了。組織図、UI部品(パネルの中にボタンの中に…)、HTML DOM ── 木構造のあるところに Composite あり。

適用場面

  • 1対象が再帰的な木構造で表現される(ファイルシステム、UIツリー、組織図、AST)。
  • 2個別と集合を区別せず一様に操作したい場合(『全部の合計』『全部に表示』など)。
  • 3操作を構造全体に再帰的に適用したい場合。

概念図

サンプルコード

interface FsNode {
  size(): number;
  name: string;
}

class FileNode implements FsNode {
  constructor(public name: string, private bytes: number) {}
  size(){ return this.bytes; }
}

class Directory implements FsNode {
  private children: FsNode[] = [];
  constructor(public name: string) {}
  add(node: FsNode){ this.children.push(node); return this; }
  size(){ return this.children.reduce((acc, c) => acc + c.size(), 0); }
}

const root = new Directory("/")
  .add(new FileNode("a.txt", 100))
  .add(new Directory("sub").add(new FileNode("b.txt", 50)));
console.log(root.size()); // 150
Pros · メリット
  • クライアントが葉と合成を区別しなくて済む。`if フォルダなら` の地獄から解放。
  • 再帰構造を自然に表現でき、操作も再帰呼出し1行で書ける。
  • 新しい部品を追加しても、利用側コードは無変更。
Cons · デメリット
  • 共通I/Fに `add/remove` を含めると、葉でも呼べてしまい型安全が緩くなる。
  • 全要素を共通I/Fに揃えるため、I/Fが太りやすい(『全員に必要じゃないメソッド』が混ざる)。
  • 深い木の操作はパフォーマンスとデバッグの両方で気を遣う。

関連パターン

⚔ ready to test

理解度を確かめる時間だ

3 問のクイズで、 このパターンを身体に染み込ませよう。

挑戦する →