【実践】Adapterパターンで実現するレガシーシステムとの共存戦略

## はじめに
システム開発の現場では、新しいコードを書くだけでなく、既存のシステムやサードパーティのライブラリと連携する必要があることが多いですよね。特に、以下のような状況に遭遇したことはありませんか？

- レガシーシステムのインターフェースが現在の設計と合わない
- 複数の外部APIを統一的に扱いたい
- テスト時にモックに置き換えやすくしたい

これらの課題に対して、Adapterパターンは非常に効果的なソリューションを提供してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/5528b27b-92f7-c9f9-92d4-61c17d4e3a69.png)

## Adapterパターンとは？
Adapterパターンは、互換性のないインターフェース同士を組み合わせるためのデザインパターンです。

### 特徴
- 既存のクラスを修正することなく、新しいインターフェースに適合させることができる
- 単一責任の原則に従い、変換ロジックを分離できる
- 依存関係を緩和し、テスタビリティを向上させる

## 実践的な実装例
では、実際のユースケースを想定して実装例を見ていきましょう。ここでは、PayPay、Stripe、楽天ペイなどの異なる決済プロバイダーのAPIを統一的に扱う必要があるケースを考えます。

### ユースケース：決済システムの統合
異なる決済プロバイダーのAPIを統一的に扱う必要があるケースを考えてみましょう。

```mermaid
classDiagram
    PaymentProcessor <|.. LegacyPaymentAdapter
    PaymentProcessor <|.. NewPaymentAdapter
    LegacyPaymentAdapter --> LegacyPaymentSystem
    NewPaymentAdapter --> NewPaymentSystem
    PaymentError <|-- InvalidAmountError
    PaymentError <|-- UnsupportedCurrencyError

    class PaymentProcessor {
        <<interface>>
        +process(float amount, str currency) PaymentResult
    }

    class PaymentResult {
        +str transaction_id
        +str timestamp
        +str status
        +Dict details
        +__init__(transaction_id, timestamp, status, details)
        +__repr__()
    }

    class LegacyPaymentSystem {
        +set supported_currencies
        +make_old_payment(float amount, str currency) str
    }

    class NewPaymentSystem {
        +set supported_currencies
        +process_payment(Dict payment_info) str
    }

    class LegacyPaymentAdapter {
        -LegacyPaymentSystem legacy_system
        +__init__(LegacyPaymentSystem system)
        +process(float amount, str currency) PaymentResult
    }

    class NewPaymentAdapter {
        -NewPaymentSystem new_system
        +__init__(NewPaymentSystem system)
        +process(float amount, str currency) PaymentResult
    }

    class PaymentError {
        <<abstract>>
    }

    class InvalidAmountError {
        +str message
    }

    class UnsupportedCurrencyError {
        +str message
    }
```

```mermaid
sequenceDiagram
    participant Client
    participant LegacyAdapter as LegacyPaymentAdapter
    participant LegacySystem as LegacyPaymentSystem
    participant NewAdapter as NewPaymentAdapter
    participant NewSystem as NewPaymentSystem

    %% レガシーシステムでの正常系
    Note over Client,LegacySystem: レガシーシステム - 正常系
    Client->>+LegacyAdapter: process(1000, "JPY")
    LegacyAdapter->>LegacySystem: make_old_payment(1000, "JPY")
    LegacySystem-->>LegacyAdapter: transaction_id
    LegacyAdapter->>LegacyAdapter: create PaymentResult
    LegacyAdapter-->>-Client: PaymentResult

    %% 新システムでの正常系
    Note over Client,NewSystem: 新システム - 正常系
    Client->>+NewAdapter: process(1000, "USD")
    NewAdapter->>NewAdapter: create payment_info dict
    NewAdapter->>NewSystem: process_payment(payment_info)
    NewSystem-->>NewAdapter: transaction_id
    NewAdapter->>NewAdapter: create PaymentResult
    NewAdapter-->>-Client: PaymentResult

    %% レガシーシステムでのエラー系
    Note over Client,LegacySystem: レガシーシステム - エラー系（未対応通貨）
    Client->>+LegacyAdapter: process(1000, "EUR")
    LegacyAdapter->>LegacySystem: make_old_payment(1000, "EUR")
    LegacySystem-->>LegacyAdapter: throw UnsupportedCurrencyError
    LegacyAdapter-->>-Client: raise PaymentError

    %% 新システムでのエラー系
    Note over Client,NewSystem: 新システム - エラー系（不正な金額）
    Client->>+NewAdapter: process(-100, "JPY")
    NewAdapter->>NewAdapter: create payment_info dict
    NewAdapter->>NewSystem: process_payment(payment_info)
    NewSystem-->>NewAdapter: throw InvalidAmountError
    NewAdapter-->>-Client: raise PaymentError
```

```python
# 既存の決済システム（レガシー）
class LegacyPaymentSystem:
    def make_old_payment(self, amount, currency):
        print(f"レガシーシステムで決済実行: {amount} {currency}")
        return f"OLD_TRANSACTION_{amount}"

# 新しい決済システム
class NewPaymentSystem:
    def process_payment(self, payment_info: dict):
        amount = payment_info["amount"]
        currency = payment_info["currency"]
        print(f"新システムで決済実行: {amount} {currency}")
        return f"NEW_TRANSACTION_{amount}"

# 統一的なインターフェース
class PaymentProcessor:
    def process(self, amount: float, currency: str) -> str:
        pass

# レガシーシステム用のAdapter
class LegacyPaymentAdapter(PaymentProcessor):
    def __init__(self, legacy_system: LegacyPaymentSystem):
        self.legacy_system = legacy_system
    
    def process(self, amount: float, currency: str) -> str:
        return self.legacy_system.make_old_payment(amount, currency)

# 新システム用のAdapter
class NewPaymentAdapter(PaymentProcessor):
    def __init__(self, new_system: NewPaymentSystem):
        self.new_system = new_system
    
    def process(self, amount: float, currency: str) -> str:
        payment_info = {
            "amount": amount,
            "currency": currency,
            "timestamp": "2024-10-27"
        }
        return self.new_system.process_payment(payment_info)

# 使用例
def process_payment(processor: PaymentProcessor, amount: float, currency: str):
    return processor.process(amount, currency)

# テストコード
def run_example():
    # レガシーシステムでの決済
    legacy_system = LegacyPaymentSystem()
    legacy_adapter = LegacyPaymentAdapter(legacy_system)
    legacy_result = process_payment(legacy_adapter, 1000, "JPY")
    print(f"レガシー取引ID: {legacy_result}")

    # 新システムでの決済
    new_system = NewPaymentSystem()
    new_adapter = NewPaymentAdapter(new_system)
    new_result = process_payment(new_adapter, 1000, "JPY")
    print(f"新システム取引ID: {new_result}")

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

## メリット
1. **インターフェースの統一**
   - 異なるシステムを統一的なインターフェースで扱える
   - コードの一貫性が保てる

2. **保守性の向上**
   - 変換ロジックが分離されているため、修正が容易
   - 新しいシステムの追加が簡単

3. **テスタビリティの向上**
   - モックやスタブへの置き換えが容易
   - 単体テストが書きやすい

4. **段階的な移行が可能**
   - レガシーシステムを一度に置き換える必要がない
   - リスクを抑えた移行が可能

## デメリット
1. **コードの増加**
   - Adapter用のクラスが必要になる
   - 小規模なシステムでは過剰な可能性がある

2. **複雑さの増加**
   - 間接的な呼び出しが増える
   - デバッグが若干複雑になる可能性がある

## 使い所
以下のような状況で特に効果を発揮します：

1. レガシーシステムとの統合
   - 古いAPIや互換性のないインターフェースの統合
   - 段階的なシステム移行

2. サードパーティライブラリの統合
   - 複数の外部APIの統一的な扱い
   - ベンダーロックインの防止

3. テスト容易性の向上
   - モックへの置き換えが必要な場合
   - テスト環境と本番環境での切り替え

## 実装時の注意点
1. **単一責任の原則を守る**
   - Adapterには変換ロジックのみを含める
   - ビジネスロジックは別のクラスに分離する

2. **テスト容易性を考慮**
   - モックやスタブに置き換えやすい設計を心がける
   - 依存性注入を活用する

3. **エラーハンドリング**
   - 両システムのエラー形式の違いを適切に変換
   - エラーメッセージの統一的な処理

## 実行結果と解説
実際にコードを実行すると、以下のような結果が得られます：

```
=== レガシーシステムのテスト ===
成功: PaymentResult(transaction_id='OLD_TRANSACTION_1000', timestamp='2024-10-27T12:18:33.990273', status='success', details={'system': 'legacy', 'amount': 1000, 'currency': 'JPY'})
成功: PaymentResult(transaction_id='OLD_TRANSACTION_50', timestamp='2024-10-27T12:18:33.990336', status='success', details={'system': 'legacy', 'amount': 50, 'currency': 'USD'})
失敗: amount=100, currency=EUR
失敗: amount=-100, currency=JPY

=== 新システムのテスト ===
成功: PaymentResult(transaction_id='NEW_TRANSACTION_1000', timestamp='2024-10-27T12:18:34.005310', status='success', details={'system': 'new', 'amount': 1000, 'currency': 'JPY'})
成功: PaymentResult(transaction_id='NEW_TRANSACTION_50', timestamp='2024-10-27T12:18:34.005402', status='success', details={'system': 'new', 'amount': 50, 'currency': 'USD'})
成功: PaymentResult(transaction_id='NEW_TRANSACTION_100', timestamp='2024-10-27T12:18:34.005446', status='success', details={'system': 'new', 'amount': 100, 'currency': 'EUR'})
失敗: amount=-100, currency=JPY
```

### 実行結果の解説

#### 1. レガシーシステムの動作
- **成功ケース**：
  - JPY（日本円）とUSD（米ドル）での決済が正常に処理
  - トランザクションIDが適切に生成
  - タイムスタンプと詳細情報が付加

- **失敗ケース**：
  - EUR（ユーロ）：未対応通貨のため失敗
  - 負の金額（-100）：不正な入力として適切に拒否

#### 2. 新システムの動作
- **成功ケース**：
  - JPY, USD, EURすべての通貨で決済が成功
  - レガシーシステムで失敗したEURも正常に処理
  - 各決済に固有のトランザクションIDを付与

- **失敗ケース**：
  - 負の金額（-100）：システムの制約として適切に拒否

#### 3. エラーハンドリングの確認
```
ERROR:__main__:レガシーシステムでエラー発生: Unsupported currency: EUR
ERROR:__main__:新システムでエラー発生: Invalid amount: -100
```
- 適切なエラーメッセージがログに記録
- エラーの種類に応じた異なる例外が発生
- ログレベルが適切に設定（ERRORレベル）

### 重要なポイント
1. **統一的なインターフェース**
   - 異なるシステムでも同じ形式の結果を返却
   - PaymentResultオブジェクトによる一貫性のある結果形式

2. **システム固有の制約の維持**
   - レガシーシステム：限定された通貨対応
   - 新システム：より広い通貨対応
   - 各システムの特性を保持しながら統一的に扱える

3. **堅牢なエラーハンドリング**
   - 不正な入力の適切な検出
   - 詳細なエラー情報の提供
   - システム別のエラー処理

この実行結果から、Adapterパターンが以下の目的を達成していることが確認できます：
- 異なるインターフェースの統一
- 既存システムの機能維持
- エラー処理の一貫性
- システム固有の制約の適切な管理

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/d0214b74-6675-8d7c-4809-50daa1aed49e.png)

Adapterパターンは、システム統合やレガシーコードの管理において非常に強力なツールです。適切に使用することで、コードの保守性、テスタビリティ、拡張性を向上させることができます。