DP-08: Composite パターン
単一のオブジェクト(葉)と複数のオブジェクトをまとめた複合オブジェクト(枝)を 同じインターフェースで統一的に扱うパターンです。 ファイルシステム・UIコンポーネント・組織図など、再帰的なツリー構造をシンプルに表現できます。
Composite パターンとは
「ファイルのサイズを取得する」と「ディレクトリのサイズを取得する」は、 利用者の視点では同じ操作です。しかし実装が異なるため、 型を区別してコードを書くと instanceof チェックが増えて複雑になります。 Composite パターンでは「単一オブジェクト(葉)」と「複合オブジェクト(枝)」を共通の型(Component)で統一し、 利用側が型を意識しなくて済むようにします。
3つの登場人物
- Component(共通インターフェース): 葉と枝が共有する抽象型。今回は
FileSystemNode - Leaf(葉ノード): 子を持たない末端のオブジェクト。今回は
FileNode - Composite(複合ノード): 子ノードのリストを持つオブジェクト。今回は
DirectoryNode
ファイルシステム以外にも、以下のような場面で活用されます。
代表的な応用例
- UIコンポーネント: ボタン(葉)もパネル(枝)も
Componentとして描画処理を統一する - 組織図: 一般社員(葉)も部署(枝)も同じ
Employeeインターフェースで給与合計を計算する - 数式ツリー: 数値(葉)も演算子(枝)も
Expressionとして評価できる
ツリーが深くなると再帰呼び出しが増えます。非常に深い階層(数千レベル以上)では スタックオーバーフローが発生する可能性があります。 実務で深いツリーを扱う場合は再帰を反復処理(スタックを使った非再帰版)に書き換えることを検討しましょう。
サンプルコード
Java 8 では抽象クラス FileSystemNode を共通の型として使います。FileNode(葉)と DirectoryNode(複合)が同じ抽象クラスを継承することで、利用側はどちらか区別せずに getName() / getSize() / print() を呼び出せます。
よくあるミス・注意点
⚠️ 親への参照を保持すると循環参照が発生する
ノードが親への参照を持つ場合(例: parent フィールド)、 誤って子が自分自身の祖先を子として追加してしまうと、getSize() が無限再帰になりスタックオーバーフローが発生します。 add() メソッドで循環参照のチェックを行うか、 親への参照を持つ設計を避けることを検討しましょう。
⚠️ 葉ノードに add() メソッドを定義してしまう
Component(共通型)に add() を定義すると、 ファイル(葉)にも add() が呼び出せてしまいます。 葉ノードの add() で UnsupportedOperationException を スローする方法もありますが、コンパイル時に検出できません。add() は Composite(複合ノード)だけに定義し、 共通型には含めない設計を推奨します。
⚠️ ツリーが深くなるとスタックオーバーフローになる可能性がある
再帰的に getSize() や print() を呼び出す実装では、 ツリーの深さがスタックの限界(通常数百〜数千レベル)を超えるとStackOverflowError が発生します。 実務で深いツリーを扱う場合は、スタック(Deque)を使った 反復処理に書き換えることを検討しましょう。
テストする観点
- 葉ノード(
FileNode)と枝ノード(DirectoryNode)が共通の型(FileSystemNode)として操作できること - ファイルの
getSize()が正しいサイズを返すこと - 空のディレクトリの
getSize()が 0 を返すこと(境界値) - 子を1つだけ持つディレクトリの
getSize()が子のサイズと等しいこと - ネストしたディレクトリ(root → src → test)の
getSize()が全子孫のサイズ合計を返すこと - ディレクトリにノードを追加後、
getSize()の結果が正しく更新されること