1. パターンの意図
コンポジット(Composite)パターン は、
個々のオブジェクト(Leaf)と、それらをまとめたコンテナ(Composite)を同一視して扱えるようにする デザインパターンです。
解決する問題
- 階層構造(ツリー構造)を表現したい
- 「個」と「集合」を 同じ操作で扱いたい
- ファイルシステム(フォルダとファイル)のように、再帰的な構造を自然に表現したい
ポイント
- Component:共通インターフェース
- Leaf:末端要素
- Composite:子要素を持ち、Component として振る舞う
- クライアントは「個」と「集合」を区別せず操作できる
2. UML 図
3. Flutter / Dart 実装例
3.1 Component
abstract class FileSystemNode {
void display(String indent);
}
3.2 Leaf
class FileNode implements FileSystemNode {
final String name;
FileNode(this.name);
@override
void display(String indent) {
print("$indent- File: $name");
}
}
3.3 Composite
class DirectoryNode implements FileSystemNode {
final String name;
final List<FileSystemNode> _children = [];
DirectoryNode(this.name);
void add(FileSystemNode node) => _children.add(node);
void remove(FileSystemNode node) => _children.remove(node);
@override
void display(String indent) {
print("$indent+ Directory: $name");
for (var child in _children) {
child.display("$indent ");
}
}
}
3.4 利用例
void main() {
var root = DirectoryNode("root");
root.add(FileNode("file1.txt"));
var subDir = DirectoryNode("sub");
subDir.add(FileNode("file2.txt"));
root.add(subDir);
root.display("");
}
出力:
+ Directory: root
- File: file1.txt
+ Directory: sub
- File: file2.txt
4. Android / Kotlin 実装例
4.1 Component
interface FileSystemNode {
fun display(indent: String = "")
}
4.2 Leaf
class FileNode(private val name: String) : FileSystemNode {
override fun display(indent: String) {
println("$indent- File: $name")
}
}
4.3 Composite
class DirectoryNode(private val name: String) : FileSystemNode {
private val children = mutableListOf<FileSystemNode>()
fun add(node: FileSystemNode) = children.add(node)
fun remove(node: FileSystemNode) = children.remove(node)
override fun display(indent: String) {
println("$indent+ Directory: $name")
children.forEach { it.display("$indent ") }
}
}
4.4 利用例
fun main() {
val root = DirectoryNode("root")
root.add(FileNode("file1.txt"))
val subDir = DirectoryNode("sub")
subDir.add(FileNode("file2.txt"))
root.add(subDir)
root.display()
}
出力は Dart 版と同じ。
5. メリット / デメリット
メリット
- 個と集合を一貫して扱える(クライアントコードがシンプルになる)
- 階層構造を自然に表現できる(ファイルシステム、UIツリーなど)
- 再帰処理との相性が良い
デメリット
- 設計がやや複雑になる
- 子要素の管理方法(追加・削除)を誤ると混乱する
6. 実務ユースケース
Flutter
-
Widget ツリー(
Container
の中にText
、Row
、Column
を入れる構造) - メニュー構造(親メニュー・子メニュー)
- カスタム UI コンポーネントの階層管理
Android (Kotlin)
- View 階層(ViewGroup と View)
- ファイルシステムの抽象化
- ツリー状のデータ構造(メニュー、カテゴリ)
7. 実装上の注意点
- 子要素の管理 API(
add/remove
)は Composite のみに持たせるのが一般的 - Leaf が子管理を持つと SRP 違反になる
- 再帰構造のため テストでは小さなケースから確認すると安全
8. どんなときに使う?
- ツリー構造のデータや UI を表現したいとき
- 個と集合を区別せず処理したいとき
- Widget / View のように入れ子構造が自然な場面
まとめ
- コンポジットパターンは「個と集合を同一視して扱う」ためのパターン
- Flutter では Widget ツリー、Android では View 階層が代表例
- 再帰的な処理やツリー構造の表現で非常に有効
- 個と集合を分けずに一貫して操作できるのが最大のメリット
付記
Flutter Widget ツリーの図解(Composite として)
1) ツリー例(Column
の中に Text
と Row
、さらに Row
の中に Icon
と Text
)
ポイント:
- Column / Row は Composite(子を持つ)
- Text / Icon は Leaf(子を持たない)
- クライアント(フレームワーク)は “Widget” として一様に扱う(個と集合の同一視)
2) 役割マッピング(UML 風)
-
Widget
= Component -
LeafWidget
(例:Text
,Icon
)= Leaf -
MultiChildWidget
(例:Column
,Row
,Stack
)= Composite