java-recipes

ホーム デザインパターン › DP-08

DP-08: Composite パターン

単一のオブジェクト(葉)と複数のオブジェクトをまとめた複合オブジェクト(枝)を 同じインターフェースで統一的に扱うパターンです。 ファイルシステム・UIコンポーネント・組織図など、再帰的なツリー構造をシンプルに表現できます。

Composite パターンとは

「ファイルのサイズを取得する」と「ディレクトリのサイズを取得する」は、 利用者の視点では同じ操作です。しかし実装が異なるため、 型を区別してコードを書くと instanceof チェックが増えて複雑になります。 Composite パターンでは「単一オブジェクト(葉)」と「複合オブジェクト(枝)」を共通の型(Component)で統一し、 利用側が型を意識しなくて済むようにします。

3つの登場人物

  • Component(共通インターフェース): 葉と枝が共有する抽象型。今回は FileSystemNode
  • Leaf(葉ノード): 子を持たない末端のオブジェクト。今回は FileNode
  • Composite(複合ノード): 子ノードのリストを持つオブジェクト。今回は DirectoryNode

ファイルシステム以外にも、以下のような場面で活用されます。

代表的な応用例

  • UIコンポーネント: ボタン(葉)もパネル(枝)も Component として描画処理を統一する
  • 組織図: 一般社員(葉)も部署(枝)も同じ Employee インターフェースで給与合計を計算する
  • 数式ツリー: 数値(葉)も演算子(枝)も Expression として評価できる

ツリーが深くなると再帰呼び出しが増えます。非常に深い階層(数千レベル以上)では スタックオーバーフローが発生する可能性があります。 実務で深いツリーを扱う場合は再帰を反復処理(スタックを使った非再帰版)に書き換えることを検討しましょう。

サンプルコード

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 String getName() {
            return name;
        }

        // サイズを返す(ファイルは実サイズ、ディレクトリは子の合計)
        public abstract long getSize();

        // ツリー構造を表示する(indent はインデントの文字列)
        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 + " bytes)");
        }
    }

    // ディレクトリを表すクラス(Composite: 複合ノード)
    // 子ノードを持つことができる
    static class DirectoryNode extends FileSystemNode {
        private final List<FileSystemNode> children = new ArrayList<FileSystemNode>();

        public DirectoryNode(String name) {
            super(name);
        }

        // 子ノードを追加する
        public void add(FileSystemNode node) {
            children.add(node);
        }

        // サイズは子ノードのサイズ合計を返す(再帰的に計算)
        @Override
        public long getSize() {
            long total = 0;
            for (FileSystemNode child : children) {
                total += child.getSize();
            }
            return total;
        }

        @Override
        public void print(String indent) {
            System.out.println(indent + "+ " + name + "/ (" + getSize() + " bytes)");
            for (FileSystemNode child : children) {
                // インデントを深くして再帰的に表示する
                child.print(indent + "  ");
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Composite パターン: ファイルシステム ===");

        // ルートディレクトリを作成する
        DirectoryNode root = new DirectoryNode("root");

        // ルート直下のファイル
        root.add(new FileNode("README.txt", 512));
        root.add(new FileNode("config.properties", 256));

        // src ディレクトリ(サブディレクトリ)
        DirectoryNode src = new DirectoryNode("src");
        src.add(new FileNode("Main.java", 2048));
        src.add(new FileNode("Utils.java", 1024));

        // src/test サブディレクトリ
        DirectoryNode test = new DirectoryNode("test");
        test.add(new FileNode("MainTest.java", 768));
        test.add(new FileNode("UtilsTest.java", 512));
        src.add(test);

        root.add(src);

        // lib ディレクトリ
        DirectoryNode lib = new DirectoryNode("lib");
        lib.add(new FileNode("commons.jar", 4096));
        root.add(lib);

        System.out.println("--- ツリー構造 ---");
        root.print("");

        System.out.println("\n--- サイズ確認 ---");
        System.out.println("root 全体: " + root.getSize() + " bytes");
        System.out.println("src ディレクトリ: " + src.getSize() + " bytes");
        System.out.println("test ディレクトリ: " + test.getSize() + " bytes");
        System.out.println("lib ディレクトリ: " + lib.getSize() + " bytes");

        System.out.println("\n--- ポリモーフィズム(同じインターフェースで扱う例)---");
        // ファイルもディレクトリも FileSystemNode として同じように扱える
        List<FileSystemNode> nodes = new ArrayList<FileSystemNode>();
        nodes.add(new FileNode("single.txt", 128));
        nodes.add(src);
        for (FileSystemNode node : nodes) {
            System.out.println(node.getName() + " -> " + node.getSize() + " bytes");
        }
    }
}

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() の結果が正しく更新されること

GitHub でソースコードを見る →