多重 if-elif を排除するストラテジーパターン(Python)

多重 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 のままでよい。過度な抽象化は避けるべきだ。