Python ストラテジーパターン

ストラテジーパターンは、実行時にアルゴリズムや動作を選択できるようにする行動デザインパターンです。簡単に言えば、ストラテジーパターンは異なるアルゴリズムを独立したクラスにカプセル化し、それらを相互に置き換えられるようにするものです。

核心理念

レストランで食事を注文する場面を想像してみてください。現金、クレジットカード、モバイル決済など、さまざまな支払い方法を選択できます。どの支払い方法を選んでも最終的に支払いは完了しますが、具体的な支払いプロセスが異なるだけです。これがストラテジーパターンの現実的な例です。

設計原則

ストラテジーパターンは以下の重要な原則に従います:

  • オープン・クローズド原則:拡張に対して開き、修正に対して閉じる
  • 単一責任原則:各ストラテジークラスは1つのアルゴリズムだけを担当する
  • 依存性逆転の原則:具象実装ではなく抽象に依存する

ストラテジーパターンの構造

UMLクラス図を通してストラテジーパターンの構成を理解しましょう:

コンポーネントの説明

Context(コンテキスト)

  • ストラテジーオブジェクトへの参照を保持する
  • ストラテジーを動的に切り替えられる
  • 処理をストラテジーオブジェクトに委譲する

Strategy(ストラテジーインターフェース)

  • サポートされるすべてのアルゴリズムの共通インターフェースを定義する
  • アルゴリズムを実行するメソッドを宣言する

ConcreteStrategy(具象ストラテジー)

  • ストラテジーインターフェースを実装する
  • 具体的なアルゴリズム実装を提供する

基本構文と実装

ストラテジーインターフェースの定義

Pythonでは、抽象基底クラス(ABC)を使用してストラテジーインターフェースを定義できます:

例

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    """支払いストラテジー抽象基底クラス"""
   
    @abstractmethod
    def pay(self, amount: float) -> bool:
        """支払い方法"""
        pass

具体的なストラテジーの実装

例

class CreditCardPayment(PaymentStrategy):
    """クレジットカード支払いストラテジー"""
   
    def __init__(self, card_number: str, expiry_date: str, cvv: str):
        self.card_number = card_number
        self.expiry_date = expiry_date
        self.cvv = cvv
   
    def pay(self, amount: float) -> bool:
        print(f"クレジットカードで {amount} 元を支払う")
        print(f"カード番号: {self.card_number}")
        # ここに実際の支払いロジックを記述する
        return True

class AlipayPayment(PaymentStrategy):
    """Alipay支払いストラテジー"""
   
    def __init__(self, account: str):
        self.account = account
   
    def pay(self, amount: float) -> bool:
        print(f"Alipayで {amount} 元を支払う")
        print(f"Alipayアカウント: {self.account}")
        # ここに実際の支払いロジックを記述する
        return True

class WechatPayment(PaymentStrategy):
    """WeChat Pay支払いストラテジー"""
   
    def __init__(self, openid: str):
        self.openid = openid
   
    def pay(self, amount: float) -> bool:
        print(f"WeChat Payで {amount} 元を支払う")
        print(f"WeChat OpenID: {self.openid}")
        # ここに実際の支払いロジックを記述する
        return True

コンテキストクラスの実装

例

class PaymentContext:
    """支払いコンテキストクラス"""
   
    def __init__(self, strategy: PaymentStrategy = None):
        self._strategy = strategy
   
    def set_strategy(self, strategy: PaymentStrategy):
        """支払いストラテジーを設定"""
        self._strategy = strategy
   
    def execute_payment(self, amount: float) -> bool:
        """支払いを実行"""
        if not self._strategy:
            raise ValueError("支払いストラテジーが設定されていません")
       
        return self._strategy.pay(amount)

完全な例:Eコマース決済システム

完全なEコマース決済システムを通して、ストラテジーパターンの実際の応用をデモンストレーションしましょう:

例

from abc import ABC, abstractmethod
from typing import Dict, Any

# ストラテジーインターフェース
class DiscountStrategy(ABC):
    """割引ストラテジーインターフェース"""
   
    @abstractmethod
    def calculate_discount(self, original_price: float) -> float:
        """割引後の価格を計算"""
        pass

# 具体的なストラテジーの実装
class NoDiscountStrategy(DiscountStrategy):
    """割引なしストラテジー"""
   
    def calculate_discount(self, original_price: float) -> float:
        return original_price

class PercentageDiscountStrategy(DiscountStrategy):
    """パーセント割引ストラテジー"""
   
    def __init__(self, percentage: float):
        if not 0 <= percentage <= 100:
            raise ValueError("割引率は0〜100の間でなければなりません")
        self.percentage = percentage
   
    def calculate_discount(self, original_price: float) -> float:
        discount_amount = original_price * (self.percentage / 100)
        return original_price - discount_amount

class FixedAmountDiscountStrategy(DiscountStrategy):
    """固定額割引ストラテジー"""
   
    def __init__(self, discount_amount: float):
        if discount_amount < 0:
            raise ValueError("割引額は負の数にできません")
        self.discount_amount = discount_amount
   
    def calculate_discount(self, original_price: float) -> float:
        return max(0, original_price - self.discount_amount)

class SeasonalDiscountStrategy(DiscountStrategy):
    """季節割引ストラテジー"""
   
    def __init__(self, base_discount: float, seasonal_multiplier: float):
        self.base_discount = base_discount
        self.seasonal_multiplier = seasonal_multiplier
   
    def calculate_discount(self, original_price: float) -> float:
        total_discount = self.base_discount * self.seasonal_multiplier
        return max(0, original_price - total_discount)

# コンテキストクラス
class ShoppingCart:
    """ショッピングカートクラス"""
   
    def __init__(self):
        self.items = []
        self._discount_strategy = NoDiscountStrategy()
   
    def add_item(self, item: str, price: float):
        """商品を追加"""
        self.items.append({"item": item, "price": price})
   
    def set_discount_strategy(self, strategy: DiscountStrategy):
        """割引ストラテジーを設定"""
        self._discount_strategy = strategy
   
    def calculate_total(self) -> float:
        """合計価格を計算"""
        total = sum(item["price"] for item in self.items)
        return self._discount_strategy.calculate_discount(total)
   
    def display_cart(self):
        """ショッピングカートの内容を表示"""
        print("ショッピングカートの内容:")
        for item in self.items:
            print(f" - {item['item']}: {item['price']}元")
       
        original_total = sum(item["price"] for item in self.items)
        final_total = self.calculate_total()
       
        print(f"元の価格: {original_total}元")
        print(f"割引後の価格: {final_total}元")
       
        if original_total != final_total:
            discount = original_total - final_total
            print(f"節約: {discount}元")

# 使用例
def main():
    # ショッピングカートを作成
    cart = ShoppingCart()
   
    # 商品を追加
    cart.add_item("Pythonプログラミング書", 89.0)
    cart.add_item("ワイヤレスマウス", 129.0)
    cart.add_item("メカニカルキーボード", 399.0)
   
    print("=== 割引なし ===")
    cart.set_discount_strategy(NoDiscountStrategy())
    cart.display_cart()
   
    print("\n=== 2割引 ===")
    cart.set_discount_strategy(PercentageDiscountStrategy(20))  # 2割引
    cart.display_cart()
   
    print("\n=== 50元引き ===)
    cart.set_discount_strategy(FixedAmountDiscountStrategy(50))
    cart.display_cart()
   
    print("\n=== 季節割引 ===)
    cart.set_discount_strategy(SeasonalDiscountStrategy(30, 1.5))  # 基本割引30、季節係数1.5
    cart.display_cart()

if __name__ == "__main__":
    main()

上記のコードを実行すると、次の出力が表示されます:

=== 无折扣 ===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 617.0元

=== 8折优惠 ===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 493.6元
节省: 123.4元

=== 满减优惠(减50元)===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 567.0元
节省: 50.0元

=== 季节性优惠 ===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 572.0元
节省: 45.0元

ストラテジーパターンの応用

1. ストラテジーファクトリーパターン

ファクトリーパターンを組み合わせてストラテジーの生成を管理します:

例

class DiscountStrategyFactory:
    """割引ストラテジーファクトリー"""
   
    @staticmethod
    def create_strategy(strategy_type: str, **kwargs) -> DiscountStrategy:
        """割引ストラテジーを作成"""
        strategies = {
            "no_discount": NoDiscountStrategy,
            "percentage": PercentageDiscountStrategy,
            "fixed_amount": FixedAmountDiscountStrategy,
            "seasonal": SeasonalDiscountStrategy
        }
       
        if strategy_type not in strategies:
            raise ValueError(f"サポートされていないストラテジータイプ: {strategy_type}")
       
        return strategies[strategy_type](**kwargs)

# ファクトリーパターンを使用
factory = DiscountStrategyFactory()

# 異なるストラテジーを作成
strategy1 = factory.create_strategy("percentage", percentage=15)  # 15%オフ
strategy2 = factory.create_strategy("fixed_amount", discount_amount=100)  # 100元引き

2. 動的ストラテジー選択

条件に応じてストラテジーを動的に選択します:

例

class DynamicDiscountSelector:
    "動的割引セレクター"
   
    @staticmethod
    def select_strategy(user_type: str, total_amount: float) -> DiscountStrategy:
        """ユーザータイプと合計金額に基づいてストラテジーを選択"""
        if user_type == "vip":
            if total_amount > 500:
                return PercentageDiscountStrategy(25)  # VIP 500以上で25%オフ
            else:
                return PercentageDiscountStrategy(15)  # VIP 15%オフ
        elif user_type == "normal":
            if total_amount > 300:
                return FixedAmountDiscountStrategy(30)  # 一般ユーザー 300以上で30引き
            else:
                return NoDiscountStrategy()
        else:
            return NoDiscountStrategy()

# 動的選択を使用
cart = ShoppingCart()
cart.add_item("商品A", 200)
cart.add_item("商品B", 150)

selector = DynamicDiscountSelector()
strategy = selector.select_strategy("vip", cart.calculate_total())
cart.set_discount_strategy(strategy)
cart.display_cart()

ストラテジーパターンの利点と適用シーン

利点の比較

特性 従来方式 ストラテジーパターン
拡張性 既存コードの修正が必要 ストラテジークラスを追加するだけでよい
保守性 コードの結合度が高い 責務が分離され、保守が容易
柔軟性 実行時にアルゴリズムを切り替えにくい ストラテジーを動的に切り替えられる
テスト容易性 アルゴリズムを単独でテストしにくい 各ストラテジーを独立してテストできる

適用シーン

  1. 複数のアルゴリズムのバリエーション:似たようなクラスが複数あり、一部の動作だけが異なる場合
  2. 条件分岐を避ける:多数の条件分岐(if-else や switch)の使用を避けたい場合
  3. 実行時のアルゴリズム選択:実行時に異なるアルゴリズムを選択する必要がある場合
  4. アルゴリズムのカプセル化:アルゴリズムの詳細を、それを使用するクライアントから分離したい場合

不適切なシーン

  1. 単純なアルゴリズム:変化がほとんどないアルゴリズムが1つか2つしかない場合、過剰設計になる可能性がある
  2. クライアントがストラテジーの詳細を知る必要がある:クライアントがストラテジーの具体的な実装を知らなければならない場合
  3. ストラテジーの数が多すぎる:ストラテジークラスの数が爆発的に増える場合は、他のパターンを検討する

ベストプラクティスと注意事項

コード構成の提案

例

# 推奨ファイル構成
project/
├── strategies/
│   ├── __init__.py
│   ├── base_strategy.py      # 基本戦略インターフェース
│   ├── discount_strategies.py # 割引関連戦略
│   └── payment_strategies.py # 支払い関連戦略
├── contexts/
│   ├── __init__.py
│   └── shopping_cart.py      # コンテキストクラス
└── main.py

エラーハンドリング

例

class SafeDiscountStrategy(DiscountStrategy):
    """エラーハンドリング付き割引戦略"""
   
    def __init__(self, base_strategy: DiscountStrategy, fallback_strategy: DiscountStrategy = None):
        self.base_strategy = base_strategy
        self.fallback_strategy = fallback_strategy or NoDiscountStrategy()
   
    def calculate_discount(self, original_price: float) -> float:
        try:
            return self.base_strategy.calculate_discount(original_price)
        except Exception as e:
            print(f"割引計算エラー: {e}、代替戦略を使用します")
            return self.fallback_strategy.calculate_discount(original_price)

パフォーマンスの考慮事項

パフォーマンスが重要なシナリオでは、以下の最適化を検討できます:

例

from functools import lru_cache

class CachedDiscountStrategy(DiscountStrategy):
    """キャッシュ付き割引戦略"""
   
    def __init__(self, base_strategy: DiscountStrategy):
        self.base_strategy = base_strategy
   
    @lru_cache(maxsize=128)
    def calculate_discount(self, original_price: float) -> float:
        return self.base_strategy.calculate_discount(original_price)

実践演習

演習1:ソートストラテジーの実装

複数のソートアルゴリズムをサポートするソーターを実装します:

例

from abc import ABC, abstractmethod
from typing import List

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: List) -> List:
        pass

# TODO: バブルソート戦略を実装
class BubbleSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # あなたの実装コード
        pass

# TODO: クイックソート戦略を実装
class QuickSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # あなたの実装コード
        pass

# TODO: マージソート戦略を実装
class MergeSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # あなたの実装コード
        pass

class Sorter:
    def __init__(self, strategy: SortStrategy = None):
        self._strategy = strategy
   
    def set_strategy(self, strategy: SortStrategy):
        self._strategy = strategy
   
    def sort_data(self, data: List) -> List:
        if not self._strategy:
            raise ValueError("ソート戦略が設定されていません")
        return self._strategy.sort(data)

# あなたの実装をテスト
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSortStrategy())
result = sorter.sort_data(data)
print(f"ソート結果: {result}")

演習2:ファイル圧縮ストラテジー

複数の圧縮形式をサポートするファイル圧縮器を設計します:

例

from abc import ABC, abstractmethod

class CompressionStrategy(ABC):
    @abstractmethod
    def compress(self, file_path: str) -> str:
        pass
   
    @abstractmethod
    def decompress(self, file_path: str) -> str:
        pass

# TODO: ZIP圧縮戦略を実装
class ZipCompressionStrategy(CompressionStrategy):
    def compress(self, file_path: str) -> str:
        # あなたの実装コード
        pass
   
    def decompress(self, file_path: str) -> str:
        # あなたの実装コード
        pass

# TODO: GZIP圧縮戦略を実装
class GzipCompressionStrategy(CompressionStrategy):
    def compress(self, file_path: str) -> str:
        # あなたの実装コード
        pass
   
    def decompress(self, file_path: str) -> str:
        # あなたの実装コード
        pass

class FileCompressor:
    def __init__(self, strategy: CompressionStrategy = None):
        self._strategy = strategy
   
    def set_strategy(self, strategy: CompressionStrategy):
        self._strategy = strategy
   
    def compress_file(self, file_path: str) -> str:
        if not self._strategy:
            raise ValueError("圧縮戦略が設定されていません")
        return self._strategy.compress(file_path)
   
    def decompress_file(self, file_path: str) -> str:
        if not self._strategy:
            raise ValueError("圧縮戦略が設定されていません")
        return self._strategy.decompress(file_path)

まとめ

ストラテジーパターンはPythonで非常に実用的なデザインパターンであり、アルゴリズムを独立したストラテジークラスにカプセル化することで、優れた拡張性と柔軟性を提供します。この記事の学習を通じて、以下のことができるようになるはずです:

  1. ストラテジーパターンの核心的な概念と適用シナリオを理解する
  2. ストラテジーパターンの基本的な実装方法を習得する
  3. 実際のプロジェクトでストラテジーパターンを適切に適用する
  4. ストラテジーパターンのよくある誤用を避ける
その他の拡張