スポンサーリンク

Pythonで学ぶStrategyパターン|if文地獄から抜け出す設計思考

クラス設計・OOP入門
  1. はじめに
  2. 1. なぜ「if文地獄」から抜け出せないのか
    1. 保守性が下がる理由
    2. OCP(開放閉鎖の原則)に反しやすい
    3. テストもしづらくなる
  3. 2. Strategyパターンとは何か(考え方を掴む)
    1. 「何をするか」と「どうやるか」を分ける
    2. 継承ではなく「委譲」を使う理由
    3. Template Methodパターンとの違い
  4. 3. Strategyパターンの基本構造(Python視点)
    1. Contextの役割
    2. Strategy(戦略インターフェース)
    3. Concrete Strategy(具体的戦略)
    4. 重要なのは「ContextがStrategyを知らない」こと
  5. 4. Pythonでの実装例(if文からStrategyへ)
    1. Before:if文で処理を切り替えるコード
    2. After:Strategyパターンを適用
      1. Strategyインターフェース
      2. Concrete Strategy
      3. Context
      4. 利用側(クライアントコード)
    3. よくある失敗パターン
  6. 5. PythonらしいStrategyの書き方
    1. Protocolを使った疎結合なStrategy
    2. __call__ を使って関数のように扱う
    3. 関数ベースのStrategy
    4. クロージャで状態を持たせる
  7. 6. 実務での活用シーン
    1. 決済方法の切り替え
    2. データ出力形式の切り替え
    3. ゲームAIの行動パターン
    4. 機械学習・データ処理パイプライン
    5. ID生成・ルールベース処理
  8. 7. Strategyパターンのメリット・デメリット
    1. メリット
      1. 拡張しやすい設計になる
      2. 単体テストが書きやすい
      3. 「継承より委譲」を自然に実践できる
    2. デメリット・注意点
      1. クラス(またはStrategy)が増える
      2. 全体像が見えにくくなることがある
      3. 使う側に選択責任が生まれる
  9. まとめ
    1. あわせて読みたい
    2. 参考文献
  10. よくある質問(Q&A)
    1. 関連投稿:

はじめに

Pythonで開発をしていると、最初はシンプルだったはずのコードに、 いつの間にか if / elif がずらっと並んでいる…そんな経験、ありませんか?

機能追加のたびに条件分岐が増え、
「ここ直したら、別の処理が壊れそうで怖い」
「テストを書くのも大変…」
そんなモヤモヤを感じ始めたら、それは設計を見直すサインかもしれません。

本記事では、そのような “if文地獄”から抜け出すための考え方として、 Strategy(ストラテジー)パターンをPython視点で解説します。

Strategyパターンは、
「処理をどう切り替えるか」ではなく、
「変更にどう耐える設計にするか」を考えるためのデザインパターンです。

クラスベースの王道な実装はもちろん、
Pythonならではの Protocol関数ベースのStrategy まで含めて、 実務で使える形を意識して紹介していきます。

「if文を消すこと」がゴールではありません。
拡張しやすく、テストしやすく、安心して変更できるコードを 書けるようになることがゴールです。

それでは、まずは
なぜ私たちは if文地獄にハマってしまうのか から、一緒に見ていきましょう 😊




1. なぜ「if文地獄」から抜け出せないのか

多くのPythonコードで見かけるのが、機能追加のたびに if / elif が増えていく構造です。 最初は読みやすかった処理も、条件が増えるにつれて一気に見通しが悪くなっていきます。

たとえば、処理内容を条件で切り替えるコードは、こんな形になりがちです。


if mode == "a":
    do_a()
elif mode == "b":
    do_b()
elif mode == "c":
    do_c()
else:
    raise ValueError("unknown mode")
  

一見すると問題なさそうですが、
モードが増えるたびにこの関数そのものを修正し続ける必要があります。

保守性が下がる理由

if文が増殖すると、次のような問題が起きやすくなります。

  • どの条件がどの処理に対応しているのか把握しづらい
  • 一部を修正したつもりが、別の分岐に影響してしまう
  • 処理の追加=既存コードの編集になる

これは「変更に弱い構造」の典型例です。 動いているコードを毎回触らなければならないため、バグを生む心理的コストも高くなります。

OCP(開放閉鎖の原則)に反しやすい

オブジェクト指向には 「拡張に対して開かれ、修正に対して閉じているべき」 という設計原則(OCP)があります。

しかし、if文による分岐構造では、 新しい処理を追加するたびに既存の関数を修正する必要があります。 つまり、「拡張=修正」になってしまうのです。

テストもしづらくなる

条件分岐が大量に詰め込まれた関数は、テストも書きにくくなります。

  • 特定の分岐だけを切り出してテストできない
  • 分岐の組み合わせが増え、テストケースが爆発する
  • 1つの変更で多くのテストが壊れる

このように、if文地獄は可読性・拡張性・テスト容易性を じわじわと奪っていきます。

では、こうした問題をどう設計で解決するのか。
次のセクションでは、Strategyパターンの考え方から整理していきます。




2. Strategyパターンとは何か(考え方を掴む)

Strategy(ストラテジー)パターンは、 「処理のやり方(アルゴリズム)を部品として切り出し、あとから差し替えられるようにする」 ための設計パターンです。

ポイントは、条件分岐を減らすこと自体ではなく、
変更されやすいロジックを、変更されにくい部分から分離することにあります。

「何をするか」と「どうやるか」を分ける

Strategyパターンでは、次の2つを明確に分離します。

  • 何をするか:処理の流れや役割(Context)
  • どうやるか:具体的な処理内容(Strategy)

Contextは「処理を実行する責務」だけを持ち、 実際の中身はStrategyに丸投げ(委譲)します。

これにより、Contextのコードはほとんど変更せずに、 新しい処理を追加できるようになります。

継承ではなく「委譲」を使う理由

振る舞いを切り替えたいとき、
つい「継承で分ける」設計を考えがちです。

しかし、継承ベースの設計は次のような問題を抱えやすくなります。

  • クラス階層が深くなり、把握しづらい
  • 実行時に振る舞いを切り替えにくい
  • 一部の変更が派生クラス全体に影響する

Strategyパターンは、 「has-a(持っている)」関係で振る舞いを差し替えます。 これが、柔軟さの正体です。

Template Methodパターンとの違い

Strategyとよく比較されるのが、Template Methodパターンです。

Template Methodは継承によって処理の一部を差し替えますが、 Strategyはオブジェクトの差し替えで振る舞いを変更します。

そのためStrategyは、 実行時に処理を切り替えたいケースや、 変更頻度の高いロジックに特に向いています。




3. Strategyパターンの基本構造(Python視点)

Strategyパターンは、構造自体はとてもシンプルです。 重要なのは「クラスの数」ではなく、責務の分け方にあります。

基本となる構成要素は、次の3つです。

  • Context(コンテキスト)
  • Strategy(戦略インターフェース)
  • Concrete Strategy(具体的な戦略)

Contextの役割

Contextは、処理の流れや目的を管理する側のクラスです。 ただし、具体的な処理内容は自分では実装しません

ContextはStrategyを「知っている」だけで、 中身がどう実装されているかは気にしません。


class Context:
    def __init__(self, strategy):
        self._strategy = strategy

    def execute(self, data):
        return self._strategy.execute(data)
  

Contextがやっているのは、 処理をStrategyに委譲しているだけです。

Strategy(戦略インターフェース)

Strategyは、すべての戦略に共通する メソッドの形(契約)を定義します。

Pythonでは abc.ABC を使って 抽象基底クラスとして定義するのが王道です。


from abc import ABC, abstractmethod

class Strategy(ABC):
    @abstractmethod
    def execute(self, data):
        pass
  

Contextは、このインターフェースだけを見て動作します。 具体的な実装には依存しません。

Concrete Strategy(具体的戦略)

Concrete Strategyは、 Strategyインターフェースを実装したクラスです。

if文で分岐していたロジックを、 それぞれ独立したクラスとして切り出します。


class StrategyA(Strategy):
    def execute(self, data):
        return f"A: {data}"

class StrategyB(Strategy):
    def execute(self, data):
        return f"B: {data}"
  

新しい処理を追加したい場合は、 新しいStrategyクラスを作るだけで済みます。 Contextの修正は不要です。

重要なのは「ContextがStrategyを知らない」こと

Strategyパターンで最も大切なのは、 Contextが具体的な戦略クラス名を知らないことです。

Contextが if isinstance(strategy, StrategyA) のようなコードを書き始めたら、 それはもうStrategyパターンではありません。

Contextは「戦略を実行する」だけ。
それ以上の判断はしない。

次のセクションでは、 実際に if文ベースのコードを Strategyパターンへリファクタリングする流れを、 Before / After で見ていきます。




4. Pythonでの実装例(if文からStrategyへ)

ここでは、よくある if / elif ベースの実装を出発点にして、 Strategyパターンへリファクタリングしていく流れを見ていきます。

Before:if文で処理を切り替えるコード

まずは、ありがちな実装例です。


def process(data, mode):
    if mode == "upper":
        return data.upper()
    elif mode == "lower":
        return data.lower()
    elif mode == "title":
        return data.title()
    else:
        raise ValueError("unknown mode")
  

処理が少ないうちは問題ありませんが、 モードが増えるたびに この関数を修正し続ける必要があります。

さらに、

  • テストは mode ごとに分岐を意識しなければならない
  • 処理の追加が関数全体の変更になる

という状態になりやすいです。

After:Strategyパターンを適用

では、この処理をStrategyパターンで分離してみましょう。

Strategyインターフェース


from abc import ABC, abstractmethod

class TextStrategy(ABC):
    @abstractmethod
    def execute(self, data: str) -> str:
        pass
  

Concrete Strategy


class UpperStrategy(TextStrategy):
    def execute(self, data: str) -> str:
        return data.upper()

class LowerStrategy(TextStrategy):
    def execute(self, data: str) -> str:
        return data.lower()

class TitleStrategy(TextStrategy):
    def execute(self, data: str) -> str:
        return data.title()
  

Context


class TextProcessor:
    def __init__(self, strategy: TextStrategy):
        self._strategy = strategy

    def process(self, data: str) -> str:
        return self._strategy.execute(data)
  

利用側(クライアントコード)


processor = TextProcessor(UpperStrategy())
result = processor.process("hello world")
  

この構造にすると、
新しい処理を追加したい場合は Strategyクラスを1つ追加するだけで済みます。

Contextである TextProcessor は一切変更しません。 これが「拡張に強い設計」です。

よくある失敗パターン

  • Context内で if isinstance() を使ってしまう
  • Strategyが巨大化し、結局1クラスにロジックが集まる
  • 小さすぎる処理まで無理にStrategy化してしまう

Strategyパターンは万能ではありません。
「変更頻度が高いか?」 「処理の切り替えが本質か?」 を意識して使うことが大切です。

次のセクションでは、 ここまでのクラスベース実装を踏まえた上で、 PythonらしいStrategyの書き方を紹介します。




5. PythonらしいStrategyの書き方

ここまで、クラス+抽象基底クラスを使った 「教科書どおり」のStrategyパターンを見てきました。

ただしPythonは、必ずしもクラス継承にこだわらなくてもよい言語です。 実務では、もっと軽量で柔軟な書き方が選ばれることも少なくありません。

このセクションでは、 Pythonだからこそ選べるStrategyの実装スタイルを紹介します。

Protocolを使った疎結合なStrategy

typing.Protocol を使うと、 明示的な継承なしで「このメソッドを持っていればOK」 という契約を定義できます。


from typing import Protocol

class TextStrategy(Protocol):
    def execute(self, data: str) -> str:
        ...
  

この場合、Strategyクラスは Protocolを継承する必要すらありません


class UpperStrategy:
    def execute(self, data: str) -> str:
        return data.upper()
  

静的型チェックの恩恵を受けつつ、 クラス間の依存関係を最小限に抑えられます。

__call__ を使って関数のように扱う

Strategyを「処理そのもの」として扱いたい場合、 __call__ を実装するのも便利です。


class UpperStrategy:
    def __call__(self, data: str) -> str:
        return data.upper()
  

Context側は、 Strategyを関数と同じ感覚で呼び出せます。


class TextProcessor:
    def __init__(self, strategy):
        self._strategy = strategy

    def process(self, data: str) -> str:
        return self._strategy(data)
  

「Strategyは callable であれば何でもよい」 という発想に切り替えると、設計の自由度が一気に上がります。

関数ベースのStrategy

処理がシンプルな場合は、 クラスすら作らず、関数をそのままStrategyとして使う という選択もあります。


def upper_strategy(data: str) -> str:
    return data.upper()

def lower_strategy(data: str) -> str:
    return data.lower()
  

これらを辞書で管理すれば、 if文を使わずに戦略を切り替えられます。


strategies = {
    "upper": upper_strategy,
    "lower": lower_strategy,
}

processor = TextProcessor(strategies["upper"])
  

この形は、 設定ファイルや環境変数と相性が良いのが特徴です。

クロージャで状態を持たせる

関数ベースStrategyでも、 クロージャを使えば状態を持たせられます。


def prefix_strategy(prefix: str):
    def strategy(data: str) -> str:
        return f"{prefix}{data}"
    return strategy
  

これは、クラスのインスタンス変数と ほぼ同じ役割を果たします。

「クラスにするか?関数にするか?」の判断基準は、 状態の複雑さ・拡張の頻度・可読性で決めるのが現実的です。

次のセクションでは、 Strategyパターンが実務でどう使われているかを 具体例とともに見ていきましょう。




6. 実務での活用シーン

Strategyパターンは「設計の勉強用パターン」ではなく、 実務の中で自然に出番が多い設計です。

ここでは、Pythonの現場でよくある活用シーンを見ていきましょう。

決済方法の切り替え

決済処理は、Strategyパターンの代表的なユースケースです。

  • クレジットカード決済
  • PayPal決済
  • 銀行振込

これらは「流れは同じだが、処理の中身だけが違う」典型例です。 決済方法が増えても、Context側を変更せずに対応できます。

データ出力形式の切り替え

同じデータを、用途に応じて異なる形式で出力したいケースも多くあります。

  • CSV出力
  • JSON出力
  • XML出力

出力形式ごとにStrategyを用意すれば、 「どの形式で出すか」は設定で切り替えるだけになります。

ゲームAIの行動パターン

ゲーム開発では、キャラクターの行動切り替えに Strategyパターンがよく使われます。

  • 待機
  • 巡回
  • 追跡
  • 逃走

状況に応じてStrategyを差し替えることで、 if文だらけにならずにAIの行動を制御できます。

機械学習・データ処理パイプライン

機械学習やデータ処理の分野でも、 Strategyパターンは非常に相性が良いです。

  • 異なるモデル構造の切り替え
  • 前処理・後処理ロジックの差し替え
  • 評価指標の切り替え

実験条件を変えても、 パイプライン全体を壊さずに済むのが強みです。

ID生成・ルールベース処理

ID生成やビジネスルールのように、 仕様変更が頻繁に起きる処理もStrategy向きです。

新しいルールが追加されても、 Strategyを1つ足すだけで対応できます。

次のセクションでは、 Strategyパターンを使うメリットと注意点を整理します。 「使いどころ」を見極めるための判断材料にしてください。




7. Strategyパターンのメリット・デメリット

Strategyパターンはとても強力ですが、 どんな場面でも使えばよいという万能薬ではありません。

ここでは、実務で判断しやすくなるように メリットと注意点(デメリット)を整理します。

メリット

拡張しやすい設計になる

新しい処理を追加したい場合でも、 既存コードを修正せずにStrategyを追加するだけで対応できます。

これは OCP(開放閉鎖の原則)に沿った設計であり、 機能追加のたびに感じていた「壊してしまいそうな不安」を大きく減らしてくれます。

単体テストが書きやすい

各Strategyは独立したクラス(または関数)として存在するため、 1つの戦略だけを切り出してテストできます。

巨大なif文を相手にする必要がなくなり、 テストケースの爆発も防ぎやすくなります。

「継承より委譲」を自然に実践できる

Strategyパターンは、 継承に頼らず 委譲(has-a) で振る舞いを切り替えます。

その結果、 クラス階層が深くなりすぎる問題を避けつつ、 実行時に柔軟な切り替えが可能になります。

デメリット・注意点

クラス(またはStrategy)が増える

Strategyパターンでは、 処理の数だけクラスや関数が増えます。

処理が少ない場合や、 今後ほとんど変更されないことが明らかな場合は、 オーバーエンジニアリングになる可能性があります。

全体像が見えにくくなることがある

処理が分散するため、 初見の人には「どこで何が起きているか」が 分かりにくく感じられることもあります。

そのため、 命名ディレクトリ構成は特に重要になります。

使う側に選択責任が生まれる

Strategyは「差し替え可能」な分、 どのStrategyを使うかを決める責務が必要になります。

この判断をContextに押し戻してしまうと、 再びif文地獄に逆戻りするので注意が必要です。




まとめ

Strategyパターンは、
「if文を消すためのテクニック」ではなく、変更に強い設計へ思考を切り替えるための道具です。

if / elif が増えて苦しくなる原因は、 書き方ではなく構造にあります。 処理の切り替えを条件分岐に押し込め続ける限り、 保守性・拡張性・テスト容易性は少しずつ失われていきます。

Strategyパターンを使うことで、

  • 変更されやすいロジックを独立させる
  • 既存コードを触らずに機能追加できる
  • テストしやすい単位に分解できる

という設計上のメリットを得られます。

またPythonでは、 クラス継承に縛られず Protocol・Callable・関数ベースなど、 状況に応じた柔軟なStrategyの形を選べます。

まずは、 「if文が増えそうだな」と感じた処理を1つだけ切り出してみてください。 小さなStrategy化の積み重ねが、 安心して変更できるコードにつながっていきます 😊

あわせて読みたい


参考文献


よくある質問(Q&A)

Q
if文が2〜3個しかなくてもStrategyパターンを使うべきですか?
A

無理に使う必要はありません。 ただし、今後条件が増える可能性が高い、 あるいは仕様変更が頻繁に起きそうな場合は、 早めにStrategy化しておく価値があります。

Q
StrategyパターンとFactoryパターンは必ずセットで使うものですか?
A

必須ではありません。 ただし、Strategyの生成ロジックが複雑になってきた場合は、 Factoryと組み合わせることで、 「選択」と「生成」の責務を分離できます。

Q
関数ベースのStrategyは保守性が下がりませんか?
A

処理が小さく、状態をほとんど持たない場合は、 関数ベースのほうが読みやすく保守しやすいことも多いです。 複雑さが増えてきたら、クラスへ移行する、 という段階的な選択で問題ありません。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク