0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

全30回:静的と動的でどう違うのか、JavaとPythonで学ぶデザインパターン - Day 11 Compositeパターン:木構造を扱う

Posted at

はじめに

皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第11回目です。
今回は、個々のオブジェクトと、それらの集合体である複合オブジェクトを統一的に扱うためのComposite(コンポジット)パターンについて解説します。


Compositeパターンとは?

Compositeパターンは、オブジェクトをツリー構造で構成し、個々のオブジェクト(葉:Leaf)と複合オブジェクト(枝:Composite)を同じように扱えるようにする構造パターンです。これにより、クライアントはオブジェクトの階層構造を意識することなく、シンプルにコードを書くことができます。

身近な例

パソコンのファイルシステムを考えてみましょう。ユーザーは、単一のファイル(葉)も、複数のファイルやフォルダを含むフォルダ(枝)も、同じようにopencopydeleteといった操作で扱うことができます。フォルダの中にさらにフォルダがあっても、操作方法は変わりません。

Compositeパターンが解決する問題

階層構造を扱うとき、従来のアプローチでは以下のような問題が発生します:

// 問題のあるアプローチ例
if (object instanceof Employee) {
    ((Employee) object).showDetails();
} else if (object instanceof Department) {
    ((Department) object).showDetails();
    // さらに子要素に対する処理...
}

このような条件分岐は、新しい型が追加されるたびに修正が必要になり、保守性が悪化します。

パターンの構成要素

  1. コンポーネント(Component): 個々のオブジェクトと複合オブジェクトの共通インターフェース
  2. 葉(Leaf): 末端のオブジェクト。子要素を持たない
  3. 複合体(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();
    }
}

この実装では、DepartmentEmployeeはどちらも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パターンの利点と注意点

利点

  1. 統一的な操作: 個別オブジェクトと複合オブジェクトを同じインターフェースで扱える
  2. 再帰的な構造: 複雑な階層構造を自然に表現できる
  3. 拡張性: 新しいコンポーネントタイプを簡単に追加できる
  4. クライアントコードの単純化: 階層構造を意識する必要がない

注意点

  1. 型の制約: すべての子要素が同じインターフェースを持つ必要がある
  2. 設計の複雑さ: 単純な構造に対しては過度に複雑になる場合がある
  3. メモリ使用量: 深い階層構造では再帰処理によりスタックオーバーフローのリスクがある

実践的な使用例

Compositeパターンは以下のような場面で特に有効です:

  • GUI フレームワーク: ウィンドウ、パネル、ボタンなどのUI要素の階層構造
  • ファイルシステム: フォルダとファイルの階層構造
  • 組織図: 部署と従業員の階層構造
  • 文書構造: 章、節、段落などの階層構造
  • 数式解析: 演算子と被演算子の階層構造

まとめ:本質は「階層の透明性」

特性 Java Python
主な解決策 厳格なインターフェースとクラス階層 ABCとダック・タイピング
設計思想 継承による型の強制 柔軟な振る舞いを許容
コードの意図 型を通じて階層構造の意図を明示 実行時のメソッド呼び出しで階層構造を扱う
型安全性 コンパイル時に型チェック 実行時に型チェック(ABCを使用した場合)
柔軟性 厳密だが予測可能 柔軟だが実行時エラーのリスク

Compositeパターンは、両言語で実装のスタイルは異なりますが、 「階層構造を透過的に操作する」 という本質は共通です。Javaはインターフェースでその透明性を強制し、Pythonはダック・タイピングと動的型付けを使って柔軟にツリー構造を構築します。

重要なのは、「部分と全体を同じように扱う」という原則を理解し、複雑な階層構造を単純なインターフェースで操作できるようにすることです。

次回は構造パターンのDecoratorパターンについて解説します。お楽しみに!

次回のテーマは、「Day 12 Decoratorパターン:オブジェクトに動的に機能を追加する」です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?