概要
組織図、ファイルシステム、メニュー構造、帳票の明細と小計――業務システムには「部分と全体を同じように扱いたい」木構造の場面が少なくありません。Composite パターンは、Leaf(葉)と Composite(複合ノード)に共通のインターフェースを持たせ、クライアントが個別の要素か集合かを意識せずに操作できる構造を作ります。この記事ではファイルシステムを題材に、ファイル(Leaf)とディレクトリ(Composite)を同一の FileSystemNode として扱い、サイズの集計やツリー表示を再帰的に実装します。Java 17 の record をファイル情報の保持に使い、var による型推論で記述を簡潔にしたコード例を示します。
使いどころ
ファイルシステムのディレクトリとファイルを同一インターフェースで扱い、合計サイズを再帰的に計算する
組織図の部署と個人を Composite で表現し、指定部署配下の全メンバー一覧を再帰的に取得する
帳票の明細行と小計行を同一インターフェースで扱い、合計金額を階層的に集計する
コード例
import java.util.ArrayList;
import java.util.List;
public class CompositePatternSample {
// Component: 共通インターフェース
static abstract class FileSystemNode {
protected final String name;
public FileSystemNode(String name) { this.name = name; }
public abstract long getSize();
public abstract void print(String indent);
}
// Leaf: ファイル
static class FileNode extends FileSystemNode {
private final long size;
public FileNode(String name, long size) {
super(name);
this.size = size;
}
@Override
public long getSize() { return size; }
@Override
public void print(String indent) {
System.out.println(indent + "- " + name + " (" + size + " B)");
}
}
// Composite: ディレクトリ
static class DirectoryNode extends FileSystemNode {
private final List<FileSystemNode> children = new ArrayList<>();
public DirectoryNode(String name) { super(name); }
public void add(FileSystemNode node) { children.add(node); }
@Override
public long getSize() {
long total = 0;
for (var child : children) { total += child.getSize(); }
return total;
}
@Override
public void print(String indent) {
System.out.println(indent + "+ " + name + "/ (" + getSize() + " B)");
for (var child : children) { child.print(indent + " "); }
}
}
public static void main(String[] args) {
var root = new DirectoryNode("project");
root.add(new FileNode("README.md", 512));
var src = new DirectoryNode("src");
src.add(new FileNode("Main.java", 2048));
src.add(new FileNode("Utils.java", 1024));
root.add(src);
var lib = new DirectoryNode("lib");
lib.add(new FileNode("commons.jar", 4096));
root.add(lib);
root.print("");
System.out.println("合計: " + root.getSize() + " bytes");
}
}Version Coverage
record でファイル情報を保持し、var と拡張 for ループの組み合わせで記述量を削減できる。
// Java 17: var + record で簡潔に
var root = new DirectoryNode("root");
root.add(new FileNode("README.txt", 512));
record FileInfo(String name, long size) {}
var info = new FileInfo("README.txt", 512);Library Comparison
注意点
Composite のインターフェースに add / remove を含めると、Leaf でも呼べてしまう。Leaf で UnsupportedOperationException を投げるか、Composite だけに追加メソッドを持たせるかはトレードオフ
木構造が深くなると再帰呼び出しでスタックオーバーフローが起きる可能性がある。実務の木構造はたいてい浅いが、データ異常で循環参照が発生すると無限再帰になる
Composite パターンは構造の表現に特化している。構造に対する操作を拡張したい場合は Visitor パターンとの併用を検討する
FAQ
ファイルシステム、組織図、メニュー階層、帳票の明細・小計など、部分と全体を同じ操作で扱いたい木構造に適しています。
透過性を重視する設計では Leaf にも add を定義し、UnsupportedOperationException を投げます。安全性を重視するなら Composite だけに add を持たせます。
実務の木構造は通常数十〜数百レベルなので問題ありません。数万レベルの深さが必要な場合はスタック溢れに注意し、反復処理への変換を検討してください。