🔥

Rubyで学ぶアダプタパターン (Adapter Pattern)

2025/01/17に公開

1. どんなもの?

アダプタパターンは、異なるインターフェースを持つクラス同士を接続するためのデザインパターンです。
本来互換性のないクラスを「アダプタ」と呼ばれる仲介役のクラスを通して連携させることができます。

例えば、外部ライブラリや古いコードベースを新しいインターフェースに合わせて利用する場合に有効です。

2. 通常の実装方法と比べてどこがすごいの?

通常の方法

通常、異なるインターフェースを持つクラスを直接利用する場合、クライアントコードがそのクラスの詳細な実装に依存します。

class LegacySystem
  def legacy_method
    puts "Performing legacy system action"
  end
end

class ModernSystem
  def modern_method
    puts "Performing modern system action"
  end
end

# クライアントコード
legacy_system = LegacySystem.new
modern_system = ModernSystem.new

if some_condition
  legacy_system.legacy_method
else
  modern_system.modern_method
end
  • 課題:
    • クライアントコードがlegacy_methodやmodern_methodといった異なるメソッド名を直接呼び出しているため、コードが複雑化。
    • 新しいシステムが追加されるたびに、条件分岐を追加する必要があり、保守が困難になる。

アダプタパターンの利点

アダプタを利用すると、異なるインターフェースを統一して扱えるようになります。

# 旧システム
class LegacySystem
  def legacy_method
    puts "Legacy system functionality"
  end
end

# 新システム
class ModernSystem
  def modern_method
    puts "Modern system functionality"
  end
end

# 旧システムと新システムでインターフェースが異なるので、アダプタを利用して統一する
# LegacySystem専用アダプタ
class LegacyAdapter
  def initialize(legacy_system)
    @legacy_system = legacy_system
  end

  # 統一されたインターフェース
  def unified_method
    @legacy_system.legacy_method
  end
end

# ModernSystem専用アダプタ
class ModernAdapter
  def initialize(modern_system)
    @modern_system = modern_system
  end

  # 統一されたインターフェース
  def unified_method
    @modern_system.modern_method
  end
end

# クライアントコード
legacy_adapter = LegacyAdapter.new(LegacySystem.new)
modern_adapter = ModernAdapter.new(ModernSystem.new)

# 統一されたインターフェースで呼び出し
legacy_adapter.unified_method # => "Legacy system functionality"
modern_adapter.unified_method # => "Modern system functionality"
  • 利点:
    • クライアントコードは統一されたインターフェースを利用でき、各システムの詳細な仕様を意識する必要がない。
    • 新しいシステムが追加されても、アダプタを拡張するだけでクライアントコードには変更が不要。
    • コードの一貫性が向上し、可読性が高まる。

3. 技術や手法の"キモ"はどこにある?

  1. アダプタの役割

    • アダプタは、既存のクラス(既存の機能)とクライアントコードの間に立つ中間的な存在です。
    • インターフェースを変換し、互換性を提供します。
  2. 既存コードの再利用

    • アダプタを利用することで、古いコードや外部ライブラリをそのままの形で新しいシステムに組み込むことが可能です。
  3. クライアントコードの独立性

    • クライアントコードはアダプタのインターフェースにのみ依存するため、変更が容易になります。

4. 実装例

例1: 外部APIとの連携

複数の外部の支払いAPIを利用する際、異なるAPI仕様を統一するためにアダプタを利用します。

# APIの実装
class PayPalAPI
  # API ごとに異なるインターフェースを持つ
  def make_payment(amount)
    puts "PayPal processed payment of $#{amount}"
  end
end

class StripeAPI
  # API ごとに異なるインターフェースを持つ
  def charge(amount_in_cents)
    puts "Stripe processed payment of $#{amount_in_cents / 100.0}"
  end
end

# 統一されたインターフェースを定義
class PaymentGateway
  def process_payment(amount)
    raise NotImplementedError, "Subclasses must implement the process_payment method"
  end
end

# PayPal用アダプタ
class PayPalAdapter < PaymentGateway
  def initialize(paypal_api)
    @paypal_api = paypal_api
  end

  # このメソッドでインターフェースを統一
  def process_payment(amount)
    @paypal_api.make_payment(amount)
  end
end

# Stripe用アダプタ
class StripeAdapter < PaymentGateway
  def initialize(stripe_api)
    @stripe_api = stripe_api
  end

  # このメソッドでインターフェースを統一
  def process_payment(amount)
    @stripe_api.charge(amount * 100) # Stripeは金額をセント単位で扱う
  end
end

# クライアントコード
paypal_api = PayPalAPI.new
stripe_api = StripeAPI.new

paypal_adapter = PayPalAdapter.new(paypal_api)
stripe_adapter = StripeAdapter.new(stripe_api)

paypal_adapter.process_payment(100) # => PayPal processed payment of $100
stripe_adapter.process_payment(100) # => Stripe processed payment of $100
  • 実装ポイント:
    • 外部APIの異なるインターフェースをアダプタで統一することで、クライアントコードがシンプルに。

例2: Adapterの統一(応用編)

例1の外部APIのアダプタをまとめる。

# PayPalAPI のインターフェース
class PayPalAPI
  def make_payment(amount)
    puts "PayPal processed payment of $#{amount}"
  end
end

# StripeAPI のインターフェース
class StripeAPI
  def charge(amount_in_cents)
    puts "Stripe processed payment of $#{amount_in_cents / 100.0}"
  end
end

# アダプタによって統一されたインターフェース
class UnifiedPaymentAdapter
  def initialize(api)
    @api = api
  end

  def process_payment(amount)
    case @api
    when PayPalAPI
      @api.make_payment(amount)
    when StripeAPI
      @api.charge(amount * 100)
    else
      raise "Unsupported API"
    end
  end
end

# クライアントコード
paypal_gateway = UnifiedPaymentAdapter.new(PayPalAPI.new)
stripe_gateway = UnifiedPaymentAdapter.new(StripeAPI.new)

paypal_gateway.process_payment(100) # => PayPal processed payment of $100
stripe_gateway.process_payment(100) # => Stripe processed payment of $100
  • 実装ポイント:
    • Adapterの処理が一箇所にまとまり、新しいAPIを追加する場合は、そのAPIに対応した処理をprocess_payment内に追加すればよい。。

5. 議論はあるか?

メリット

  • 異なるインターフェースを統一できるため、システムの一貫性が向上。
  • 既存コードや外部ライブラリを変更せずに再利用できる。
  • クライアントコードがシンプルで保守性が高くなる。

デメリット

  • アダプタを導入することでコードが増え、設計が複雑化する可能性がある。
  • アダプタの数が増える場合、管理が煩雑になる。
    • 異なるインターフェースを持つクラスが増えるごとに、それぞれに対応するアダプタを作成する必要があり、どのアダプタがどの対象に対応しているのかを把握する負担が増える。

議論

アダプタパターンは、異なるインターフェースの統一や既存コードの再利用が求められる場面で非常に有効です。
ただし、適用箇所が多すぎるとコードベースが複雑になるため、適切な範囲で利用することが重要です。


6. まとめ

アダプタパターンは、異なるインターフェースを持つクラスを統一するための重要なデザインパターンです。

このパターンを活用することで、既存コードや外部サービスを変更することなく、柔軟で拡張性の高いシステムを構築できます。

GitHubで編集を提案

Discussion