Strategy
1. 目的
必要に応じてロジックを切り替える
- 状況や必要に応じてロジックを切り替えたい
- 呼び出し元から呼び出すメソッドはひとつに限定したい
2. 課題
条件分岐で処理を分けると保守性が低下する
- シンプルにメソッド内で条件に応じて分岐させるパターン
- if文のブロック内のコードが多くなるので好ましくない
- できれば避けたい実装である
class Program:
def execute(self, type):
if type == 1:
# 諸々の処理が20-30行ほどある
...
return "わーい"
elif type == 2:
# 諸々の処理が20-30行ほどある
...
return "うぇーい"
else:
raise ValueError("未定義のtype")
メソッドに分割するとクラスの責務が増える
- 条件分岐内のロジックをメソッドに切り出すパターン
- 簡単なロジックならこれで十分なことも多い
- ただし、ビジネスロジックが複雑化した場合はやはり保守性が低下するし、プライベートメソッドとした場合テストもやりにくくなる
- また、
Program
クラスの責務が大きくなり過ぎることも問題である
class Program:
def execute(self, type):
if type == 1: reutrn self.logic01()
if type == 2: reutrn self.logic02()
raise ValueError("未定義のtype")
def logic01(self):
# 諸々の処理が20-30行ほどある
...
return "わーい"
def logic02(self):
# 諸々の処理が20-30行ほどある
...
return "うぇーい"
3. 解決策
ロジックを別のクラスとして外部化する
- ロジックを機能クラスから切り出し、別クラスとして定義する
- ロジックは機能クラスのコンストラクタで渡すようにし、機能クラスは自身のプロパティに設定されたロジッククラスを呼び出す
- このようにする事で機能クラス本体が担う責務を限定させつつ、クラスの切り替えによるロジックの変更を可能にしている
- ロジッククラスは自身が提供するロジックの実行のみに集中すればよくなるので、コードの見通しも良くなる
class Program:
""" 機能クラス """
def __init__(self, logic):
self.logic = logic
def execute(self):
return self.logic.execute()
class Logic(metaclass=ABCMeta):
""" ロジッククラスのインターフェイス """
@abstractmethod
def execute(self):
pass
class Logic01(Logic):
""" ロジッククラスその1 """
def execute(self):
# 諸々の処理が20-30行ほどある
...
return "わーい"
class Logic02(Logic):
""" ロジッククラスその2 """
def execute(self):
# 諸々の処理が20-30行ほどある
...
return "うぇーい"
if __name__ == '__main__':
p1 = Program(Logic01())
print(p1.execute()) # <= わーい
p2 = Program(Logic02())
print(p2.execute()) # <= うぇーい
4. メリット
機能とロジックを分離し、任意に組み合わせることが可能になる
- 機能クラスに対して必要に応じたロジックを与えることで、機能クラス自体の責務は一定のサイズに保ちつつ柔軟にロジックの種類を変更することができる
機能クラスとロジッククラスのコード量を減らすことができる
- 機能とロジックを同じクラスに詰め込む場合、ひとつのクラスのコード量が増加してしまう
- 大量のコードはどれだけ気を配っても理解することや修正が困難なコードになってしまう
- 機能とロジックを分割することで、それぞれを適切なサイズに保つことができる
5. デメリット
コード量が増える
- コード量が増える為、複雑度は上がる
- また、
Strategy
パターンを知らないエンジニアにとっては動作の理解が難しかったり、修正難易度が上がったと感じられることもあるので、十分な説明や教育が必要になる
6. 注意事項
Factoryと組み合わせる
- 利用側がロジッククラスの詳細を知っている必要は無いので、無用な依存関係を生じさせぬよう
Factory
でオブジェクトの生成を隠蔽するべきである
- ↓のようにしておけば、利用側は
Factory
にパラメータを渡すだけで適切に構成された機能クラスを得ることができる
def factory(type):
if type == 1: return Program(Logic01())
if type == 2: return Program(Logic02())
raise ValueError()
if __name__=='__main__':
p1 = factory(1)
print(p1.execute()) # <= わーい
p2 = factory(2)
print(p2.execute()) # <= うぇーい
クロージャを使えば更に簡潔に実装できる
- デザインパターンは元々JavaやC++で生まれ、その当時のJavaやC++にはクロージャが無かった
- その為、クラスを用いて実装されているが、クロージャを使える言語であればクロージャを使った方が簡潔に実装できることが多い
- ↓はロジッククラスを関数に置き換え、機能クラスに関数オブジェクトとして与える例である
class Program:
""" 機能クラス """
def __init__(self, logic):
self.logic = logic
def execute(self):
return self.logic()
def logic01():
return "わーい"
def logic02():
return "うぇーい"
if __name__ == '__main__':
p1 = Program(logic01)
print(p1.execute()) # <= わーい
p2 = Program(logic02)
print(p2.execute()) # <= うぇーい
- クラスで実装するのと比較して記述量が大幅に減少していることがわかる
関連
参考資料