概要

組織図、ファイルシステム、メニュー構造、帳票の明細と小計――業務システムには「部分と全体を同じように扱いたい」木構造の場面が少なくありません。Composite パターンは、Leaf(葉)と Composite(複合ノード)に共通のインターフェースを持たせ、クライアントが個別の要素か集合かを意識せずに操作できる構造を作ります。この記事ではファイルシステムを題材に、ファイル(Leaf)とディレクトリ(Composite)を同一の FileSystemNode として扱い、サイズの集計やツリー表示を再帰的に実装します。Java 17 の record をファイル情報の保持に使い、var による型推論で記述を簡潔にしたコード例を示します。

使いどころ

ファイルシステムのディレクトリとファイルを同一インターフェースで扱い、合計サイズを再帰的に計算する

組織図の部署と個人を Composite で表現し、指定部署配下の全メンバー一覧を再帰的に取得する

帳票の明細行と小計行を同一インターフェースで扱い、合計金額を階層的に集計する

コード例

CompositePatternSample.java
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");
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record でファイル情報を保持し、var と拡張 for ループの組み合わせで記述量を削減できる。

Java 17
// 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

標準 API(abstract class + List)木構造の深さや要素数が限られ、自前で構造を管理するとき。木構造の操作(検索・フィルタ・変換)を追加するたびにメソッドが増える。
DOM API(org.w3c.dom)XML の木構造を操作する場合。Composite パターンの適用例として参考になる。汎用の木構造には冗長。XML 以外の用途では使いにくい。

注意点

Composite のインターフェースに add / remove を含めると、Leaf でも呼べてしまう。Leaf で UnsupportedOperationException を投げるか、Composite だけに追加メソッドを持たせるかはトレードオフ

木構造が深くなると再帰呼び出しでスタックオーバーフローが起きる可能性がある。実務の木構造はたいてい浅いが、データ異常で循環参照が発生すると無限再帰になる

Composite パターンは構造の表現に特化している。構造に対する操作を拡張したい場合は Visitor パターンとの併用を検討する

FAQ

Composite パターンはどんなデータ構造に使えますか。

ファイルシステム、組織図、メニュー階層、帳票の明細・小計など、部分と全体を同じ操作で扱いたい木構造に適しています。

Leaf に add メソッドがあるのは変ではないですか。

透過性を重視する設計では Leaf にも add を定義し、UnsupportedOperationException を投げます。安全性を重視するなら Composite だけに add を持たせます。

Composite と再帰の組み合わせで性能は問題になりませんか。

実務の木構造は通常数十〜数百レベルなので問題ありません。数万レベルの深さが必要な場合はスタック溢れに注意し、反復処理への変換を検討してください。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます