CompositeパターンとFactory Methodで構築する複雑なオブジェクト階層管理

## はじめに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/cf57e420-9909-6eeb-c979-fa2698339f39.png)


複雑な階層構造を持つオブジェクトの管理は、多くのシステム開発で直面する課題です。この記事では、CompositeパターンとFactory Methodパターンを組み合わせて、Pythonでこの課題を解決する方法を解説します。

## なぜこの組み合わせが必要なのか？

### 課題
1. 複雑な階層構造を持つオブジェクトの管理が煩雑
2. オブジェクトの生成ロジックが複雑化
3. コードの保守性と拡張性の確保が困難

### 解決策
- Compositeパターン：階層構造の統一的な扱い
- Factory Methodパターン：オブジェクト生成の抽象化
- 両者の組み合わせ：柔軟で保守性の高い設計の実現

## メリット

1. **統一的なインターフェース**
   - すべてのコンポーネントを同じように扱える
   - クライアントコードがシンプルになる

2. **拡張性の向上**
   - 新しい種類のコンポーネントを追加しやすい
   - 生成ロジックの変更が容易

3. **カプセル化の促進**
   - オブジェクト生成の詳細をクライアントから隠蔽
   - 階層構造の内部実装を隠蔽

## デメリット

1. **設計の複雑化**
   - 単純な階層構造には過剰な場合がある
   - 初期設計に時間がかかる

2. **パフォーマンスへの影響**
   - 抽象クラスや継承による若干のオーバーヘッド
   - 深い階層での再帰処理のコスト

## 実装例

```mermaid
classDiagram
    %% Compositeパターン部分
    class FileSystemComponent {
        <<abstract>>
        -name: str
        -_parent: FileSystemComponent
        +get_size(): int
        +print_structure(indent: str): None
    }
    
    class File {
        -size: int
        +get_size(): int
        +print_structure(indent: str): None
    }
    
    class Directory {
        -children: List[FileSystemComponent]
        +get_size(): int
        +print_structure(indent: str): None
        +add(component: FileSystemComponent): None
        +remove(component: FileSystemComponent): None
    }
    
    %% Factory Methodパターン部分
    class FileSystemFactory {
        <<abstract>>
        +create_file(name: str, size: int): File
        +create_directory(name: str): Directory
    }
    
    class StandardFileSystemFactory {
        +create_file(name: str, size: int): File
        +create_directory(name: str): Directory
    }
    
    %% 関係性の定義
    FileSystemComponent <|-- File : 継承
    FileSystemComponent <|-- Directory : 継承
    Directory o-- FileSystemComponent : 含む
    FileSystemFactory <|-- StandardFileSystemFactory : 継承
    FileSystemFactory ..> File : 生成
    FileSystemFactory ..> Directory : 生成
    
    %% クラスの補足説明
    note for File "単一ファイルを表現するクラス"
    note for Directory "ディレクトリを表現するクラス\n他のコンポーネントを含むことができる"
    note for FileSystemFactory "コンポーネントの生成を\n抽象化するファクトリ"
```

```mermaid
sequenceDiagram
    autonumber
    
    participant クライアント as Client
    participant ファクトリ as StandardFileSystemFactory
    participant ルート as RootDirectory
    participant 文書フォルダ as DocumentsDirectory
    participant ファイル as DocumentFile

    %% システムの初期化とルートディレクトリの作成
    Note over クライアント,ファイル: 1. 初期化とルートディレクトリの作成
    クライアント->>+ファクトリ: create_directory("root")<br/>ルートディレクトリの作成を要求
    ファクトリ-->>-ルート: new Directory("root")<br/>ルートディレクトリを生成
    
    %% 文書フォルダの作成
    Note over クライアント,ファイル: 2. 文書フォルダの作成
    クライアント->>+ファクトリ: create_directory("documents")<br/>文書フォルダの作成を要求
    ファクトリ-->>-文書フォルダ: new Directory("documents")<br/>文書フォルダを生成
    
    %% 文書ファイルの作成
    Note over クライアント,ファイル: 3. ファイルの作成
    クライアント->>+ファクトリ: create_file("resume.doc", 1000)<br/>履歴書ファイルの作成を要求
    ファクトリ-->>-ファイル: new File("resume.doc", 1000)<br/>ファイルを生成
    
    %% 階層構造の構築
    Note over クライアント,ファイル: 4. 階層構造の構築
    クライアント->>+文書フォルダ: add(documentFile)<br/>文書フォルダにファイルを追加
    文書フォルダ->>ファイル: _parent = self<br/>親子関係を設定
    deactivate 文書フォルダ
    
    クライアント->>+ルート: add(documentsDir)<br/>ルートに文書フォルダを追加
    ルート->>文書フォルダ: _parent = self<br/>親子関係を設定
    deactivate ルート
    
    %% 構造の表示処理
    Note over クライアント,ファイル: 5. 階層構造の表示とサイズ計算
    クライアント->>+ルート: print_structure()<br/>構造の表示を要求
    ルート->>ルート: get_size()<br/>合計サイズを計算
    ルート->>+文書フォルダ: get_size()<br/>フォルダサイズを要求
    文書フォルダ->>+ファイル: get_size()<br/>ファイルサイズを要求
    ファイル-->>-文書フォルダ: return 1000<br/>ファイルサイズを返却
    文書フォルダ-->>-ルート: return 1000<br/>フォルダサイズを返却
    ルート-->>-クライアント: 構造を表示

    %% エラー処理の例
    Note over クライアント,ファイル: 6. エラー処理の例
    クライアント->>+文書フォルダ: add(self)<br/>自身を追加しようとする
    文書フォルダ-->>-クライアント: エラー：循環参照はできません<br/>ValueError発生

    %% 表示結果の例
    Note over クライアント,ファイル: 表示結果例：<br/>+ root (1000 bytes)<br/>  + documents (1000 bytes)<br/>    - resume.doc (1000 bytes
```
```python
from abc import ABC, abstractmethod
from typing import List, Union

class FileSystemComponent(ABC):
    def __init__(self, name: str):
        self.name = name
    
    @abstractmethod
    def get_size(self) -> int:
        pass
    
    @abstractmethod
    def print_structure(self, indent: str = "") -> None:
        pass

class File(FileSystemComponent):
    def __init__(self, name: str, size: int):
        super().__init__(name)
        if size < 0:
            raise ValueError("File size cannot be negative")
        self.size = size
    
    def get_size(self) -> int:
        return self.size
    
    def print_structure(self, indent: str = "") -> None:
        print(f"{indent}- {self.name} ({self.size} bytes)")

class Directory(FileSystemComponent):
    def __init__(self, name: str):
        super().__init__(name)
        self.children: List[FileSystemComponent] = []
    
    def add(self, component: FileSystemComponent) -> None:
        # 型チェック
        if not isinstance(component, FileSystemComponent):
            raise TypeError("component must be a FileSystemComponent")
        
        # 循環参照のチェック
        if component is self:
            raise ValueError("Cannot add directory to itself")
        
        # 親ディレクトリへの参照チェック（再帰的）
        if isinstance(component, Directory):
            current = self
            while hasattr(current, '_parent'):
                if current._parent is component:
                    raise ValueError("Cannot create circular reference in directory structure")
                current = current._parent
        
        # 親への参照を設定
        if hasattr(component, '_parent'):
            raise ValueError("Component already has a parent")
        component._parent = self
        
        self.children.append(component)
    
    def remove(self, component: FileSystemComponent) -> None:
        if component in self.children:
            delattr(component, '_parent')
            self.children.remove(component)
        else:
            raise ValueError("Component not found in this directory")
    
    def get_size(self) -> int:
        return sum(child.get_size() for child in self.children)
    
    def print_structure(self, indent: str = "") -> None:
        print(f"{indent}+ {self.name} ({self.get_size()} bytes)")
        for child in self.children:
            child.print_structure(indent + "  ")

class FileSystemFactory(ABC):
    @abstractmethod
    def create_file(self, name: str, size: int) -> File:
        pass
    
    @abstractmethod
    def create_directory(self, name: str) -> Directory:
        pass

class StandardFileSystemFactory(FileSystemFactory):
    def create_file(self, name: str, size: int) -> File:
        if not name:
            raise ValueError("File name cannot be empty")
        return File(name, size)
    
    def create_directory(self, name: str) -> Directory:
        if not name:
            raise ValueError("Directory name cannot be empty")
        return Directory(name)

def create_sample_structure() -> Directory:
    try:
        factory = StandardFileSystemFactory()
        
        # ルートディレクトリの作成
        root = factory.create_directory("root")
        
        # ドキュメント用ディレクトリ
        docs = factory.create_directory("documents")
        docs.add(factory.create_file("resume.doc", 1000))
        docs.add(factory.create_file("report.pdf", 2000))
        
        # 画像用ディレクトリ
        pics = factory.create_directory("pictures")
        pics.add(factory.create_file("vacation.jpg", 3000))
        pics.add(factory.create_file("family.jpg", 4000))
        
        # 構造の組み立て
        root.add(docs)
        root.add(pics)
        root.add(factory.create_file("notes.txt", 500))
        
        return root
    except (ValueError, TypeError) as e:
        print(f"Error creating file system structure: {e}")
        raise

def main() -> None:
    try:
        # サンプル構造の作成と表示
        root = create_sample_structure()
        print("ファイルシステム構造:")
        root.print_structure()
        
        # エラーケースのテスト
        print("\nエラーケースのテスト:")
        try:
            root.add(root)  # 循環参照のテスト
        except ValueError as e:
            print(f"Expected error: {e}")
        
        try:
            root.add("not a component")  # 型チェックのテスト
        except TypeError as e:
            print(f"Expected error: {e}")
            
    except Exception as e:
        print(f"Unexpected error occurred: {e}")

if __name__ == "__main__":
    main()
```

## 主要な実装ポイント

1. **型ヒントの活用**
   - 具体的な戻り値の型（`File`, `Directory`）を明示
   - メソッドの戻り値に`None`を明示
   - コレクションの型ヒントを詳細化

2. **エラー処理の実装**
   - ファイル名の空チェック
   - ファイルサイズの負値チェック
   - コンポーネントの型チェック
   - 例外処理の階層化

3. **循環参照の防止**
   - 自身への参照チェック
   - 親ディレクトリへの循環参照チェック
   - 親子関係の管理機能

## 使用シーン

このパターンの組み合わせは、以下のような状況で特に効果的です：

1. **複雑な階層構造を持つシステム**
   - ファイルシステムのエミュレーション
   - XML/HTMLドキュメントの構造表現
   - 組織図や部門構造の管理

2. **動的に構造が変化するシステム**
   - プラグインシステム
   - カスタマイズ可能なメニュー構造
   - 動的なUIコンポーネント管理

## 実行結果

このコードを実行すると、以下のような出力が得られます：

```
ファイルシステム構造:
+ root (10500 bytes)
  + documents (3000 bytes)
    - resume.doc (1000 bytes)
    - report.pdf (2000 bytes)
  + pictures (7000 bytes)
    - vacation.jpg (3000 bytes)
    - family.jpg (4000 bytes)
  - notes.txt (500 bytes)

エラーケースのテスト:
Expected error: Cannot add directory to itself
Expected error: component must be a FileSystemComponent
```

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/6bf5475a-b550-b22c-cb5a-55747f881630.png)

CompositeパターンとFactory Methodパターンの組み合わせは、複雑な階層構造の管理に強力なソリューションを提供します。Pythonの型ヒントや例外処理を活用することで、より安全で保守性の高いコードを実現できます。

ただし、システムの要件と規模を考慮し、この組み合わせが本当に必要かどうかを慎重に判断することが重要です。シンプルなシステムでは、より簡単な設計で十分な場合もあります。