多重 if-elif は、コマンドパターンやステートマシンでよく見かける構造だ。条件が増えるたびに分岐が伸び、保守が困難になる。ストラテジーパターンを使えば、条件分岐をポリモーフィズムに置き換え、拡張性の高い設計にできる。
問題:肥大化する if-elif
支払い処理を例に考える。支払い方法ごとに処理が異なるとき、if-elif で分岐すると以下のようになる。
# 問題のあるコード:if-elif の連鎖
def process_payment(method, amount, details):
if method == "credit_card":
# クレジットカード処理
card_number = details["card_number"]
expiry = details["expiry"]
# カード会社 API を呼ぶ...
return {"status": "success", "transaction_id": "CC123"}
elif method == "paypal":
# PayPal 処理
email = details["email"]
# PayPal API を呼ぶ...
return {"status": "success", "transaction_id": "PP456"}
elif method == "bank_transfer":
# 銀行振込処理
account = details["account"]
# 銀行 API を呼ぶ...
return {"status": "pending", "reference": "BT789"}
elif method == "crypto":
# 暗号通貨処理
wallet = details["wallet"]
# ブロックチェーン処理...
return {"status": "success", "tx_hash": "0xabc"}
else:
raise ValueError(f"Unknown payment method: {method}")
この設計には複数の問題がある。新しい支払い方法を追加するたびに関数を修正する必要があり、各支払い方法のロジックが 1 つの関数に詰め込まれている。テストも書きにくい。
ストラテジーパターンの導入
ストラテジーパターンでは、各アルゴリズム(ここでは支払い方法)を独立したクラスに分離する。
from abc import ABC, abstractmethod
# 抽象基底クラス(ストラテジーインターフェース)
class PaymentStrategy(ABC):
@abstractmethod
def process(self, amount, details):
pass
# 具体的なストラテジー
class CreditCardPayment(PaymentStrategy):
def process(self, amount, details):
card_number = details["card_number"]
expiry = details["expiry"]
# カード会社 API を呼ぶ...
return {"status": "success", "transaction_id": "CC123"}
class PayPalPayment(PaymentStrategy):
def process(self, amount, details):
email = details["email"]
# PayPal API を呼ぶ...
return {"status": "success", "transaction_id": "PP456"}
class BankTransferPayment(PaymentStrategy):
def process(self, amount, details):
account = details["account"]
# 銀行 API を呼ぶ...
return {"status": "pending", "reference": "BT789"}
class CryptoPayment(PaymentStrategy):
def process(self, amount, details):
wallet = details["wallet"]
# ブロックチェーン処理...
return {"status": "success", "tx_hash": "0xabc"}
コンテキストクラスの実装
ストラテジーを利用する側(コンテキスト)を実装する。
class PaymentProcessor:
def __init__(self):
self._strategies = {}
def register_strategy(self, method_name, strategy):
self._strategies[method_name] = strategy
def process_payment(self, method, amount, details):
strategy = self._strategies.get(method)
if strategy is None:
raise ValueError(f"Unknown payment method: {method}")
return strategy.process(amount, details)
# セットアップ
processor = PaymentProcessor()
processor.register_strategy("credit_card", CreditCardPayment())
processor.register_strategy("paypal", PayPalPayment())
processor.register_strategy("bank_transfer", BankTransferPayment())
processor.register_strategy("crypto", CryptoPayment())
# 使用
result = processor.process_payment("paypal", 100, {"email": "user@example.com"})
print(result) # {"status": "success", "transaction_id": "PP456"}
デコレータによる自動登録
ストラテジーの登録を自動化するとさらに便利だ。
class PaymentProcessor:
_strategies = {}
@classmethod
def register(cls, method_name):
def decorator(strategy_class):
cls._strategies[method_name] = strategy_class()
return strategy_class
return decorator
@classmethod
def process_payment(cls, method, amount, details):
strategy = cls._strategies.get(method)
if strategy is None:
raise ValueError(f"Unknown payment method: {method}")
return strategy.process(amount, details)
# デコレータで登録
@PaymentProcessor.register("credit_card")
class CreditCardPayment(PaymentStrategy):
def process(self, amount, details):
return {"status": "success", "method": "credit_card"}
@PaymentProcessor.register("paypal")
class PayPalPayment(PaymentStrategy):
def process(self, amount, details):
return {"status": "success", "method": "paypal"}
# 使用(登録は自動的に完了している)
result = PaymentProcessor.process_payment("credit_card", 100, {})
関数ベースのストラテジー
クラスを定義するほどでもない場合、関数をストラテジーとして使うこともできる。
from typing import Callable, Dict, Any
# 型エイリアス
PaymentHandler = Callable[[float, Dict[str, Any]], Dict[str, Any]]
class PaymentProcessor:
def __init__(self):
self._handlers: Dict[str, PaymentHandler] = {}
def register(self, method_name: str):
def decorator(func: PaymentHandler) -> PaymentHandler:
self._handlers[method_name] = func
return func
return decorator
def process(self, method: str, amount: float, details: Dict[str, Any]):
handler = self._handlers.get(method)
if handler is None:
raise ValueError(f"Unknown payment method: {method}")
return handler(amount, details)
processor = PaymentProcessor()
@processor.register("credit_card")
def handle_credit_card(amount, details):
return {"status": "success", "amount": amount}
@processor.register("paypal")
def handle_paypal(amount, details):
return {"status": "success", "email": details.get("email")}
# 使用
result = processor.process("paypal", 50, {"email": "user@example.com"})
ストラテジーパターンの利点
開放閉鎖原則への準拠
新しい支払い方法を追加するとき、既存のコードを修正せずに新しいクラスを追加するだけで済む。
テスト容易性
各ストラテジーを独立してテストできる。モックに差し替えるのも容易。
実践例:ロギングストラテジー
もう 1 つの例として、ログ出力先を切り替えるストラテジーを見てみよう。
from abc import ABC, abstractmethod
from datetime import datetime
class LogStrategy(ABC):
@abstractmethod
def write(self, message: str):
pass
class ConsoleLog(LogStrategy):
def write(self, message):
print(f"[CONSOLE] {message}")
class FileLog(LogStrategy):
def __init__(self, filename):
self.filename = filename
def write(self, message):
with open(self.filename, "a") as f:
f.write(f"{datetime.now()} - {message}\n")
class NullLog(LogStrategy):
def write(self, message):
pass # 何もしない
class Logger:
def __init__(self, strategy: LogStrategy):
self._strategy = strategy
def set_strategy(self, strategy: LogStrategy):
self._strategy = strategy
def log(self, message):
self._strategy.write(message)
# 実行時に切り替え可能
logger = Logger(ConsoleLog())
logger.log("Starting application")
logger.set_strategy(FileLog("app.log"))
logger.log("Switched to file logging")
logger.set_strategy(NullLog())
logger.log("This won't appear anywhere")
if-elif との比較
if-elif アプローチ
シンプルで理解しやすい。分岐が少ない場合に適している。分岐が増えると保守困難。
ストラテジーパターン
初期コストは高いが拡張性に優れる。各ストラテジーが独立してテスト可能。分岐が多い場合に有効。
いつストラテジーパターンを使うべきか
すべての if-elif をストラテジーパターンに置き換える必要はない。以下の条件に当てはまるなら検討する価値がある。
分岐が 4〜5 個以上ある
分岐が今後も増える可能性が高い
各分岐のロジックが複雑で、独立してテストしたい
実行時にアルゴリズムを切り替える必要がある
逆に、分岐が 2〜3 個で今後も増えないなら、シンプルな if-elif のままでよい。過度な抽象化は避けるべきだ。