広告

Java : Composite パターン (図解/デザインパターン)

Composite パターンとは、GoF によって定義されたデザインパターンの1つです。
木構造を持つ再帰的なデータ構造を実現するための設計です。

本記事では、Composite パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

図解/デザインパターン一覧

概要

Composite パターン(コンポジット・パターン)とは、GoF (Gang of Four; 4人のギャングたち) によって定義された デザインパターンの1つである。「構造に関するパターン」に属する。Composite パターンを用いるとディレクトリとファイルなどのような、木構造を伴う再帰的なデータ構造を表すことができる。

Composite パターンとは、

  • 木構造を持つ再帰的なデータ構造

を実現するための設計です。

Composite は、日本語的に発音すると「コンポジット」となります。
意味は「合成物」や「混合物」です。ただ、日本語に訳しても、あまりピンとはこないかも…です。

【木構造】

木構造とは、簡単にいうと…

  1. 頂点に ノード() を1つ持ちます。この節は ルート(根) と呼びます。
  2. 節は枝分かれして子供の節を複数持ちます。
  3. 子供を持つことができない節は リーフ() となります。

というデータ構造です。

節から子供、さらに子供へと … 再帰的 な構造になっているのが特徴です。

木構造図

木構造の例としては、コンピュータのファイルシステムがあります。

ファイルシステム図

ファイルシステムでは、

  • ノード(節) : フォルダ
  • リーフ(葉) : ファイル

となります。

ルートのフォルダがあり、その中にはファイルとフォルダを複数持ちます。
そして、そのフォルダの中にはさらにファイルとフォルダが … と 再帰的 な構造になります。

Composite パターンでは、このような木構造をインタフェースとクラスを使って実現します。

関連記事:


クラス図

クラス図1

上の図は、Composite パターンの一般的なクラス図です。

Composite と Leaf クラスは、共通のインタフェースとして Component を実装(implements) します。
そして、この2つのクラスは木構造でいうところの…

  • 節 : Composite クラス
  • 葉 : Leaf クラス

に相当します。

Composite クラスでは、子供(children) として Component インタフェースのリストを持ちます。
Component インタフェースのリストなので、そのサブクラスである

  • Composite クラス
  • Leaf クラス

どちらも格納できる のがポイントですね。

それでは、このクラス図をそのままコードにしてみましょう。

public interface Component {
    void operation();
}
public class Composite implements Component {
    private final List<Component> children = new ArrayList<>();

    @Override
    public void operation() {
        System.out.println("Composite!");
    }

    public void add(Component component) {
        children.add(component);
    }

    public void remove(Component component) {
        children.remove(component);
    }

    public List<Component> getChildren() {
        return List.copyOf(children);
    }
}
public class Leaf implements Component {
    @Override
    public void operation() {
        System.out.println("Leaf!");
    }
}

次に、これらのクラスを使う例です。
recursion メソッドでは、Component を 再帰的 に処理しています。

public void test() {
    final var root = new Composite();
    root.add(new Leaf());

    final var composite = new Composite();
    root.add(composite);

    composite.add(new Leaf());
    composite.add(new Leaf());

    recursion(root, 0);

    // 結果
    // ↓
    //Composite!
    //  Leaf!
    //  Composite!
    //    Leaf!
    //    Leaf!

    root.remove(composite);

    recursion(root, 0);

    // 結果
    // ↓
    //Composite!
    //  Leaf!
}

public void recursion(Component component, int depth) {
    System.out.print("  ".repeat(depth)); // インデント
    component.operation();

    if (component instanceof Composite composite) {
        for (final var child : composite.getChildren()) {
            recursion(child, depth + 1); // 再帰処理
        }
    }
}

具体的な例

もう少し具体的な例も見てみましょう。
Composite パターン を使い、シンプルな XML の構造を表現してみます。

クラス図2

クラス図の右下の2つのクラスが、木構造でいうと、

  • 節 : Element(要素)
  • 葉 : Text

になります。

それではコード例をみてみましょう。

まずは Node インタフェースです。

public interface Node {
}

今回はシンプルな XML ということで、共通のインタフェースで必要となるメソッドはありません。
(例えば、getType メソッドのような、Node の種別を返すメソッドがあってもよいかもです)

次に Element クラスです。
Element クラスは XML の要素を表します。タグ名として name フィールドを持ちます。

【XML 要素の例】

<タグ名> ... </タグ名>

そして、children として Node インタフェースを複数持ちます。
Element と Text クラスを どちらも格納できる のがポイントですね。

public class Element implements Node {
    private final List<Node> children = new ArrayList<>();
    private final String name;

    public Element(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void add(Node node) {
        children.add(node);
    }

    public List<Node> getChildren() {
        return List.copyOf(children);
    }
}

Text クラスでは、data フィールドとしてテキストデータを持ちます。

public class Text implements Node {
    private final String data;

    public Text(String data) {
        this.data = data;
    }

    public String getData() {
        return data;
    }
}

最後に、Node インタフェースを XML 形式で出力する XmlPriner クラスです。
Node インタフェースを 再帰的 に処理して出力します。

見やすさ優先で、インデントや改行も適時いれています。

public class XmlPrinter {

    public static void print(Node node) {
        recursion(node, 0);
    }

    private static void recursion(Node node, int depth) {
        final var indent = "  ".repeat(depth);

        if (node instanceof Text text) {
            System.out.println(indent + text.getData());
        } else if (node instanceof Element element) {
            final var name = element.getName();
            System.out.println(indent + "<" + name + ">");

            for (final var child : element.getChildren()) {
                recursion(child, depth + 1); // 再帰処理
            }

            System.out.println(indent + "</" + name + ">");
        }
    }
}

それでは、これらのシンプルな XML クラスを使ってみましょう。

final var root = new Element("root");
root.add(new Text("食べ物リスト"));

final var food1 = new Element("food-1");
root.add(food1);

food1.add(new Text("牛丼"));

final var food2 = new Element("food-2");
root.add(food2);

food2.add(new Text("ハンバーグ"));

XmlPrinter.print(root);

// 結果
// ↓
//<root>
//  食べ物リスト
//  <food-1>
//    牛丼
//  </food-1>
//  <food-2>
//    ハンバーグ
//  </food-2>
//</root>

無事に、XML 形式で出力できました。

今回の例では XML のノードは Element と Text だけでしたが、例えば XML のコメントを

  • Comment クラス

として、葉の1つに加えてもよいでしょう。


注意点

Composite パターンでは、自分自身も子供として持つことができます。
そのため、ループする親子関係 にならないよう注意が必要です。

Composite パターンのクラス図 のところで作成したコードを例に見てみましょう。

クラス図1

public interface Component {
    void operation();
}

public class Composite implements Component {
    private final List<Component> children = new ArrayList<>();

    @Override
    public void operation() {
        System.out.println("Composite!");
    }

    public void add(Component component) {
        children.add(component);
    }

    public List<Component> getChildren() {
        return List.copyOf(children);
    }
}

もし ループする親子関係 を持った Comosite を再帰処理すると、メソッドの呼び出しは延々と続くことになります。
最終的には StackOverflowError が発生します。

public void test() {
    final var root = new Composite();

    final var child = new Composite();
    root.add(child);

    // 注意! 意図的にループする親子関係を作ります。
    child.add(root);

    try {
        recursion(root, 0);
    } catch (StackOverflowError e) {
        System.out.println("StackOverflowError!");
    }

    // 結果
    // ↓
    //Composite!
    //Composite!
    //Composite!
    // ・
    // ・
    // ・
    //StackOverflowError!
}

public void recursion(Component component, int depth) {
    component.operation();

    if (component instanceof Composite composite) {
        for (final var child : composite.getChildren()) {
            recursion(child, depth + 1); // 再帰処理
        }
    }
}

StackOverflowError はアプリのクラッシュと同義です。
もっとも深刻な不具合の1つですね。

もし厳格な処理が必要なシステムでは、

  • 子供を追加するさいに、親子関係が正常かチェックする

という機構が必要になるでしょう。

public class Composite implements Component {
    private final List<Component> children = new ArrayList<>();

    public void add(Component component) {
        // ★ ここで正常性のチェックが必要!

        children.add(component);
    }

    ... 省略 ...
}    

まとめ

Composite パターンとは、

  • 木構造を持つ再帰的なデータ構造

を実現するための設計です。

木構造の例としては

  • コンピュータのファイルシステム
  • XML

などがあります。

もし木構造を持つ再帰的なデータ構造が必要になった場合は、Composite パターンを検討してみましょう。


関連記事

ページの先頭へ