Java : Composite パターン (図解/デザインパターン)
Composite パターンとは、GoF によって定義されたデザインパターンの1つです。
木構造を持つ再帰的なデータ構造を実現するための設計です。
本記事では、Composite パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
Composite パターンとは、
- 木構造を持つ再帰的なデータ構造
を実現するための設計です。
Composite は、日本語的に発音すると「コンポジット」となります。
意味は「合成物」や「混合物」です。ただ、日本語に訳しても、あまりピンとはこないかも…です。
【木構造】
木構造とは、簡単にいうと…
- 頂点に ノード(節) を1つ持ちます。この節は ルート(根) と呼びます。
- 節は枝分かれして子供の節を複数持ちます。
- 子供を持つことができない節は リーフ(葉) となります。
というデータ構造です。
節から子供、さらに子供へと … 再帰的 な構造になっているのが特徴です。
木構造の例としては、コンピュータのファイルシステムがあります。
ファイルシステムでは、
- ノード(節) : フォルダ
- リーフ(葉) : ファイル
となります。
ルートのフォルダがあり、その中にはファイルとフォルダを複数持ちます。
そして、そのフォルダの中にはさらにファイルとフォルダが … と 再帰的 な構造になります。
Composite パターンでは、このような木構造をインタフェースとクラスを使って実現します。
関連記事:
クラス図
上の図は、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つのクラスが、木構造でいうと、
- 節 : 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 パターンのクラス図 のところで作成したコードを例に見てみましょう。
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 パターンを検討してみましょう。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン