Strategy パターンを試してみた件について
この記事は toggle holding の アドベントカレンダー 2024 の 5 日目の記事です。
概要
複数のアルゴリズムを必要に応じて使い分けるような実装ってよくありますよね?
さらに、今後もアルゴリズムを追加することが予定されている場合は、それも考慮する必要があります。
そんな時に Strategy パターンを使うと、変更に強いコードを書くことができると、ネットの海に書いてあったので実際に試してみました。
散々やられてるネタですが、自分なりに解釈してまとめてみましたー!
本文
具体的な設定があった方がコードが書きやすいので、ここでは複数のアルゴリズムの中から、指定されたアルゴリズムを用いて建物を建てるコードを考えます。
愚直な実装
まずは一番シンプルに実装してみます。
何も考えずに書くと、以下のようなコードになると思います。
from enum import Enum
# アルゴリズムの種類
class AlgorithmType(Enum):
CONCRETE = "concrete"
WOOD = "wood"
STEEL = "steel"
# それぞれのアルゴリズムの関数
def build_by_concrete():
print("concrete building!")
def build_by_wood():
print("wood building!")
def build_by_steel():
print("steel building!")
def simulate_building(algorithm_type: AlgorithmType):
# アルゴリズムの選択と実行
if algorithm_type == AlgorithmType.CONCRETE:
build_by_concrete()
elif algorithm_type == AlgorithmType.WOOD:
build_by_wood()
elif algorithm_type == AlgorithmType.STEEL:
build_by_steel()
if __name__ == "__main__":
algorithm_type = AlgorithmType.CONCRETE
simulate_building(algorithm_type)
アルゴリズムを列挙型で指定して、if 分で分岐してそれに応じた建物を建てるというコードですね。
小規模なコードであればこれでもいいのですが、このコードには以下の 2 つの問題があります。
- アルゴリズムを選択する、アルゴリズムを実行するという 2 つの処理が混在している。
- アルゴリズムを追加するたびに、if 分を追加する必要がある。
これらのどこが問題かというと、アルゴリズムを選択して実行するという simulate_building()
関数の目的は変わらないのに、アルゴリズムを追加した際にコードを変更しなければならない点です。
さらに、アルゴリズムの種類が増えていくとその分 elif 文が増えていき、どんどんコードを読むのも辛くなっていきます。
Strategy パターンを使った実装
次に Strategy パターンを使って実装してみます。
これにより、1つ目の問題、すなわち、アルゴリズムの選択と実行が混在している問題を解決できます。
まずは説明の前にコードを書いてみます。
from abc import ABC, abstractmethod
from enum import Enum
# アルゴリズムの種類
class AlgorithmType(Enum):
CONCRETE = "concrete"
WOOD = "wood"
STEEL = "steel"
# アルゴリズムの抽象クラス
class BuildingAlgorithm(ABC):
@abstractmethod
def build(self):
pass
# アルゴリズムの具象クラス
class ConcreteBuildingAlgorithm(BuildingAlgorithm):
def build(self):
print("concrete building!")
class WoodBuildingAlgorithm(BuildingAlgorithm):
def build(self):
print("wood building!")
class SteelBuildingAlgorithm(BuildingAlgorithm):
def build(self):
print("steel building!")
from algorithm import (
AlgorithmType,
ConcreteBuildingAlgorithm,
WoodBuildingAlgorithm,
SteelBuildingAlgorithm
)
def simulate_building(algorithm_type: AlgorithmType):
# アルゴリズムの選択
if algorithm_type == AlgorithmType.CONCRETE:
algorithm = ConcreteBuildingAlgorithm()
elif algorithm_type == AlgorithmType.WOOD:
algorithm = WoodBuildingAlgorithm()
elif algorithm_type == AlgorithmType.STEEL:
algorithm = SteelBuildingAlgorithm()
# アルゴリズムの実行
algorithm.build()
if __name__ == "__main__":
algorithm_type = AlgorithmType.CONCRETE
simulate_building(algorithm_type)
コードの具体的な実装を見てみます。
アルゴリズムの抽象クラスを定義し、その中でアルゴリズムを実行するメソッド BuildingAlgorithm.build()
を abstract method として定義します。
class BuildingAlgorithm(ABC):
@abstractmethod
def build(self):
pass
そして、具体的なアルゴリズムはそれを継承したクラスで build()
メソッドを実装します。
class ConcreteBuildingAlgorithm(BuildingAlgorithm):
def build(self):
print("concrete building!")
アルゴリズムを使う側では、具象クラスのインスタンスを生成し、抽象クラスに対してアルゴリズムを実行するメソッドを呼び出します。
def simulate_building(algorithm_type: AlgorithmType):
# アルゴリズムの選択
if algorithm_type == AlgorithmType.CONCRETE:
algorithm = ConcreteBuildingAlgorithm()
elif algorithm_type == AlgorithmType.WOOD:
algorithm = WoodBuildingAlgorithm()
elif algorithm_type == AlgorithmType.STEEL:
algorithm = SteelBuildingAlgorithm()
# アルゴリズムの実行
algorithm.build()
simulate_building()
関数の中で、アルゴリズムの選択と、実行が分離されていることがわかります。
これで、1 つ目の問題は解決です。
一方で、まだ if 文で分岐してアルゴリズムを定義しているのがスマートではないですね。
もう少し工夫してみます。
インスタンスの生成を工夫する
インスタンスの生成を工夫することで、2 つ目の問題、すなわち、アルゴリズムを追加するたびに if 分を追加する必要がある問題を解決できます。
デザインパターン的には Factory method パターンに近いですが、しっかりと Factory method パターンを使うとクラスの数が増えてしまい、それはそれで読みにくいので、インスタンスの生成を関数化するだけにとどめます。
こちらもコードを見てみましょう。
# 変わらないので省略
from algorithm import (
AlgorithmType,
BuildingAlgorithm,
ConcreteBuildingAlgorithm,
WoodBuildingAlgorithm,
SteelBuildingAlgorithm
)
ALGORITHM_MAP = {
AlgorithmType.CONCRETE: ConcreteBuildingAlgorithm,
AlgorithmType.WOOD: WoodBuildingAlgorithm,
AlgorithmType.STEEL: SteelBuildingAlgorithm
}
# アルゴリズムのファクトリー
def get_algorithm(algorithm_type: AlgorithmType) -> BuildingAlgorithm:
return ALGORITHM_MAP[algorithm_type]()
from algorithm import AlgorithmType
from factory import get_algorithm
def simulate_building(algorithm_type: AlgorithmType):
# アルゴリズムの選択
algorithm = get_algorithm(algorithm_type)
# アルゴリズムの実行
algorithm.build()
if __name__ == "__main__":
algorithm_type = AlgorithmType.CONCRETE
simulate_building(algorithm_type)
こちらも具体的に何をしているか見てみます。
factory.py では、アルゴリズムの種類と、それに対応する具象クラスをマッピングした辞書を定義し、それを利用してアルゴリズムを選択し、インスタンスを生成します。
ALGORITHM_MAP = {
AlgorithmType.CONCRETE: ConcreteBuildingAlgorithm,
AlgorithmType.WOOD: WoodBuildingAlgorithm,
AlgorithmType.STEEL: SteelBuildingAlgorithm
}
# アルゴリズムのファクトリー
def get_algorithm(algorithm_type: AlgorithmType) -> BuildingAlgorithm:
return ALGORITHM_MAP[algorithm_type]()
これにより、if 文で分岐してアルゴリズムを選択する必要がなくなり、simulate_building()
関数が、アルゴリズムの選択と実行するというのがわかりやすくなりました。
def simulate_building(algorithm_type: AlgorithmType):
# アルゴリズムの選択
algorithm = get_algorithm(algorithm_type)
# アルゴリズムの実行
algorithm.build()
これで、2 つ目の問題も解決です!
アルゴリズムを追加してみる
実際にアルゴリズムを追加してみましょう。
たとえば、鉄筋コンクリートの建物を建てるアルゴリズムを追加してみます。
追加した部分だけ抜粋します。
class AlgorithmType(Enum):
# これまでのアルゴリズムは省略 ...
REINFORCED_CONCRETE = "reinforced_concrete"
# --- 省略 ---
class ReinforcedConcreteBuildingAlgorithm(BuildingAlgorithm):
def build(self):
print("reinforced concrete building!")
ALGORITHM_MAP = {
# これまでのアルゴリズムは省略 ...
AlgorithmType.REINFORCED_CONCRETE: ReinforcedConcreteBuildingAlgorithm
}
if __name__ == "__main__":
# 鉄筋コンクリートの建物アルゴリズムを選択
algorithm_type = AlgorithmType.REINFORCED_CONCRETE
simulate_building(algorithm_type) # reinforced concrete building!
simulate_building()
関数の中身は変更なく、AlgorithmType
と ALGORITHM_MAP
にアルゴリズムを追加するだけで、鉄筋コンクリートの建物アルゴリズムを選択できるようになりました!
まとめ
アルゴリズムの選択と実行を分離し、さらに if 文を使わなくてもアルゴリズムを追加できるような実装の例を紹介しました。
個人的には、このような実装は柔軟性を高くする一方で、クラスの数が増えてしまうので、むやみやたらに使ってしまうとコードが読みにくくなるなぁという印象です。
そのため、今回は Context クラスを省略したり、Factory method パターンを関数で実装するくらいにとどめて、バランスをとってみました。
結局この辺はどのくらい変更や追加があるか、または開発の規模によって変わってくるので、うまく使い分けていきたいですね。
Discussion