Composite パターン
Composite パターンは、オブジェクトをツリー構造で構成し、個々のオブジェクトと複合オブジェクトを同じように扱えるようにするデザインパターンです。クライアントがシンプルなリーフ(葉)オブジェクトと複合オブジェクトを区別せずに統一的に扱えるようにします。
目的と用途
Composite パターンの主な目的は以下の通りです:
- 部分-全体の階層構造を表現する
- クライアントが個別オブジェクトと複合オブジェクトを統一的に扱えるようにする
- 再帰的構造を簡潔に扱えるようにする
- コードの複雑さを軽減し、クライアントを単純化する
一般的な用途:
- ファイルシステム(ディレクトリとファイル)
- グラフィカルユーザーインターフェース(コンポーネントのコンテナと単純コンポーネント)
- 組織階層(部門と従業員)
- メニュー構造(サブメニューと項目)
- XML / HTML DOM(要素と子要素)
クラス図
図1: Composite パターンのクラス図
実装例
- 基本実装
- ファイルシステム例
- 組織階層例
- GUI例
Composite パターンの基本的な実装例です。コンポーネント、リーフ、複合体の関係を示しています。
// コンポーネントインターフェース(共通操作) public interface Component { void operation(); void add(Component component); void remove(Component component); Component getChild(int index); } // リーフ(葉)クラス - 個別オブジェクト public class Leaf implements Component { private String name; public Leaf(String name) { this.name = name; } @Override public void operation() { System.out.println("リーフ " + name + " の操作を実行"); } @Override public void add(Component component) { // リーフは子を持たないので、何もしないか例外をスローする throw new UnsupportedOperationException("リーフは子を追加できません"); } @Override public void remove(Component component) { // リーフは子を持たないので、何もしないか例外をスローする throw new UnsupportedOperationException("リーフは子を削除できません"); } @Override public Component getChild(int index) { // リーフは子を持たないので、何もしないか例外をスローする throw new UnsupportedOperationException("リーフは子を持ちません"); } } // 複合体クラス - 複合オブジェクト public class Composite implements Component { private String name; private List<Component> children = new ArrayList<>(); public Composite(String name) { this.name = name; } @Override public void operation() { System.out.println("複合体 " + name + " の操作を実行"); // 子コンポーネントに対して再帰的に操作を実行 for (Component component : children) { component.operation(); } } @Override public void add(Component component) { children.add(component); } @Override public void remove(Component component) { children.remove(component); } @Override public Component getChild(int index) { return children.get(index); } } // クライアントコード public class Client { public static void main(String[] args) { // リーフノードを作成 Component leaf1 = new Leaf("A"); Component leaf2 = new Leaf("B"); Component leaf3 = new Leaf("C"); Component leaf4 = new Leaf("D"); // 複合体を作成し、子を追加 Composite composite1 = new Composite("X"); composite1.add(leaf1); composite1.add(leaf2); Composite composite2 = new Composite("Y"); composite2.add(leaf3); composite2.add(leaf4); // 複合体をネストする Composite root = new Composite("Root"); root.add(composite1); root.add(composite2); // ツリー構造全体に対して操作を実行 root.operation(); } }
ファイルシステムを表現する Composite パターンの実装例です。
// ファイルシステム要素のインターフェース public interface FileSystemComponent { String getName(); long getSize(); // ファイルサイズまたはディレクトリ内のファイルの合計サイズ void print(String indent); // 階層構造を表示 } // ファイルクラス(リーフ) public class File implements FileSystemComponent { private String name; private long size; public File(String name, long size) { this.name = name; this.size = size; } @Override public String getName() { return name; } @Override public long getSize() { return size; } @Override public void print(String indent) { System.out.println(indent + "ファイル: " + name + " (" + size + " バイト)"); } } // ディレクトリクラス(複合体) public class Directory implements FileSystemComponent { private String name; private List<FileSystemComponent> children = new ArrayList<>(); public Directory(String name) { this.name = name; } public void addComponent(FileSystemComponent component) { children.add(component); } public void removeComponent(FileSystemComponent component) { children.remove(component); } @Override public String getName() { return name; } @Override public long getSize() { // 子要素のサイズを合計 long totalSize = 0; for (FileSystemComponent component : children) { totalSize += component.getSize(); } return totalSize; } @Override public void print(String indent) { System.out.println(indent + "ディレクトリ: " + name + " (" + getSize() + " バイト)"); // 子要素を表示(インデントを増やす) for (FileSystemComponent component : children) { component.print(indent + " "); } } } // クライアントコード public class FileSystemExample { public static void main(String[] args) { // ファイルを作成 File file1 = new File("document.txt", 1000); File file2 = new File("image.jpg", 2000); File file3 = new File("video.mp4", 5000); // ディレクトリを作成し、ファイルを追加 Directory documents = new Directory("Documents"); documents.addComponent(file1); Directory pictures = new Directory("Pictures"); pictures.addComponent(file2); // ディレクトリをネストする Directory media = new Directory("Media"); media.addComponent(pictures); media.addComponent(file3); // ルートディレクトリを作成 Directory root = new Directory("Root"); root.addComponent(documents); root.addComponent(media); // ファイルシステムの階層構造を表示 root.print(""); // 合計サイズを取得 System.out.println("合計サイズ: " + root.getSize() + " バイト"); } }
組織階層を表現する Composite パターンの実装例です。
// 組織階層の基本構成要素 public abstract class OrganizationComponent { protected String name; protected String title; public OrganizationComponent(String name, String title) { this.name = name; this.title = title; } public String getName() { return name; } public String getTitle() { return title; } // 共通操作 public abstract void printStructure(String indent); public abstract int getEmployeeCount(); // デフォルトの実装(サブクラスでオーバーライド) public void add(OrganizationComponent component) { throw new UnsupportedOperationException(); } public void remove(OrganizationComponent component) { throw new UnsupportedOperationException(); } } // 従業員クラス(リーフ) public class Employee extends OrganizationComponent { private double salary; public Employee(String name, String title, double salary) { super(name, title); this.salary = salary; } @Override public void printStructure(String indent) { System.out.println(indent + title + ": " + name + " (給料: " + salary + "円)"); } @Override public int getEmployeeCount() { return 1; // 従業員自身をカウント } } // 部門クラス(複合体) public class Department extends OrganizationComponent { private List<OrganizationComponent> subordinates = new ArrayList<>(); public Department(String name, String title) { super(name, title); } @Override public void add(OrganizationComponent component) { subordinates.add(component); } @Override public void remove(OrganizationComponent component) { subordinates.remove(component); } @Override public void printStructure(String indent) { System.out.println(indent + title + ": " + name + " (従業員数: " + getEmployeeCount() + "人)"); // 部下の構造を表示 for (OrganizationComponent component : subordinates) { component.printStructure(indent + " "); } } @Override public int getEmployeeCount() { int count = 0; // すべての部下(従業員と部門)の従業員数を合計 for (OrganizationComponent component : subordinates) { count += component.getEmployeeCount(); } return count; } } // クライアントコード public class OrganizationExample { public static void main(String[] args) { // 従業員を作成 Employee ceo = new Employee("山田太郎", "CEO", 1000000); Employee cto = new Employee("佐藤次郎", "CTO", 800000); Employee cfo = new Employee("鈴木三郎", "CFO", 800000); Employee dev1 = new Employee("田中一郎", "シニア開発者", 600000); Employee dev2 = new Employee("中村二郎", "開発者", 400000); Employee dev3 = new Employee("小林三郎", "開発者", 400000); Employee finance1 = new Employee("高橋花子", "経理マネージャー", 500000); Employee finance2 = new Employee("伊藤恵子", "経理スタッフ", 350000); // 部門を作成 Department devDepartment = new Department("開発部", "開発部門"); devDepartment.add(dev1); devDepartment.add(dev2); devDepartment.add(dev3); Department financeDepartment = new Department("財務部", "財務部門"); financeDepartment.add(finance1); financeDepartment.add(finance2); // 役員部門を作成 Department executiveDepartment = new Department("役員", "役員部門"); executiveDepartment.add(ceo); executiveDepartment.add(cto); executiveDepartment.add(cfo); // 会社全体を作成 Department company = new Department("ABC株式会社", "会社"); company.add(executiveDepartment); company.add(devDepartment); company.add(financeDepartment); // 組織構造を表示 company.printStructure(""); System.out.println("総従業員数: " + company.getEmployeeCount() + "人"); } }
GUIコンポーネントを表現する Composite パターンの実装例です。
// GUIコンポーネントの基本インターフェース public interface UIComponent { void render(); void resize(); String getDescription(); } // シンプルなUIコントロール(リーフ) public class UIControl implements UIComponent { private String name; private int width; private int height; public UIControl(String name, int width, int height) { this.name = name; this.width = width; this.height = height; } @Override public void render() { System.out.println("UIコントロール '" + name + "' を描画: " + width + "x" + height); } @Override public void resize() { System.out.println("UIコントロール '" + name + "' のサイズを変更"); } @Override public String getDescription() { return "UIコントロール: " + name; } } // コンテナ(複合体) public class UIContainer implements UIComponent { private String name; private List<UIComponent> components = new ArrayList<>(); private int width; private int height; public UIContainer(String name, int width, int height) { this.name = name; this.width = width; this.height = height; } public void addComponent(UIComponent component) { components.add(component); } public void removeComponent(UIComponent component) { components.remove(component); } @Override public void render() { System.out.println("UIコンテナ '" + name + "' を描画: " + width + "x" + height); // すべての子コンポーネントを描画 for (UIComponent component : components) { component.render(); } } @Override public void resize() { System.out.println("UIコンテナ '" + name + "' のサイズを変更"); // すべての子コンポーネントをリサイズ for (UIComponent component : components) { component.resize(); } } @Override public String getDescription() { return "UIコンテナ: " + name + " (子コンポーネント数: " + components.size() + ")"; } } // 具体的なUIコントロール public class Button extends UIControl { public Button(String name, int width, int height) { super(name, width, height); } @Override public String getDescription() { return "ボタン: " + super.getDescription(); } } public class TextBox extends UIControl { public TextBox(String name, int width, int height) { super(name, width, height); } @Override public String getDescription() { return "テキストボックス: " + super.getDescription(); } } // クライアントコード public class UIExample { public static void main(String[] args) { // UIコントロールを作成 Button submitButton = new Button("送信", 100, 30); Button cancelButton = new Button("キャンセル", 100, 30); TextBox nameTextBox = new TextBox("名前", 200, 30); TextBox emailTextBox = new TextBox("メール", 200, 30); // フォームコンテナを作成 UIContainer form = new UIContainer("フォーム", 300, 200); form.addComponent(nameTextBox); form.addComponent(emailTextBox); // ボタンパネルを作成 UIContainer buttonPanel = new UIContainer("ボタンパネル", 250, 40); buttonPanel.addComponent(submitButton); buttonPanel.addComponent(cancelButton); // フォームにボタンパネルを追加 form.addComponent(buttonPanel); // ダイアログを作成 UIContainer dialog = new UIContainer("ダイアログ", 350, 300); dialog.addComponent(form); // ダイアログを描画 dialog.render(); // 説明を表示 System.out.println("\n各コンポーネントの説明:"); System.out.println(dialog.getDescription()); System.out.println(form.getDescription()); System.out.println(buttonPanel.getDescription()); System.out.println(submitButton.getDescription()); } }
実際の使用例
Javaの標準ライブラリやフレームワークには、Compositeパターンを使用している例が多くあります:
- javax.swing.JComponent - Swingのコンポーネント階層(JPanel、JButton、JTextFieldなど)
- java.awt.Container - AWT(Abstract Window Toolkit)のコンテナとコンポーネントの関係
- org.w3c.dom.Node - DOMツリーにおける要素と子要素の関係
- java.io.File - ファイルとディレクトリの操作(Java 7以前)
- java.nio.file.Path - 新しいファイルシステムAPI(Java 7以降)
メリットとデメリット
メリット
- 部分-全体の階層構造を自然に表現できる
- 複雑な木構造を簡単に操作できる
- クライアントコードがシンプルになる(リーフと複合体を区別する必要がない)
- 新しいコンポーネント型の追加が容易(開放閉鎖の原則に従う)
デメリット
- コンポーネントインターフェースが汎用的すぎると、型安全性が低下する
- リーフノードに意味のない操作が定義される場合がある
- 深い階層の場合、パフォーマンスに影響する可能性がある(再帰呼び出し)
- コンポーネント間の関係が複雑になると、管理が難しくなる
実装のバリエーション
Composite パターンには、いくつかの実装バリエーションがあります:
- 透過的アプローチ: すべての操作(add、remove、getChildなど)をコンポーネントインターフェースに定義します。クライアントコードは単純化されますが、リーフノードに意味のない操作が定義されてしまいます。
- 安全なアプローチ: 子ノード管理の操作(add、remove、getChildなど)は複合体クラスにのみ定義します。型安全性は向上しますが、クライアントはリーフと複合体を区別する必要があります。
- 親参照: 子ノードが親を参照できるようにします。ツリー内のナビゲーションが容易になりますが、オブジェクト間の依存関係が複雑になります。
- コンポーネントキャッシュ: 特定のコンポーネントを効率的に検索するためのキャッシュを実装します。大規模なツリー構造の場合に便利です。
関連パターン
- Decorator: Decoratorは単一コンポーネントに機能を追加しますが、Compositeはコンポーネントのグループを扱います。両方ともコンポーネントインターフェースを共有できます。
- Iterator: Iteratorパターンを使用して、Compositeの要素を順番にアクセスできます。
- Visitor: Compositeパターンと組み合わせて、複雑な操作を実行できます。Visitorパターンは階層構造に新しい操作を追加するのに役立ちます。
- Flyweight: 共有可能な葉ノードを管理するためにFlyweightパターンを使用することがあります。
- Chain of Responsibility: コンポーネントの親子関係を利用して、イベント処理や要求の伝播を実装できます。