はじめに
皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第11回目です。
今回は、個々のオブジェクトと、それらの集合体である複合オブジェクトを統一的に扱うためのComposite(コンポジット)パターンについて解説します。
Compositeパターンとは?
Compositeパターンは、オブジェクトをツリー構造で構成し、個々のオブジェクト(葉:Leaf)と複合オブジェクト(枝:Composite)を同じように扱えるようにする構造パターンです。これにより、クライアントはオブジェクトの階層構造を意識することなく、シンプルにコードを書くことができます。
身近な例
パソコンのファイルシステムを考えてみましょう。ユーザーは、単一のファイル(葉)も、複数のファイルやフォルダを含むフォルダ(枝)も、同じようにopen、copy、deleteといった操作で扱うことができます。フォルダの中にさらにフォルダがあっても、操作方法は変わりません。
Compositeパターンが解決する問題
階層構造を扱うとき、従来のアプローチでは以下のような問題が発生します:
// 問題のあるアプローチ例
if (object instanceof Employee) {
((Employee) object).showDetails();
} else if (object instanceof Department) {
((Department) object).showDetails();
// さらに子要素に対する処理...
}
このような条件分岐は、新しい型が追加されるたびに修正が必要になり、保守性が悪化します。
パターンの構成要素
- コンポーネント(Component): 個々のオブジェクトと複合オブジェクトの共通インターフェース
- 葉(Leaf): 末端のオブジェクト。子要素を持たない
- 複合体(Composite): 子コンポーネントを保持するオブジェクト。子要素として葉や他の複合体を持つことができる
Javaでの実装:階層的なインターフェース設計
Javaは厳密なインターフェースとクラス階層を持つため、Compositeパターンに非常に適しています。
以下に、ツリー構造を表現する例として、部署と従業員を扱う組織図を実装します。
import java.util.ArrayList;
import java.util.List;
// 1. コンポーネントインターフェース
interface OrganizationComponent {
void showDetails();
void showDetails(int depth); // インデント用のオーバーロード
int getEmployeeCount(); // 従業員数を取得
}
// 2. 葉(従業員)
class Employee implements OrganizationComponent {
private String name;
private String position;
private double salary;
public Employee(String name, String position, double salary) {
this.name = name;
this.position = position;
this.salary = salary;
}
@Override
public void showDetails() {
showDetails(0);
}
@Override
public void showDetails(int depth) {
String indent = " ".repeat(depth);
System.out.printf("%s- Employee: %s, Position: %s, Salary: $%.0f%n",
indent, name, position, salary);
}
@Override
public int getEmployeeCount() {
return 1;
}
// Getter methods
public String getName() { return name; }
public String getPosition() { return position; }
public double getSalary() { return salary; }
}
// 3. 複合体(部署)
class Department implements OrganizationComponent {
private String name;
private String manager;
private List<OrganizationComponent> components = new ArrayList<>();
public Department(String name, String manager) {
this.name = name;
this.manager = manager;
}
public void add(OrganizationComponent component) {
components.add(component);
}
public void remove(OrganizationComponent component) {
components.remove(component);
}
public List<OrganizationComponent> getComponents() {
return new ArrayList<>(components); // 防御的コピー
}
@Override
public void showDetails() {
showDetails(0);
}
@Override
public void showDetails(int depth) {
String indent = " ".repeat(depth);
System.out.printf("%sDepartment: %s (Manager: %s) - %d employees%n",
indent, name, manager, getEmployeeCount());
for (OrganizationComponent component : components) {
component.showDetails(depth + 1);
}
}
@Override
public int getEmployeeCount() {
return components.stream()
.mapToInt(OrganizationComponent::getEmployeeCount)
.sum();
}
// Getter methods
public String getName() { return name; }
public String getManager() { return manager; }
}
// 使用例とデモクラス
public class OrganizationDemo {
public static void main(String[] args) {
// 開発部門の作成
Department devDepartment = new Department("Development", "Alice Johnson");
devDepartment.add(new Employee("Bob Smith", "Senior Engineer", 90000));
devDepartment.add(new Employee("Carol Wilson", "Junior Engineer", 65000));
devDepartment.add(new Employee("David Brown", "QA Engineer", 70000));
// マーケティング部門の作成
Department marketingDepartment = new Department("Marketing", "Eve Davis");
marketingDepartment.add(new Employee("Frank Miller", "Marketing Specialist", 60000));
marketingDepartment.add(new Employee("Grace Lee", "Content Creator", 55000));
// 本社の作成(複合体を子要素として追加)
Department headOffice = new Department("Head Office", "John CEO");
headOffice.add(new Employee("Jane CFO", "Chief Financial Officer", 150000));
headOffice.add(devDepartment); // 部署を子要素として追加
headOffice.add(marketingDepartment);
// 統一的なインターフェースで全体を操作
System.out.println("=== Organization Structure ===");
headOffice.showDetails();
System.out.println("\n=== Total Employee Count ===");
System.out.println("Total employees: " + headOffice.getEmployeeCount());
// 個別の部署も同じように扱える
System.out.println("\n=== Development Department Only ===");
devDepartment.showDetails();
}
}
この実装では、DepartmentとEmployeeはどちらもOrganizationComponentインターフェースを実装しているため、クライアントコードは両者を統一的に扱うことができます。
Pythonでの実装:動的型付けと柔軟性
Pythonでは、厳密なインターフェースを必要としないため、よりシンプルにCompositeパターンを実装できます。ダック・タイピングにより、同じメソッドを持つクラスは自動的に互換性を持ちます。
from abc import ABC, abstractmethod
from typing import List, Union
# 1. 抽象基底クラス(オプション:より明確なインターフェースのため)
class OrganizationComponent(ABC):
@abstractmethod
def show_details(self, depth: int = 0) -> None:
pass
@abstractmethod
def get_employee_count(self) -> int:
pass
# 2. 葉(従業員)
class Employee(OrganizationComponent):
def __init__(self, name: str, position: str, salary: float):
self.name = name
self.position = position
self.salary = salary
def show_details(self, depth: int = 0) -> None:
indent = " " * depth
print(f"{indent}- Employee: {self.name}, Position: {self.position}, Salary: ${self.salary:,.0f}")
def get_employee_count(self) -> int:
return 1
def __str__(self) -> str:
return f"Employee({self.name}, {self.position})"
# 3. 複合体(部署)
class Department(OrganizationComponent):
def __init__(self, name: str, manager: str):
self.name = name
self.manager = manager
self._components: List[OrganizationComponent] = []
def add(self, component: OrganizationComponent) -> None:
"""子コンポーネントを追加"""
self._components.append(component)
def remove(self, component: OrganizationComponent) -> None:
"""子コンポーネントを削除"""
if component in self._components:
self._components.remove(component)
def get_components(self) -> List[OrganizationComponent]:
"""子コンポーネントのリストを取得(防御的コピー)"""
return self._components.copy()
def show_details(self, depth: int = 0) -> None:
indent = " " * depth
employee_count = self.get_employee_count()
print(f"{indent}Department: {self.name} (Manager: {self.manager}) - {employee_count} employees")
for component in self._components:
component.show_details(depth + 1)
def get_employee_count(self) -> int:
"""部署内の総従業員数を計算"""
return sum(component.get_employee_count() for component in self._components)
def find_employees_by_position(self, position: str) -> List[Employee]:
"""特定の職種の従業員を検索"""
employees = []
for component in self._components:
if isinstance(component, Employee) and component.position == position:
employees.append(component)
elif isinstance(component, Department):
employees.extend(component.find_employees_by_position(position))
return employees
def __str__(self) -> str:
return f"Department({self.name}, {len(self._components)} components)"
# より高度な使用例
class OrganizationAnalyzer:
"""組織分析用のユーティリティクラス"""
@staticmethod
def calculate_total_salary(component: OrganizationComponent) -> float:
"""総給与を計算"""
if isinstance(component, Employee):
return component.salary
elif isinstance(component, Department):
return sum(OrganizationAnalyzer.calculate_total_salary(child)
for child in component.get_components())
return 0.0
@staticmethod
def get_all_employees(component: OrganizationComponent) -> List[Employee]:
"""全従業員のリストを取得"""
employees = []
if isinstance(component, Employee):
employees.append(component)
elif isinstance(component, Department):
for child in component.get_components():
employees.extend(OrganizationAnalyzer.get_all_employees(child))
return employees
# 使用例
def main():
# 開発部門の作成
dev_department = Department("Development", "Alice Johnson")
dev_department.add(Employee("Bob Smith", "Senior Engineer", 90000))
dev_department.add(Employee("Carol Wilson", "Junior Engineer", 65000))
dev_department.add(Employee("David Brown", "QA Engineer", 70000))
# マーケティング部門の作成
marketing_department = Department("Marketing", "Eve Davis")
marketing_department.add(Employee("Frank Miller", "Marketing Specialist", 60000))
marketing_department.add(Employee("Grace Lee", "Content Creator", 55000))
# 本社の作成
head_office = Department("Head Office", "John CEO")
head_office.add(Employee("Jane CFO", "Chief Financial Officer", 150000))
head_office.add(dev_department)
head_office.add(marketing_department)
# 統一的なインターフェースで操作
print("=== Organization Structure ===")
head_office.show_details()
print(f"\n=== Analysis ===")
print(f"Total employees: {head_office.get_employee_count()}")
print(f"Total salary budget: ${OrganizationAnalyzer.calculate_total_salary(head_office):,.0f}")
# 特定の職種を検索
engineers = head_office.find_employees_by_position("Senior Engineer")
print(f"Senior Engineers: {[emp.name for emp in engineers]}")
# 個別の部署も同じように扱える
print(f"\n=== Development Department Only ===")
dev_department.show_details()
if __name__ == "__main__":
main()
Pythonのこの実装では、ABCを使用してより明確なインターフェースを定義しつつ、Pythonの柔軟性を活かした追加機能も実装しています。
Compositeパターンの利点と注意点
利点
- 統一的な操作: 個別オブジェクトと複合オブジェクトを同じインターフェースで扱える
- 再帰的な構造: 複雑な階層構造を自然に表現できる
- 拡張性: 新しいコンポーネントタイプを簡単に追加できる
- クライアントコードの単純化: 階層構造を意識する必要がない
注意点
- 型の制約: すべての子要素が同じインターフェースを持つ必要がある
- 設計の複雑さ: 単純な構造に対しては過度に複雑になる場合がある
- メモリ使用量: 深い階層構造では再帰処理によりスタックオーバーフローのリスクがある
実践的な使用例
Compositeパターンは以下のような場面で特に有効です:
- GUI フレームワーク: ウィンドウ、パネル、ボタンなどのUI要素の階層構造
- ファイルシステム: フォルダとファイルの階層構造
- 組織図: 部署と従業員の階層構造
- 文書構造: 章、節、段落などの階層構造
- 数式解析: 演算子と被演算子の階層構造
まとめ:本質は「階層の透明性」
| 特性 | Java | Python |
|---|---|---|
| 主な解決策 | 厳格なインターフェースとクラス階層 | ABCとダック・タイピング |
| 設計思想 | 継承による型の強制 | 柔軟な振る舞いを許容 |
| コードの意図 | 型を通じて階層構造の意図を明示 | 実行時のメソッド呼び出しで階層構造を扱う |
| 型安全性 | コンパイル時に型チェック | 実行時に型チェック(ABCを使用した場合) |
| 柔軟性 | 厳密だが予測可能 | 柔軟だが実行時エラーのリスク |
Compositeパターンは、両言語で実装のスタイルは異なりますが、 「階層構造を透過的に操作する」 という本質は共通です。Javaはインターフェースでその透明性を強制し、Pythonはダック・タイピングと動的型付けを使って柔軟にツリー構造を構築します。
重要なのは、「部分と全体を同じように扱う」という原則を理解し、複雑な階層構造を単純なインターフェースで操作できるようにすることです。
次回は構造パターンのDecoratorパターンについて解説します。お楽しみに!
次回のテーマは、「Day 12 Decoratorパターン:オブジェクトに動的に機能を追加する」です。