Bridgeパターンで抽象と実装を分離 - データ変換処理を例に

## はじめに
システム開発において、データ形式の変換処理は頻繁に発生する要件の1つです。CSVやJSON、XMLなど様々なフォーマット間でデータを変換する必要があり、また出力先もファイルやデータベース、APIなど多岐にわたります。

このような状況で、変換ロジックと出力処理を密結合にしてしまうと、新しいフォーマットや出力先の追加が困難になってしまいます。今回は、Bridgeパターンを使ってこの問題をエレガントに解決する方法を紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/c2697641-5522-b705-3d0d-dcbc67bed195.png)

## Bridgeパターンとは
Bridgeパターンは、抽象部分と実装部分を分離し、それぞれを独立して変更可能にするデザインパターンです。

### なぜBridgeパターンを使うのか
1. **機能の追加が容易**: 新しいデータフォーマットや出力先を追加する際に、既存のコードを変更する必要がありません
2. **単一責任の原則**: データ変換のロジックと出力処理の責務を明確に分離できます
3. **テストの容易性**: 変換ロジックと出力処理を個別にテストできます

### メリット
- 抽象と実装の分離により、コードの保守性が向上
- 新機能の追加が容易
- テストがしやすい
- 依存関係が明確になる

### デメリット
- クラス数が増える
- シンプルな要件の場合は過剰な設計になる可能性がある

## 実装例
以下のコードは、JSONとCSVのデータ変換、およびファイルとコンソールへの出力を実装した例です。

```mermaid
classDiagram
    class DataConverter {
        <<abstract>>
        -writer: DataWriter
        +DataConverter(writer: DataWriter)
        +convert_and_write(data: List[Person], destination: str)*
    }

    class DataWriter {
        <<abstract>>
        +write(data: str, destination: str)*
    }

    class JsonConverter {
        +convert_and_write(data: List[Person], destination: str)
    }

    class CsvConverter {
        +convert_and_write(data: List[Person], destination: str)
    }

    class FileWriter {
        +write(data: str, destination: str)
    }

    class ConsoleWriter {
        +write(data: str, destination: str)
    }

    class Person {
        +name: str
        +age: int
        +email: str
    }

    DataConverter <|-- JsonConverter : 継承
    DataConverter <|-- CsvConverter : 継承
    DataWriter <|-- FileWriter : 継承
    DataWriter <|-- ConsoleWriter : 継承
    DataConverter o-- DataWriter : 利用

    note for DataConverter "抽象部分\n変換処理を担当"
    note for DataWriter "実装部分\n出力処理を担当"

    %% 追加実装の例
    class DatabaseWriter {
        -connection_string: str
        +write(data: str, destination: str)
    }
    class ExcelConverter {
        +convert_and_write(data: List[Person], destination: str)
    }
    
    DataWriter <|-- DatabaseWriter : 継承
    DataConverter <|-- ExcelConverter : 継承
```

```mermaid
sequenceDiagram
    participant Client as クライアント
    participant Conv as DataConverter
    participant Writer as DataWriter
    participant Model as PersonModel
    
    Note over Client,Model: データ変換処理の詳細フロー

    Note right of Client: 1. 基本的な処理フロー
    Client->>+Conv: convert_and_write(data, destination)
    Conv->>Conv: データの検証
    
    Note right of Conv: 変換処理フェーズ
    Conv->>+Model: データモデルの処理
    Model-->>-Conv: 変換用データ
    Conv->>Conv: フォーマット固有の変換処理

    Note right of Conv: 出力処理フェーズ
    Conv->>+Writer: write(converted_data, destination)
    Writer->>Writer: 出力先の検証
    Writer->>Writer: データの書き込み
    Writer-->>-Conv: 処理結果
    Conv-->>-Client: 完了通知

    Note right of Client: 2. 非同期処理の例
    Client->>+Conv: async_convert_and_write(data, destination)
    Conv-->>Client: タスクID返却
    Conv->>Conv: 非同期でデータ変換
    Conv->>+Writer: async_write(converted_data, destination)
    Writer-->>-Conv: 書き込み完了
    Client->>Conv: get_status(taskId)
    Conv-->>Client: 処理状態

    Note over Client,Model: Bridgeパターンにより、変換処理と出力処理が疎結合になっているため、<br/>それぞれを独立して変更可能
```

```python
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Dict
import json
import csv
import io

# データモデル
@dataclass
class Person:
    name: str
    age: int
    email: str

# 実装部分のインターフェース
class DataWriter(ABC):
    @abstractmethod
    def write(self, data: str, destination: str) -> None:
        pass

# 抽象部分のインターフェース
class DataConverter(ABC):
    def __init__(self, writer: DataWriter):
        self._writer = writer
    
    @abstractmethod
    def convert_and_write(self, data: List[Person], destination: str) -> None:
        pass

# ファイル出力の実装
class FileWriter(DataWriter):
    def write(self, data: str, destination: str) -> None:
        with open(destination, 'w', encoding='utf-8') as f:
            f.write(data)

# コンソール出力の実装
class ConsoleWriter(DataWriter):
    def write(self, data: str, destination: str) -> None:
        print(f"=== Output to {destination} ===")
        print(data)

# JSON変換の実装
class JsonConverter(DataConverter):
    def convert_and_write(self, data: List[Person], destination: str) -> None:
        json_data = json.dumps([vars(p) for p in data], ensure_ascii=False, indent=2)
        self._writer.write(json_data, destination)

# CSV変換の実装
class CsvConverter(DataConverter):
    def convert_and_write(self, data: List[Person], destination: str) -> None:
        output = io.StringIO()
        writer = csv.writer(output)
        # ヘッダーの書き込み
        writer.writerow(['name', 'age', 'email'])
        # データの書き込み
        for person in data:
            writer.writerow([person.name, person.age, person.email])
        self._writer.write(output.getvalue(), destination)

def main():
    # サンプルデータ
    people = [
        Person("山田太郎", 30, "yamada@example.com"),
        Person("鈴木花子", 25, "suzuki@example.com"),
    ]

    # JSONとしてファイルに出力
    json_file_converter = JsonConverter(FileWriter())
    json_file_converter.convert_and_write(people, "output.json")

    # CSVとしてコンソールに出力
    csv_console_converter = CsvConverter(ConsoleWriter())
    csv_console_converter.convert_and_write(people, "console")

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

## コードの説明

### 1. 基本構造
- `Person`: データモデルを表すデータクラス
- `DataWriter`: データ出力の抽象インターフェース
- `DataConverter`: データ変換の抽象インターフェース

### 2. Writer（実装部分）
- `FileWriter`: ファイルへの出力を実装
- `ConsoleWriter`: コンソールへの出力を実装

### 3. Converter（抽象部分）
- `JsonConverter`: JSONフォーマットへの変換を実装
- `CsvConverter`: CSVフォーマットへの変換を実装

## 動作確認方法
1. 上記のコードを`bridge_pattern.py`として保存
2. 以下のコマンドで実行:
```bash
python bridge_pattern.py
```

実行すると、以下の出力が得られます：
1. JSONファイル（output.json）が生成される
2. CSVデータがコンソールに出力される

## 実際の使用シーン

### 1. データ移行ツール
#### ユースケース
- レガシーシステムから新システムへのデータ移行
- 異なるデータベース間でのデータ同期

#### 具体例
```python
# 既存のコードに以下を追加
class DatabaseWriter(DataWriter):
    def __init__(self, connection_string: str):
        self.connection_string = connection_string

    def write(self, data: str, destination: str) -> None:
        # 実際のデータベース接続処理
        print(f"Connecting to: {self.connection_string}")
        print(f"Writing to table: {destination}")
        print(f"Data: {data}")

# 使用例
def migrate_data():
    # 移行元データの読み込み
    legacy_data = [
        Person("山田太郎", 30, "yamada@old-system.com"),
        Person("鈴木花子", 25, "suzuki@old-system.com"),
    ]
    
    # 新システムのDBに書き込み
    db_writer = DatabaseWriter("postgresql://newdb:5432/users")
    json_converter = JsonConverter(db_writer)
    json_converter.convert_and_write(legacy_data, "users_table")
```

### 2. マルチフォーマットレポート生成システム
#### ユースケース
- 営業レポートを異なる形式（PDF、Excel、CSV）で出力
- 経営ダッシュボード用のデータ出力

#### 具体例
```python
class ExcelConverter(DataConverter):
    def convert_and_write(self, data: List[Person], destination: str) -> None:
        # Excel形式への変換処理
        excel_data = "Excel format data..."  # 実際にはopenpyxlなどを使用
        self._writer.write(excel_data, destination)

def generate_monthly_report(month: str):
    # 月次データの取得
    monthly_data = [
        Person("営業部 田中", 35, "tanaka@company.com"),
        Person("営業部 佐藤", 28, "sato@company.com"),
    ]
    
    # 異なる形式でレポート出力
    excel_converter = ExcelConverter(FileWriter())
    csv_converter = CsvConverter(FileWriter())
    
    # Excel形式で保存
    excel_converter.convert_and_write(
        monthly_data, 
        f"reports/{month}_sales_report.xlsx"
    )
    
    # CSV形式で保存
    csv_converter.convert_and_write(
        monthly_data,
        f"reports/{month}_sales_report.csv"
    )
```

### 3. ログ集約システム
#### ユースケース
- 複数サーバーからのログを統合
- 異なる形式のログを標準化して保存

#### 具体例
```python
@dataclass
class LogEntry:
    timestamp: str
    level: str
    message: str
    source: str

class LogConverter(DataConverter):
    def convert_and_write(self, data: List[LogEntry], destination: str) -> None:
        # ログ形式への変換処理
        log_lines = []
        for entry in data:
            log_lines.append(
                f"[{entry.timestamp}] {entry.level}: {entry.message} (source: {entry.source})"
            )
        self._writer.write("\n".join(log_lines), destination)

class CloudStorageWriter(DataWriter):
    def write(self, data: str, destination: str) -> None:
        print(f"Uploading to cloud storage: {destination}")
        # 実際のクラウドストレージへの書き込み処理
        print(data)

def aggregate_logs():
    # 異なるサーバーからのログ
    server_logs = [
        LogEntry("2024-01-01 10:00:00", "INFO", "User login", "web-server-1"),
        LogEntry("2024-01-01 10:01:00", "ERROR", "Database connection failed", "db-server-1"),
    ]
    
    # クラウドストレージに保存
    cloud_writer = CloudStorageWriter()
    log_converter = LogConverter(cloud_writer)
    log_converter.convert_and_write(server_logs, "logs/2024/01/01/system.log")
```

### 4. マルチチャネル通知システム
#### ユースケース
- システムアラートを複数の通知先（Slack、メール、SMS）に送信
- 監視システムからの通知を統合管理

#### 具体例
```python
class SlackWriter(DataWriter):
    def write(self, data: str, destination: str) -> None:
        print(f"Sending to Slack channel: {destination}")
        print(f"Message: {data}")

class EmailWriter(DataWriter):
    def write(self, data: str, destination: str) -> None:
        print(f"Sending email to: {destination}")
        print(f"Content: {data}")

def send_system_alert(alert_message: str):
    # アラートデータ
    alert_data = [
        Person("System Alert", 0, alert_message),
    ]
    
    # Slack通知
    slack_converter = JsonConverter(SlackWriter())
    slack_converter.convert_and_write(alert_data, "#system-alerts")
    
    # メール通知
    email_converter = JsonConverter(EmailWriter())
    email_converter.convert_and_write(alert_data, "admin@company.com")
```

## Bridgeパターンの効果的な使用方法

### 1. 導入を検討すべきケース
- 出力形式や出力先が複数存在する場合
- 将来的に新しい形式や出力先の追加が予想される場合
- 変換処理と出力処理の独立したテストが必要な場合

### 2. 注意点
- 小規模なシステムでは過剰な設計になる可能性がある
- インターフェースの設計は将来の拡張性を考慮する
- 共通処理は基底クラスに実装することで重複を避ける

### 3. パフォーマンスに関する考慮事項
- 大量データを扱う場合はストリーム処理の実装を検討
- メモリ使用量に注意（特に大きなファイルの変換時）
- 必要に応じてバッチ処理やチャンク処理を実装

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/a46d01e2-4208-8a27-991a-1051ca1f3a93.png)

Bridgeパターンは、データ変換と出力の分離を実現する強力なツールです。特に、データ移行、レポート生成、ログ管理、通知システムなど、様々な実践的なシーンで活用できます。

システムの要件や将来の拡張性を考慮して、適切なタイミングでBridgeパターンを導入することで、保守性が高く、拡張しやすいシステムを構築することができます。

ただし、シンプルな要件の場合は過剰な設計になる可能性があるため、システムの規模や要件の複雑さを考慮して導入を検討することをお勧めします。