2
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?

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

Last updated at Posted at 2024-10-28

はじめに

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

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

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

image.png

Adapterパターンとは?

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

特徴

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

実践的な実装例

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

ユースケース:決済システムの統合

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

# 既存の決済システム(レガシー)
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

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

2
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
2
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?