Strategy、Factory Method、Decoratorパターンで実現する柔軟なデータ変換システム

## 1. はじめに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/63ad244d-0f99-af1c-09a9-14454c864f0d.png)

データ変換システムは、ビジネス要件の変化や技術の進歩に伴い、様々な課題に直面します。本記事では、具体的な要件から始まり、データ変換システムが直面する問題と、それらを解決するためのデザインパターンの適用について説明します。特に、Strategy、Factory Method、Decoratorパターンの活用に焦点を当てます。

## 2. 初期要件と仕様

以下の要件を持つデータ変換システムの開発があったとします。

### 2.1 基本要件
1. 受注データをJSON形式からシステム内部の形式に変換する。
2. 変換されたデータを検証し、エラーがあれば報告する。
3. 変換プロセスの各ステップをログに記録する。

### 2.2 初期仕様
- 入力: JSON形式の受注データ
- 出力: 内部システム用のPythonディクショナリ
- エラー処理: 無効なデータの場合は例外をスロー
- ログ記録: 標準的なPythonのloggingモジュールを使用

## 3. 問題と解決策の概要

初期の実装後、システムの拡張性と保守性を向上させるため、以下の問題に対処する必要が生じました：

| 問題 | 解決策 | 適用パターン |
|------|--------|--------------|
| 1. 新しいデータ形式のサポート | 変換ロジックを個別のクラスにカプセル化 | Strategy パターン |
| 2. 変換器の選択と初期化の複雑さ | 変換器オブジェクトの生成を専門のクラスに委譲 | Factory Method パターン |
| 3. 変換プロセスへの機能追加 | 基本変換処理に動的に機能を追加 | Decorator パターン |
| 4. 変換プロセスの柔軟な組み立て | 上記のパターンを組み合わせて使用 | 複合パターン |

## 4. 詳細説明

### 4.1 Strategy パターンの適用

**問題**:
初期のデータ変換システムはJSON形式のみを処理するように設計されていました。新しいデータ形式（XML、CSV等）をサポートする必要が生じた場合、既存のコードに大幅な変更が必要になります。

**解決策**:
Strategy パターンを適用し、各データ形式の変換ロジックを個別のクラスにカプセル化します。

**パフォーマンスの考慮**:
Strategy パターンの使用により、実行時のオーバーヘッドが若干増加する可能性がありますが、その影響は通常小さいです。大量のデータを処理する場合は、各戦略の実装を最適化することで対応できます。

### 4.2 Factory Method パターンの適用

**問題**:
複数の変換戦略が存在する場合、適切な戦略の選択と初期化が複雑になります。

**解決策**:
Factory Method パターンを導入し、変換器オブジェクトの生成を専門のクラスに委譲します。

**パフォーマンスの考慮**:
Factory Method パターンによる直接的なパフォーマンスへの影響は小さいですが、オブジェクトの生成が頻繁に行われる場合は、オブジェクトプールなどの最適化技術を検討することができます。

### 4.3 Decorator パターンの適用

**問題**:
変換処理の前後で追加の操作（ログ記録、バリデーション、暗号化など）が必要になることがあります。

**解決策**:
Decorator パターンを使用して、基本的な変換処理に追加の機能を動的に付加します。

**パフォーマンスの考慮**:
Decorator パターンは、多数のデコレータを連鎖させると、各デコレータの処理がスタックされるため、パフォーマンスに影響を与える可能性があります。必要最小限のデコレータを使用し、重い処理はデコレータの外で行うことを検討してください。

## 5. 最終的な実装

これらのパターンを組み合わせた最終的な実装例を示します：

```mermaid
classDiagram
    class ConversionStrategy {
        <<abstract>>
        +convert(data: str): dict
        #_to_internal_format(order: dict): dict
    }
    class JsonStrategy {
        +convert(data: str): dict
    }
    class XmlStrategy {
        +convert(data: str): dict
    }
    class ConverterFactory {
        <<abstract>>
        +create_converter(): ConversionStrategy
    }
    class JsonConverterFactory {
        +create_converter(): ConversionStrategy
    }
    class XmlConverterFactory {
        +create_converter(): ConversionStrategy
    }
    class ConverterDecorator {
        -_converter: ConversionStrategy
        +convert(data: str): dict
    }
    class LoggingDecorator {
        +convert(data: str): dict
    }
    class ValidationDecorator {
        +convert(data: str): dict
        -_is_valid(data: dict): bool
    }
    class OrderProcessor {
        -converter: ConversionStrategy
        +process(data: str, decorators: list): dict
    }

    ConversionStrategy <|-- JsonStrategy
    ConversionStrategy <|-- XmlStrategy
    ConverterFactory <|-- JsonConverterFactory
    ConverterFactory <|-- XmlConverterFactory
    ConversionStrategy <|-- ConverterDecorator
    ConverterDecorator <|-- LoggingDecorator
    ConverterDecorator <|-- ValidationDecorator
    ConverterDecorator o-- ConversionStrategy
    OrderProcessor o-- ConverterFactory
    OrderProcessor o-- ConversionStrategy
```
```mermaid
sequenceDiagram
    participant Client as クライアント
    participant OP as OrderProcessor
    participant JCF as JsonConverterFactory
    participant JS as JsonStrategy
    participant LD as LoggingDecorator
    participant VD as ValidationDecorator

    Client->>+OP: OrderProcessor(JsonConverterFactory)を作成
    OP->>+JCF: create_converter()
    JCF-->>-OP: JsonStrategy
    Client->>+OP: process(json_data, [LoggingDecorator, ValidationDecorator])

    OP->>+LD: LoggingDecoratorを作成
    LD-->>-OP: LoggingDecorator
    OP->>+VD: ValidationDecoratorを作成
    VD-->>-OP: ValidationDecorator

    OP->>+VD: convert(json_data)
    VD->>+LD: convert(json_data)
    LD->>LD: ログ記録開始
    LD->>+JS: convert(json_data)
    JS->>JS: JSON解析
    JS->>JS: 内部形式に変換
    JS-->>-LD: 変換結果
    LD->>LD: ログ記録完了
    LD-->>-VD: 変換結果
    VD->>VD: データ検証
    VD-->>-OP: 検証済み変換結果

    OP-->>-Client: 変換・検証済みデータ
```

```python
import json
import xml.etree.ElementTree as ET
from abc import ABC, abstractmethod
import logging

# Strategy Pattern
class ConversionStrategy(ABC):
    @abstractmethod
    def convert(self, data):
        pass

    def _to_internal_format(self, order):
        try:
            return {
                'id': order['order_id'],
                'items': [{'product_id': item['id'], 'quantity': item['qty']} for item in order['items']],
                'total': sum(float(item['price']) * int(item['qty']) for item in order['items'])
            }
        except KeyError as e:
            raise ValueError(f"Invalid order format: missing {str(e)} key") from e
        except (ValueError, TypeError) as e:
            raise ValueError(f"Invalid data in order: {str(e)}") from e

class JsonStrategy(ConversionStrategy):
    def convert(self, data):
        try:
            order = json.loads(data)
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid JSON data: {str(e)}") from e
        return self._to_internal_format(order)

class XmlStrategy(ConversionStrategy):
    def convert(self, data):
        try:
            root = ET.fromstring(data)
            order = {
                'order_id': root.find('order_id').text,
                'items': [
                    {
                        'id': item.find('id').text,
                        'qty': item.find('qty').text,
                        'price': item.find('price').text
                    }
                    for item in root.findall('.//item')
                ]
            }
        except (ET.ParseError, AttributeError) as e:
            raise ValueError(f"Invalid XML data: {str(e)}") from e
        return self._to_internal_format(order)

# Factory Method Pattern
class ConverterFactory(ABC):
    @abstractmethod
    def create_converter(self):
        pass

class JsonConverterFactory(ConverterFactory):
    def create_converter(self):
        return JsonStrategy()

class XmlConverterFactory(ConverterFactory):
    def create_converter(self):
        return XmlStrategy()

# Decorator Pattern
class ConverterDecorator(ConversionStrategy):
    def __init__(self, converter):
        self._converter = converter

    def convert(self, data):
        return self._converter.convert(data)

class LoggingDecorator(ConverterDecorator):
    def convert(self, data):
        logging.info(f"Converting data: {data[:20]}...")
        result = super().convert(data)
        logging.info("Conversion completed")
        return result

class ValidationDecorator(ConverterDecorator):
    def convert(self, data):
        result = super().convert(data)
        if not self._is_valid(result):
            raise ValueError("Invalid converted data")
        return result

    def _is_valid(self, data):
        return all(key in data for key in ['id', 'items', 'total']) and isinstance(data['items'], list)

# Client code
class OrderProcessor:
    def __init__(self, factory):
        self.converter = factory.create_converter()

    def process(self, data, decorators=[]):
        for decorator in decorators:
            self.converter = decorator(self.converter)
        return self.converter.convert(data)

# Usage
if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)

    # JSON データの処理
    json_data = '{"order_id": "12345", "items": [{"id": "A001", "qty": "2", "price": "100"}]}'
    json_processor = OrderProcessor(JsonConverterFactory())
    result = json_processor.process(json_data, [LoggingDecorator, ValidationDecorator])
    print("JSON Result:", result)

    # XML データの処理
    xml_data = '''
    <order>
        <order_id>67890</order_id>
        <items>
            <item>
                <id>B002</id>
                <qty>1</qty>
                <price>200</price>
            </item>
        </items>
    </order>
    '''
    xml_processor = OrderProcessor(XmlConverterFactory())
    result = xml_processor.process(xml_data, [LoggingDecorator, ValidationDecorator])
    print("XML Result:", result)

    # エラーケースのテスト
    try:
        invalid_json = '{"invalid": "data"}'
        json_processor.process(invalid_json, [LoggingDecorator, ValidationDecorator])
    except ValueError as e:
        print(f"Caught expected error: {e}")

    # 追加のエラーケース
    try:
        invalid_xml = '<invalid>xml</invalid>'
        xml_processor.process(invalid_xml, [LoggingDecorator, ValidationDecorator])
    except ValueError as e:
        print(f"Caught expected error: {e}")
```

実行例: google colab
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/e078fc51-3e4f-fd66-3584-806b26098306.png)


## 6. まとめ

このアプローチにより、データ変換システムは以下の特性を獲得しました：

1. 新しいデータ形式の追加が容易（Strategy パターン）
2. 変換器の生成と選択が簡素化（Factory Method パターン）
3. 変換プロセスへの機能追加が柔軟（Decorator パターン）
4. 全体として拡張性と保守性が向上（複合パターン）

実際のプロジェクトでは、これらのパターンを状況に応じて適切に組み合わせることで、柔軟で拡張性の高いデータ変換システムを構築できます。システムの要件や複雑さに応じて、必要なパターンを選択し適用することが重要です。同時に、パフォーマンス要件も考慮し、必要に応じて最適化を行うことを忘れないようにしましょう。

Strategy、Factory Method、Decoratorパターンの組み合わせは、データ変換システムに限らず、多くのソフトウェア設計の課題に対して効果的なソリューションを提供します。これらのパターンの理解と適切な適用は、柔軟で保守性の高いシステム設計のための強力なツールとなるでしょう。