はじめに
本記事は、GoFデザインパターン解説シリーズの1つです。
今回は、構造に関するパターンの中のComposite(コンポジット)パターンを解説します。
ファイル・フォルダのような「木構造(ツリー構造)」を扱うときの定番パターンです。
解決したい課題
ファイルシステムを例に考えます。
「ファイル」は単体のオブジェクトですが、「フォルダ」はファイルやフォルダを複数含むことができ、フォルダの中にさらにフォルダが入れ子になることもあります。
このとき、「1つのファイルのサイズを取得する処理」と「フォルダ全体のサイズ(中身の合計)を取得する処理」を、呼び出し側でいちいち「これはファイルか、フォルダか」を判定してから処理を分岐する実装をしてしまうと、階層が深くなるほど分岐処理が複雑になり、新しい種類の要素を追加するたびに分岐を増やす必要が出てきます。
Compositeパターンは、「個々の要素(葉)」と「要素の集まり(枝)」を同じインターフェースで扱えるようにすることで、呼び出し側が両者を区別せずに再帰的に処理できるようにします。
クラス構成
- Component(コンポーネント): 「個々の要素」「要素の集まり」の両方に共通するインターフェース
- Leaf(葉): それ以上分解できない、末端の個々の要素
- Composite(コンポジット): 複数のComponent(LeafまたはCompositeの両方)を子として持てる、集合を表すクラス
Javaでの実装例
ファイルシステムを題材に実装してみます。
Component:ファイル・フォルダ共通のインターフェース
public interface FileSystemItem {
String getName();
long getSize(); // バイト単位のサイズ
void print(String indent); // 階層構造を表示する
}
Leaf:ファイル(末端の要素)
public class File implements FileSystemItem {
private final String name;
private final long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public String getName() {
return name;
}
@Override
public long getSize() {
return size; // 自分自身のサイズをそのまま返す
}
@Override
public void print(String indent) {
System.out.println(indent + "📄 " + name + " (" + size + " bytes)");
}
}
Composite:フォルダ(要素の集まり)
import java.util.ArrayList;
import java.util.List;
public class Folder implements FileSystemItem {
private final String name;
private final List<FileSystemItem> children = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
// フォルダにはファイル・フォルダのどちらも追加できる(型はFileSystemItemで統一)
public void add(FileSystemItem item) {
children.add(item);
}
@Override
public String getName() {
return name;
}
@Override
public long getSize() {
// 自分の中身(子要素)のサイズを再帰的に合計する
// 子がFolderであってもFileであっても、getSize()を呼ぶだけでよい
long total = 0;
for (FileSystemItem child : children) {
total += child.getSize();
}
return total;
}
@Override
public void print(String indent) {
System.out.println(indent + "📁 " + name + " (" + getSize() + " bytes)");
for (FileSystemItem child : children) {
child.print(indent + " "); // 再帰的に子要素も表示する
}
}
}
利用側のコード
public class Main {
public static void main(String[] args) {
Folder root = new Folder("project");
Folder src = new Folder("src");
src.add(new File("Main.java", 1200));
src.add(new File("Utils.java", 800));
Folder docs = new Folder("docs");
docs.add(new File("README.md", 500));
root.add(src);
root.add(docs);
root.add(new File(".gitignore", 50));
root.print("");
System.out.println();
System.out.println("project全体のサイズ: " + root.getSize() + " bytes");
}
}
実行結果:
📁 project (2550 bytes)
📁 src (2000 bytes)
📄 Main.java (1200 bytes)
📄 Utils.java (800 bytes)
📁 docs (500 bytes)
📄 README.md (500 bytes)
📄 .gitignore (50 bytes)
project全体のサイズ: 2550 bytes
Mainクラスの利用側コードは、root.getSize()を1回呼んでいるだけで、その裏ではFolderからFolder、さらにFileへと再帰的にサイズの合計処理が実行されています。
利用側は「これはFileかFolderか」を一切判定していません。
両方ともFileSystemItemという同じ型として扱っているためです。
新しい種類の要素(例えばシンボリックリンクを表すSymbolicLinkクラス)を追加する場合も、FileSystemItemを実装しさえすれば、既存のFolderのコードは一切変更する必要がありません。
使用場面
- ファイルシステムのような、木構造(ツリー構造)で表現できるデータを扱う場合
- GUIのコンポーネント階層(ウィンドウの中にパネル、パネルの中にボタン、のような入れ子構造)を扱う場合
- 組織図(部署の中にチーム、チームの中にメンバー)のような、階層構造を持つデータの集計処理
- 「単体の要素」と「要素の集合」を、呼び出し側で区別せずに同じように扱いたい場合全般
まとめ
| 項目 | 内容 |
|---|---|
| 解決する課題 | 木構造データにおいて、「個々の要素」と「要素の集まり」を区別する分岐処理が複雑になる |
| 実現方法 | 両者に共通のインターフェースを持たせ、Compositeは自分の子要素に処理を再帰的に委譲する |
| メリット | 呼び出し側は階層の深さや種類を意識せず、統一的な操作で木構造全体を扱える |
| 典型例 | ファイルシステム、GUIコンポーネント階層、組織図 |
おすすめ書籍
GoFの23パターンをより深く学びたい方には、以下の書籍がおすすめです。
Java言語のサンプルコードとUMLを使い、初心者にもわかりやすく解説されています。
Java言語で学ぶデザインパターン入門 第3版 [ 結城 浩 ]
※本リンクはアフィリエイトリンクを含みます。


