📘

PythonにおけるStrategyパターンの学習

に公開

はじめに

  • Pythonのコードをリファクタリングする機会があり、デザインパターンを交えた改善提案をしたく、気になっているデザインパターンについて学んでまとめてみました。

Strategyパターンにおける3つの登場人物

  • Context
    • 実際にアルゴリズム(Startegy/振る舞い)を呼び出す役割
  • Interface
    • 振る舞いの定義を行う役割
  • Strategy(振る舞い)
    • Interfaceによって定義された振る舞いのルールのもと、実際のアルゴリズムを実装する役割

出典: https://yusuke-ujitoko.hatenablog.com/entry/2009/12/29/000000

上記だけではわかりにくいので実際のコードを読みながら理解していきます。

以下のコードをリーディング

いかにシンプルなデータのクレンジングを行うコードを用意しました。

from abc import ABC, abstractmethod
from datetime import datetime
import re


# --- Strategyのインターフェース ---
class ProcessingStrategy(ABC):
    @abstractmethod
    def process(self, data: dict) -> dict:
        pass


# --- 具体的な処理戦略たち ---
class DateFormatStrategy(ProcessingStrategy):
    """ 'date' キーを ISO形式 → yyyy-mm-dd に変換 """
    def process(self, data):
        if 'date' in data:
            try:
                dt = datetime.fromisoformat(data['date'])
                data['date'] = dt.strftime('%Y-%m-%d')
            except Exception:
                pass
        return data


class RemoveKeysStrategy(ProcessingStrategy):
    """ 指定されたキーを削除する """
    def __init__(self, keys_to_remove):
        self.keys_to_remove = keys_to_remove

    def process(self, data):
        for key in self.keys_to_remove:
            data.pop(key, None)
        return data

# --- パイプラインの本体 ---
class JsonProcessor:
    def __init__(self):
        self.strategies = []

    def add_strategy(self, strategy: ProcessingStrategy):
        self.strategies.append(strategy)

    def process(self, data: dict) -> dict:
        for strategy in self.strategies:
            data = strategy.process(data)
        return data

if __name__ == "__main__":
    data = {
        'date': '2025-01-01',
        'name': 'John Doe',
        'age': 30,
        'address': '123 Main St, Anytown, USA'
    }

    processor = JsonProcessor()
    processor.add_strategy(RemoveKeysStrategy(["debug_info"]))
    processor.add_strategy(SnakeToCamelCaseStrategy())
    processor.add_strategy(DateFormatStrategy())

    result = processor.process(data)

    print(result)

output

{'date': '2025-01-01', 'name': 'John Doe', 'age': 30, 'address': '123 Main St, Anytown, USA'}

各クラスの紹介

JsonProcessor(Context)

キャプション

  • 今回のコンテキストクラス
  • コンテキストが各アルゴリズムを利用する側になる
  • コンテキストがアルゴリズムクラスを持つことをコンポジションという
  • アルゴリズムを追加する機能を持つ

各メソッドに関して

  • __init__
    • インスタンス変数(配列)strategiesを用意する
  • add_strategy
    • 各アルゴリズムのクラスを引数にとり、扱うアルゴリズムのクラスを配列として保持
  • process
    • add_strategyにて追加したアルゴリズムクラスを実際に実行するクラス

ProcessingStrategy(Interface)

キャプション

  • 今回のインターフェースとなるクラス
  • 同じメソッドシグネチャを持つ
    • 同じメソッドシグネチャとは
    • 「メソッド、引数、戻り値」をもつ
    • コンテキスト側はStrategyの中身を知らなくても統一的に扱うことが可能
  • ポリモーフィズムを活用するために必要な設計で上記のようにコンテキストが処理の中身を知らなくても使える -> 疎結合の実現に寄与

※ポリモーフィズムとは...同じ名前のメソッドで異なるクラスで動作すること。ギリシャ語で多様性。
今回の場合は以下コード。

def process(self, data: dict) -> dict:
        for strategy in self.strategies:
            data = strategy.process(data)
        return data

各Strategyクラス(Strategy)

※今回のDateFormatStrategy,RemoveKeysStrategy

キャプション

  • 全ての振る舞いクラスはアブストラクトクラスProcessingStrategyを継承している
  • 継承することにより、コンポジションの際に統一的に扱うことが可能

各クラスに関して

  • 以下のクラスは全てProcessingStrategyを継承している

  • 継承することでシグネチャーを共有できる

  • DateFormatStrategy

    • ISOをymdへ変換
  • RemoveKeysStrategy

    • 引数にて指定されたキー名を辞書から削除

変更を追加してみる

  • 名前を姓と名で分けるクラスを追加
class SeparateNameStrategy(ProcessingStrategy):
    """ 名前を分割して、first_nameとlast_nameに格納する """
    def process(self, data):
        if 'name' in data:
            name = data['name']
            parts = name.split(' ')
            data['first_name'] = parts[0]
            data['last_name'] = parts[1]
        return data
  • add_strategyにてコンテキストにアルゴリズム追加
processor.add_strategy(SeparateNameStrategy())
  • output
{'date': '2025-01-01', 'name': 'John Smith', 'age': 30, 'address': '123 Main St, Anytown, USA', 'first_name': 'John', 'last_name': 'Smith'}
  • 上記のように追加のアルゴリズムが必要な場合も容易に追加できました

Strategyパターンの使い所

Strategyパターンが有用なシーン

  • 複数のアルゴリズムパターンを使い分けたい時
  • 処理のロジックを変更しても呼び出す側を変えたくない時
  • if-elseやcaseで増えそうな分岐をクラスで整理したいとき

Strategyパターンが有用でない場合

  • 処理が一つしかない時 or 増える予定がない時
  • Strategyパターンは複数の処理(アルゴリズム)を切り替えることに意味があるため

所感

  • デザインパターンと聞くとすこし複雑な印象がありましたが、実際に体系だたて学んでみると、シンプルな構造になっているとわかりました。
  • 上記のようにデザインパターンのように実装をおこない、各クラスの関係性をドキュメンテーションに残しておくことで保守や拡張がしやすくなるとも感じました。

Discussion