Python | OOP:デザインパターン(Strategy)

Python Python
スポンサーリンク
  1. 概要(Strategyは「アルゴリズムを入れ替える差し替え口」を用意する設計)
  2. 基本構造(インターフェース・具体戦略・コンテキスト)
    1. 戦略インターフェースを決める(「この名前で呼ぶ」を固定)
    2. 具体戦略を実装する(アルゴリズムをカプセル化)
    3. コンテキストが戦略を“持って使う”(委譲して呼ぶ)
  3. 重要ポイントの深掘り(分岐排除・実行時切替・拡張容易性)
    1. 分岐だらけのコードをやめる(責務を戦略へ委ねる)
    2. 実行時に差し替えできる(状況に応じた動的切替)
    3. 新しいアルゴリズムの追加が安全(既存コードに手を入れない)
  4. 実務例(データ前処理・ソート・課金ルール)
    1. データ前処理の戦略(入力の揺れに対応)
    2. ソート戦略(アルゴリズムの差し替え)
    3. 課金・割引ルール(ビジネスロジックの差し替え口)
  5. 設計のコツ(インターフェースの明確化・依存性注入・テスト容易性)
    1. インターフェースの契約を明示し、意味を崩さない
    2. 依存性注入で差し替え可能に保つ
    3. テストダブルを戦略として注入する
  6. よくある落とし穴と回避(過剰抽象・戦略の責務過多・分岐の亡霊)
    1. 戦略に責務を詰め込みすぎない
    2. インターフェースの逸脱で互換性が壊れる
  7. 例題(テキスト正規化パイプ+選択可能なルールセット)
    1. ルールセットを戦略化して差し替える
  8. まとめ(Strategyは“分岐を戦略へ追い出す”ことで柔軟さと保守性を手に入れる)

概要(Strategyは「アルゴリズムを入れ替える差し替え口」を用意する設計)

Strategy(ストラテジー)パターンは、処理のアルゴリズムを独立した“戦略オブジェクト”に切り出し、実行時に差し替えられるようにする設計です。これにより、if/elifの分岐だらけのコードを避け、柔軟で保守しやすい構造になります。登場人物は、戦略のインターフェース(共通メソッドの約束)、具体的戦略のクラス群、そして戦略を使う側のコンテキスト(委譲して利用する側)です。


基本構造(インターフェース・具体戦略・コンテキスト)

戦略インターフェースを決める(「この名前で呼ぶ」を固定)

戦略は「同じメソッド名・同じ意味」で呼べるように、インターフェースを定義します。Pythonでは抽象基底クラス(ABC)で表現するのがわかりやすいです。

from abc import ABC, abstractmethod

class DiscountStrategy(ABC):
    @abstractmethod
    def apply(self, price: float) -> float:
        """価格に割引を適用して返す"""
        pass
Python

具体戦略を実装する(アルゴリズムをカプセル化)

同じインターフェースに従って、異なるアルゴリズムをクラスで用意します。切り替えは差し替えだけで済みます。

class NoDiscount(DiscountStrategy):
    def apply(self, price: float) -> float:
        return price

class TenPercentOff(DiscountStrategy):
    def apply(self, price: float) -> float:
        return price * 0.9
Python

コンテキストが戦略を“持って使う”(委譲して呼ぶ)

利用側(コンテキスト)は戦略を受け取り、処理の一部を委譲します。これで拡張やテストが楽になります。

class Checkout:
    def __init__(self, strategy: DiscountStrategy):
        self.strategy = strategy  # 差し替え可能な窓口

    def total(self, base: float) -> float:
        return self.strategy.apply(base)

print(Checkout(NoDiscount()).total(100))     # 100.0
print(Checkout(TenPercentOff()).total(100))  # 90.0
Python

重要ポイントの深掘り(分岐排除・実行時切替・拡張容易性)

分岐だらけのコードをやめる(責務を戦略へ委ねる)

if/elifでアルゴリズムを選ぶスタイルは、分岐の増加とともに保守が困難になります。Strategyはアルゴリズムをクラスへ分離し、選択は「どの戦略を渡すか」に置き換えるため、読みやすさと拡張性が向上します。

実行時に差し替えできる(状況に応じた動的切替)

Strategyは実行時にコンストラクタ注入やメソッド注入で切り替え可能です。これにより、設定・ユーザー選択・環境(開発/本番)に応じたアルゴリズム変更をコード改修なしで行えます。

新しいアルゴリズムの追加が安全(既存コードに手を入れない)

新戦略はインターフェースに従う限り、既存のコンテキストを変更せずに差し込めます。変更が局所化され、回帰リスクが低く保たれます。


実務例(データ前処理・ソート・課金ルール)

データ前処理の戦略(入力の揺れに対応)

処理ごとに挙動が異なる前処理を戦略化すると、選択とテストが容易になります。

from abc import ABC, abstractmethod
from datetime import datetime

class ProcessStrategy(ABC):
    @abstractmethod
    def process(self, data: dict) -> dict:
        pass

class DateNormalize(ProcessStrategy):
    def process(self, data: dict) -> dict:
        if "date" in data:
            dt = datetime.fromisoformat(data["date"])
            data["date"] = dt.strftime("%Y-%m-%d")
        return data

class NameNormalize(ProcessStrategy):
    def process(self, data: dict) -> dict:
        data["name"] = (data.get("name") or "unknown").strip()
        return data

class Processor:
    def __init__(self, strategy: ProcessStrategy):
        self.strategy = strategy
    def run(self, d: dict) -> dict:
        return self.strategy.process(d)
Python

ソート戦略(アルゴリズムの差し替え)

ソートアルゴリズムをクラス化し、データや要件に応じて切り替えると、条件分岐を排除できます。

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, xs: list[int]) -> list[int]: ...

class QuickSort(SortStrategy):
    def sort(self, xs: list[int]) -> list[int]:
        return sorted(xs)  # 簡略化

class ReverseSort(SortStrategy):
    def sort(self, xs: list[int]) -> list[int]:
        return sorted(xs, reverse=True)

class Sorter:
    def __init__(self, strategy: SortStrategy):
        self.strategy = strategy
    def run(self, xs: list[int]) -> list[int]:
        return self.strategy.sort(xs)
Python

課金・割引ルール(ビジネスロジックの差し替え口)

価格計算のルールを戦略として切り出せば、キャンペーンや地域差の追加が容易です。

class PriceRule(ABC):
    @abstractmethod
    def calc(self, amount: float) -> float: ...

class Tax10(PriceRule):
    def calc(self, amount: float) -> float:
        return amount * 1.10

class Campaign(PriceRule):
    def calc(self, amount: float) -> float:
        return amount * 0.85

class Billing:
    def __init__(self, rule: PriceRule):
        self.rule = rule
    def total(self, base: float) -> float:
        return self.rule.calc(base)
Python

設計のコツ(インターフェースの明確化・依存性注入・テスト容易性)

インターフェースの契約を明示し、意味を崩さない

メソッド名・引数・戻り値の意味を固定し、具体戦略では契約を守ります。拡張は“追加の戦略”で行い、既存のシグネチャを変えないのが安全です。

依存性注入で差し替え可能に保つ

コンテキストは具体クラスを直接生成せず、外から戦略を渡してもらいます。DIにすることで、切り替え・テスト・設定駆動が自然にできます。

テストダブルを戦略として注入する

戦略は独立してテストでき、コンテキストにはフェイク戦略を注入して素早く検証できます。副作用を持つ戦略(I/O)はモックで置き換えて失敗パスの検証を容易にします。


よくある落とし穴と回避(過剰抽象・戦略の責務過多・分岐の亡霊)

戦略に責務を詰め込みすぎない

戦略は「置き換えたいアルゴリズム」に絞ります。前後の検証・I/O・ログまで抱えると再利用が難しくなります。必要ならテンプレートメソッドやミドルレイヤーへ分離します。

インターフェースの逸脱で互換性が壊れる

メソッド名や戻り値の形を戦略ごとに変えると、コンテキストの利用手順が壊れます。意味の追加は戻り値のフィールド追加など“後方互換”を守る形にします。

###「結局ifに戻る」症候群 戦略選択をコンテキスト内部でifで決めてしまうと、再び分岐の渋滞になります。選択は上位(工場や設定)に寄せ、コンテキストは“渡された戦略を使う”だけにします。


例題(テキスト正規化パイプ+選択可能なルールセット)

ルールセットを戦略化して差し替える

複数の前処理をまとめた“ルールセット”を戦略として持たせると、用途ごとに切替可能になります。

from abc import ABC, abstractmethod

class CleanRule(ABC):
    @abstractmethod
    def apply(self, row: dict) -> dict: ...

class BasicClean(CleanRule):
    def apply(self, row: dict) -> dict:
        return {"id": str(row.get("id", "")),
                "name": (row.get("name") or "unknown").strip()}

class StrictClean(CleanRule):
    def apply(self, row: dict) -> dict:
        uid = row.get("id")
        if uid is None:
            raise ValueError("id必須")
        name = (row.get("name") or "").strip()
        return {"id": str(uid), "name": name or "unknown"}

class Cleaner:
    def __init__(self, rule: CleanRule):
        self.rule = rule
    def run(self, rows: list[dict]) -> list[dict]:
        return [self.rule.apply(r) for r in rows]

rows = [{"id": 1, "name": "  Taro "}, {"name": None}]
print(Cleaner(BasicClean()).run(rows))
Python

まとめ(Strategyは“分岐を戦略へ追い出す”ことで柔軟さと保守性を手に入れる)

Strategyパターンは、アルゴリズムを戦略オブジェクトに切り出し、実行時に差し替え可能にする設計です。インターフェースで呼び方を統一し、具体戦略を追加してもコンテキストを変えない。選択は上位で行い、依存性注入で差し替えとテストを容易にする。これを徹底すれば、初心者でも“柔軟で壊れにくい”コードへ自然に踏み出せます。

タイトルとURLをコピーしました