【Strategyパターン】すべての開発者が知っておくべき強力なツール

# はじめに

Strategyパターンは、オブジェクト指向プログラマーが全員が知っておくべき、強力なツールです。

うまく活用すると、プログラムはより柔軟になり、新しい機能を追加するために既存のコードを変更する必要がなくなるので、**より変化に強くなります**。

この記事では、Strategyパターンとは何か、どのような構造なのか、そして**今すぐあなたのコードで使う方法**を紹介します。

# Strategyパターン

## 概要

Strategyパターンでは、同じインターフェイスを実装する**交換可能な**「アルゴリズム」をいくつか定義して、プログラム実行時に適切なアルゴリズムを選択します。

ここでいう「アルゴリズム」は「複数あるやり方の中の一つのやり方」という意味です。例えば、ファイルをアップロードする機能に例えると、S3 にアップロードするか、Google Cloud Storage にアップロードするか、あるいはローカルファイルシステムの `/mnt` ディレクトリーに入れるか、それぞれの方法が「アルゴリズム」になるわけです。

```mermaid
classDiagram
    direction TB
    class Storage{
        +upload()
    }
    <<interface>> Storage
    Storage <|-- S3
    Storage <|-- GoogleCloudStorage
    Storage <|-- LocalStorage
    class S3{
        +upload()
    }
    class GoogleCloudStorage{
        +upload()
    }
    class LocalStorage{
        +upload()
    }
```

実際にアルゴリズムを使うのは「コンテキスト」と呼ばれるクラスです。コンテキストのコンストラクターにアルゴリズムオブジェクトを渡して、インスタンス変数に設定します。このように「コンテキストがアルゴリズムを持つ」関係を **コンポジション** と言います。

プログラム実行時に、コンテキストがアルゴリズムのメソッドを呼ぶことで、一部の処理を委託します。

```python:context.py
class Context:
    def __init__(self, strategy):
        self.strategy = strategy

    def upload(self):
        self.strategy.upload()
```
コンテキストは、どのアルゴリズムを持っているかは分からないので入れ替えることができるわけです。
```python:app.py
if url.startswith("s3://"):
    context = Context(strategy=S3())
elif url.startswith("gs://"):
    context = Context(strategy=GoogleCloudStorage())
else:
    context = Context(strategy=LocalStorage())
```

このように独立したアルゴリズムをコンテキストに渡すことで、**クラスのコードを変えずに拡張できるようになります**。拡張しやすくすることで、既存のコードを変更する必要がなくなり、その分のテスト修正やデグレ確認も不要になります。これがStrategyパターンの本質です。

:::note
コードを拡張しやすく設計すると、変化に対する適応力が上がる
:::

## いつ使うか

Strategyパターンは、以下のような場合に使うことが多いです。

**1. いくつかの関連しているアルゴリズムの「やること」が同じで「やり方」だけ違う時**

例えば、機械学習モデルを RandomForest で作っても DeepLearning で作っても「モデルを作っている」という事実は変わりません。異なるのは、学習/予測のアルゴリズムだけです。

**2. ディスク容量、実行時間、ネットワーク速度のような物理的制限を考慮して実装する時**

ネットワークが遅い時は、画像の画質を多少落として送信すると、ファイルサイズが小さくなるのでもっと早く送信できます。Strategyパターンで、動的に「ネットワーク速度」に応じて「画像を送信するアルゴリズム」を入れ替えることができます。

**3. メソッドの振る舞いを `if/else` で分岐して実装している時**

`if/else` の分岐条件をそれぞれ「アルゴリズム」として切り離して実装すると、`if/else` が少なくなり、実装したアルゴリズムを他のところで再利用できます。

## 構造

Strategyパターンは、以下の3つのメンバーで構成されています。
1. **Strategy**
    - アルゴリズムが実装する共通のインターフェイス
1. **ConcreteStrategy**
    - Strategy インターフェイスを実装するクラス（例：RandomForest や LinearRegression）
1. **Context**
    - ConcreteStrategy をインスタンス変数として持つクラス（コンポジション）
    - ConcreteStrategy のメソッドを呼ぶことで、一部の処理を委託する

```mermaid
classDiagram
    direction TB
    class Context{
        +Strategy strategy
    }
    class Strategy{
        +operation()
    }
    <<interface>> Strategy
    Context *-- Strategy: ContextはStrategyを持つ
    Strategy <|-- ConcreteStrategyA
    Strategy <|-- ConcreteStrategyB
    Strategy <|-- ConcreteStrategyC
    class ConcreteStrategyA{
        +operation()
    }
    class ConcreteStrategyB{
        +operation()
    }
    class ConcreteStrategyC{
        +operation()
    }
```

Pythonでの最低限の実装はこんな感じです。

```python
import abc


class Strategy(abc.ABC):
    """
    アルゴリズム（ConcreteStrategy）が実装する共通のインターフェイス
    """
    @abc.abstractmethod
    def operation(self):
        pass


class ConcreteStrategyA(Strategy):
    """
    Strategy インターフェイスを実装するクラス
    """
    def operation(self):
        print("A")


class Context:
    """
    ConcreteStrategy をインスタンス変数として持つクラス
    """
    def __init__(self, strategy: Strategy):
        self.strategy = strategy

    def operation(self):
        # ConcreteStrategy のメソッドを呼ぶことで、一部の処理を委託する
        self.strategy.operation()


context = Context(strategy=ConcreteStrategyA())
context.operation()
```

`Context` のコンストラクターの引数の型が `Strategy` であって `ConcreteStrategyA|B|C` ではないことに気付きましたか？`Context` から**アルゴリズムの詳細を隠しています**。こうすることで、後からアルゴリズムが増えても `Context` のコードを修正する必要はありません。

## サブクラスで十分ではないか？

もちろん、クラスを継承して振る舞いを一部変えることは可能ですが、Strategyパターンが使う「コンポジション」の方が、コードを変化に強くする効果があります。

実際のコード例で、サブクラスに頼りすぎたときの罠を見てみましょう。以下のコードで、データを `input_path` から `output_path` に移す `Job` クラスを定義しています。`Job` を継承して S3 やローカルファイルシステムの「ストレージ基盤」を入れ替えています。

```python:subclass_example.py
class Job:
    """
    継承して「get_data」と「save_data」を実装してください
    """
    def __init__(self, input_path, output_path):
        self.input_path = input_path
        self.output_path = output_path

    def run(self):
        data = self.get_data()
        self.save_data(data)

    def get_data(self):
        pass

    def save_data(self, data):
        pass


class S3Job(Job):
    """
    S3にデータを格納/取得する
    """
    def get_data(self):
        return self.s3_bucket.get(self.input_path)

    def save_data(self, data):
        self.s3_bucket.put(data, self.output_path)


class LocalJob(Job):
    """
    ローカルファイルシステムにデータを格納/取得する
    """
    def get_data(self):
        return Path(self.input_path).read_bytes()

    def save_data(self, data):
        Path(self.output_path).write_bytes(data)
```

ここで、`input_data` を S3 から取得して、ローカルファイルシステムの `output_path` に出力したかったらどうしますか？

```python:subclass_example.py
class S3InputLocalOutputJob(Job):
    def get_data(self):
        return self.s3_bucket.get(self.input_path)

    def save_data(self, data):
        Path(self.output_path).write_bytes(data)
```

こうなります。:sweat_smile:

パターンが増えるたびに、クラスも増えます。GoogleCloudStorage、AzureBlob、GoogleDrive、HDFSの追加など想像したくないですね！どんどん可読性が悪くなってメンテナンスしにくくなっていきます。

:::note warn
継承に頼りすぎると、クラスの役割が増えてしまい、プログラムの柔軟性が下がる
:::

Strategyパターンを使うことで、アルゴリズムとコンテキスト（このコード例でいう `Job` クラス）を切り離して管理するため、それぞれの役割が明確になります。そして、アルゴリズムを交換できるので、**コードを変更しなくても**、多様なパターンに対応できるようになります。

```python:strategy_example.py
class Job:
    def __init__(
        self,
        input_storage_strategy,
        input_path,
        output_storage_strategy,
        output_path
    ):
        # Strategyをコンストラクターで渡して、インスタンス変数にします
        self.input_storage_strategy = input_storage_strategy
        self.input_path = input_path
        self.output_storage_strategy = output_storage_strategy
        self.output_path = output_path

    def run(self):
        data = self.get_data()
        self.save_data(data)

    def get_data(self):
        # ロジックをStrategyに委託する
        self.input_storage_strategy.get(self.input_path)

    def save_data(self, data):
        # ロジックをStrategyに委託する
        self.output_storage_strategy.save(data, self.input_path)
```

## 実装例

さて、Strategyパターンとは何か、どのような構造なのかが分かったところで、**今すぐ使う方法**をいくつかのコード例で見てみましょう。

このセクションでは、Strategyパターンを使う2つのコード例を紹介します。両方の例では、基本的に同じことを（クラス名だけ変えて）繰り返していることに気付いてほしいです（クラス図がすべて同じように見える！）

### データを圧縮する

データを圧縮することは、メールで送信するファイルのサイズを小さくしたり、ストレージを最適化できたりと、一般的な要件であることは言うまでもないと思います。ただ、同じ圧縮処理をいろんなところで書くことは非効率で、要件が変わったら、それだけ多くの修正が必要になってきます。

以下のクラス図のように、圧縮処理をStrategyパターンで「アルゴリズム化」して、再利用できるようにしましょう。

```mermaid
classDiagram
    direction TB
    class Context{
        +Compression compression
    }
    class Compression{
        +compress()
    }
    <<interface>> Compression
    Context *-- Compression
    Compression <|-- Zip
    Compression <|-- Gzip
    Compression <|-- Bzip2
    Compression <|-- NoCompression
    class Zip{
        +compress()
    }
    class Gzip{
        +compress()
    }
    class Bzip2{
        +compress()
    }
    class NoCompression{
        +compress()
    }
```
`Compression` が Strategy のインターフェイスで、`Zip` や `Gzip` がそのインターフェイスを実装する ConcreteStrategy（つまりアルゴリズム）です。
```python:strategies.py
class Compression(ABC):
    @abstractmethod
    def compress(self, data):
        pass


class Zip(Compression):
    def compress(self, data):
        # ...


class Gzip(Compression):
    def compress(self, data):
        # ...


class Bzip2(Compression):
    def compress(self, data):
        # ...


class NoCompression(Compression):
    """
    圧縮を行わない NULL オブジェクト（デフォルト値に良い）
    """
    pass
```
次にコンテキストを実装します。クラスの名前を `Context` にする必要はありません。`Job` や `S3Client` など、圧縮アルゴリズムを使いたいクラスのことなので、自由に決めてください。

コンテキストに、`Compression` タイプのオブジェクトを渡して、インスタンス変数に設定します。コンテキストが `Compression` オブジェクトを持つ関係を「コンポジション」と言います。
```python:context.py
from strategies import Compression


class Context:
    def __init__(self, compression: Compression, output_path):
        self.compression = compression
        self.output_path = output_path

    def run(self):
        data = self.get_data()
        compressed = self.compression.compress(data)
        with open(self.output_path) as file:
            file.write(compressed)

    def get_data(self):
        # ...
```
`main.py` でユーザーが指定した「圧縮レベル」によって、使う Strategy クラスを変えます。
```python:main.py
import sys

from context import Context
from strategies import Bzip2, Gzip, NoCompression, Zip


if __name__ == "__main__":
    compress_level = int(sys.argv[1])

    compression = NoCompression()

    if compress_level == 1:
        compression = Zip()
    elif compress_level == 2:
        compression = Gzip()
    elif compress_level >= 3:
        compression = Bzip2()

    context = Context(compression)
    context.run()
```

### ストレージ

複数のストレージ基盤を使用したアプリケーションを開発したことはありますか？例えば、開発環境ならローカルファイルシステム、本番環境なら S3 を使うようなアプリケーションです。

コードの再利用をあまり意識しないで実装すると、以下のような `if` 文が数多くできてしまいます。

```python
path = "path/to/file.csv"
if env == "prod":
    s3_bucket.put(data, path)
else:
    with open(path, "w") as file:
        file.write(data)
```

ここも Strategy パターンの出番です。以下のクラス図のように、それぞれのストレージクライアントを独立したクラスとして実装しましょう。

```mermaid
classDiagram
    direction TB
    class Context{
        +Storage storage
    }
    class Storage{
        +get()
        +put()
    }
    <<interface>> Storage
    Context *-- Storage
    Storage <|-- GoogleCloud
    Storage <|-- S3
    Storage <|-- LocalStorage
    class GoogleCloud{
        +get()
        +put()
    }
    class S3{
        +get()
        +put()
    }
    class LocalStorage{
        +get()
        +put()
    }
```

まずは、Strategy のインターフェイスと ConcreteStrategy（アルゴリズム）を実装します。

```python:strategies.py
import abc
import io
from urllib.parse import urlparse

import boto3


class Storage(abc.ABC):
    """
    継承して `get` と `put` を実装してください
    """
    @abc.abstractmethod
    def get(self, uri):
        pass

    @abc.abstractmethod
    def put(self, data, uri):
        pass


class S3(Storage):
    def get(self, uri):
        bucket_name, object_name = self._parse_s3_uri(uri)
        file = io.BytesIO()
        boto3.client("s3").download_file(bucket_name, object_name, file)
        data = file.read()
        return data

    def put(self, data, path):
        ...

    def _parse_s3_uri(self, uri):
        parts = urlparse(uri)
        bucket_name = parts.netloc
        object_name = parts.path.lstrip("/")
        return bucket_name, object_name


class GoogleCloudStorage(Storage):
    ...


class LocalStorage(Storage):
    ...
```
次にコンテキストを実装します。`input_path` からデータを取得して `output_path` に保存する簡単なプログラムです。ストレージ Strategy を外から渡しているので、S3、GoogleCloudStorage など自由に変えられるわけです。
```python:context.py
class Context:
    def __init__(
        self,
        input_storage: Storage,
        output_storage: Storage,
        input_path,
        output_path
    ):
        self.input_storage = input_storage
        self.output_storage = output_storage
        self.input_path = input_path
        self.output_path = output_path

    def run(self):
        data = self.get_data()
        self.save_data(data)

    def get_data(self):
        return self.input_storage.get(self.input_path)

    def save_data(self, data):
        self.output_storage.put(data, self.output_path)
```
プログラムのメインファイルで、ユーザーから「入力URL」と「出力URL」を受け取り、それぞれの URL のスキームによって ConcreteStrategy を決めます。
```python:main.py
from context import Context
from strategies import GoogleCloudStorage, LocalStorage, S3


if __name__ == "__main__":
    input_uri, output_uri = sys.argv[1:3]

    if input_uri.startswith("s3://"):
        input_storage = S3()
    elif input_uri.startswith("gs://"):
        input_storage = GoogleCloudStorage()
    elif input_uri.startswith("file://"):
        input_storage = LocalStorage()
    else:
        raise ValueError()

    # .. output_uri で同じことを行う

    context = Context(input_storage, input_path, output_storage, output_path)
    context.run_program()
```

# 終わりに

以上、Strategyパターンの概要と使い方を紹介しました。これからご自身の開発で使ってみてください。
\<!-- 役に立つにｷﾏｯﾄﾙ -->

デザインパターンについてもっと知りたい方は、以下の本をおすすめします。

https://www.amazon.co.jp/-/en/Eric-Freeman/dp/4873119766