Move in silence, only speak when it's time to say Checkmate.
(目立たず行動せよ。そして相手の負けを知らせるときだけ言葉を発せよ。)
ある問題を解決する方法をその時々でスイッチしたい、という状況に出くわす場合もあるかもしれません。つまり、実行時にいずれの「解決方法」を用いるかを選択し、それを使ってプログラムを実行したい、という場合です。それぞれの「解決方法 = 戦略」にはそれぞれの長所、短所があります。
例えば2つの値を1つに「減らす (統合する: reduce)」場合を考えましょう。2つの値が数値であると仮定した場合、これらを1つの値にする方法はたくさん考えられます。問題を簡単にするために、ここでは2つの戦略を使用することにします; 足し算と引き算です。2つの数値を arg1、arg2 と表した場合、簡単な解決策は次のようになるでしょう:
def reducer(arg1, arg2, strategy=None):
if strategy == 'addition':
print(arg1 + arg2)
elif strategy == 'subtraction':
print(arg1 - arg2)
else:
print('該当する "戦略" は実装されていません')
def main():
reducer(4, 6)
reducer(4, 6, 'addition')
reducer(4, 6, 'subtraction')
if __name__ == '__main__':
main()
該当する "戦略" は実装されていません
10
-2
結果は期待通りです。しかし悲しかな、ここでもこのシリーズで散々見てきている「危険な香り」を放つコードが登場しています。もしこのコードに新たな戦略を組み込む場合、reducer() には新たな elif ステートメントを記述する必要がありますね。新たな戦略を組み込むたびに新たな elif 文が追加され、あぁ、見るに堪えない状況を想像するのはそれほど難しくありません。戦略を選択し実行するコードに手を加えることなく、実行時に新たな戦略を追加できるよりモジュラー化された方法が必要です。
そして我々の期待通り、この問題に対してもデザインパターンが用意されています。
そのデザインパターンの名前はまさに「ストレトジーパターン (strategy patter: 戦略パターン) です。そしてその名前通り、問題解決のために使用する「戦略」を実行時に選択するための方法を我々に与えてくれます。しかも、それらの戦略について1つのことを除き詳細を知っている必要はありません。それは、各戦略は実行時に呼び出される共通するシグネチャ (signature) を備えている、ということです。
この章の最初に取り上げた例題に戻ってストレトジーパターンによる実装を考えてみましょう。まずは従来通りの実装から始めます:
class StrategyBase:
def execute(self, arg1, arg2):
pass
class StrategyExecutor(StrategyBase):
def __init__(self, strategy: StrategyBase = None):
self.stragegy = strategy
def execute(self, arg1, arg2):
if self.stragegy is None:
print('該当する "戦略" は実装されていません')
else:
self.stragegy.execute(arg1, arg2)
class AddtionStrategy(StrategyBase):
def execute(self, arg1, arg2):
print(arg1 + arg2)
class SubtractionStrategy(StrategyBase):
def execute(self, arg1, arg2):
print(arg1 - arg2)
def main():
no_strategy = StrategyExecutor()
addition_strategy = StrategyExecutor(AddtionStrategy())
subtraction_strategy = StrategyExecutor(SubtractionStrategy())
no_strategy.execute(4, 6)
addition_strategy.execute(4, 6)
subtraction_strategy.execute(4, 6)
if __name__ == '__main__':
main()
この変更により、少なくとも if 文が延々と巨大化することを抑止し、新たな戦略を追加するたびに executor メソッドを変更する必要もなくなりました。正しい方向へ一歩を踏み出した、と言えるでしょう。我々のシステムの結合はより「疎」になり、それぞれの構成パートは自らの為すべきことだけを為し、他のパートとは分離されています。
この「従来」の実装方法では、このシリーズでここまで何回もお話ししてきた Python のダックタイピングを利用しています:
しかしこのコードでは実際のところ「インターフェースクラス」を定義しています。このこともこれまで何回か触れてきましたが、これはあくまでも「タイプヒントを付ける」ためと「構造をより明確にするため」です。ダックタイピングでは本来必要ありません。
しかし今回は Python の他の「強力なツール」を利用して実装を試みてみましょう。それは「値」として受け渡しができる「関数」です。この性質によって、Executor クラスに渡す関数をいちいちクラスメソッドとして定義する必要なくそのまま直に渡すことが可能となります。長い目で見た場合に記述するコード量が減少するだけではなく、プログラム全体の読解性も向上し、関数に対して直接引数を渡しそれに対する返却値も直接受け取れることからテストも容易に記述することが可能となります:
from typing import Callable
class StrategyExecutor:
def __init__(self, func: Callable[..., None] = None):
if func is not None:
self.execute = func
def execute(self, *args):
print('該当する "戦略" は実装されていません')
def strategy_addition(arg1, arg2, *args):
print(arg1 + arg2)
def strategy_subtraction(arg1, arg2, *args):
print(arg1 - arg2)
def main():
no_strategy = StrategyExecutor()
addition_strategy = StrategyExecutor(strategy_addition)
subtraction_strategy = StrategyExecutor(strategy_subtraction)
no_strategy.execute(4, 6)
addition_strategy.execute(4, 6)
subtraction_strategy.execute(4, 6)
if __name__ == '__main__':
main()
この StrategyExecutor クラスの実装、ちょっと面白いな、と思いました。クラスで定義したメソッドに対して初期化時に動的に「入れ替え」をしてます。
このような記述を自分でしたことも、他の人が書いているのを見たこともなかったので新鮮でした。インタプリタでの実行もエラーが出ることなく、クラスのプロパティを調べてもちゃんと該当する関数に入れ替わっています。
ただ、型チェッカーである mypy ではエラーになりました。"Cannot assign to a method" です。勉強になりました。
さてこのコードでは先ほども言ったように Python の関数が第一級市民 (first-class citizens) であることを利用していますから、更にこれを突き詰めて Executor クラスの定義もなくしてしまいましょう。これによって「戦略」の追加・変更に対してよりフットワークが軽快な実装になります:
from typing import Callable
def executor(*args, func: Callable[..., None] = None):
if func is None:
print('該当する "戦略" は実装されていません')
else:
func(*args)
def strategy_addition(arg1, arg2, *args):
print(arg1 + arg2)
def strategy_subtraction(arg1, arg2, *args):
print(arg1 - arg2)
def main():
executor(4, 6)
executor(4, 6, func=strategy_addition)
executor(4, 6, func=strategy_subtraction)
if __name__ == '__main__':
main()
新たに実装した executor() 関数は、実行時に複数の引数 (例では2つです) とそれを1つにまとめる (減らす: reduce する) ための「戦略」関数を受け取ります。掛け算、割り算、その他の複数の値から1つの答えを導き出す演算を行うための「戦略」を関数として実装しこの関数に渡すだけです。これによって入力値によって処理を分けるための if 文の果てしない膨張と「醜い」コードの出現を防ぐことができます。
しかし残念ながらこのコードにも else 文が含まれています。通常のプログラムでは「処理結果をターミナルに出力する」ことは一般的ではありません。何らかの「値」を返すのが普通です。ここではストレトジーパターンの動作を確認するための最も容易な方法として print 文を利用しているだけですから、この print 文を main() に移動させてしまいましょう。そうすれば executor() から else 文が消えよりスッキリとしたコードにすることができます:
from typing import Callable, Any
def executor(
*args,
func: Callable[..., Any] = lambda *args: '該当する "戦略" は実装されていません'):
return func(*args)
def strategy_addition(arg1, arg2, *args) -> Any:
return arg1 + arg2
def strategy_subtraction(arg1, arg2, *args) -> Any:
return arg1 - arg2
def main():
print(executor(4, 6))
print(executor(4, 6, func=strategy_addition))
print(executor(4, 6, func=strategy_subtraction))
if __name__ == '__main__':
main()
少し無理矢理感は否めませんが、func パラメータのデフォルト値として lambda 式で関数を記述することで if 文全体を「消滅」させてみました。読解性の面から考えると余り良い例ではありませんね...。
さて、ここまで同様の処理を行ないつつ少しづつ異なるストレトジーパターンの実装を見てきました。現実的には、株式の買い付けタイミングを決定するための様々な戦略、様々な用途に則した経路を決定するための戦略、などをその時々で使い分けるためにこのパターンを利用することになるでしょう。
ブロークンウィンドウ理論 (broken windows theory) は現実世界同様コーディングの世界にも当てはまります。決して「割れているウィンドウ」をそのままにしてはいけません。「割れているウィンドウ」に匹敵するコードがどのようなものかと言えば、あなた自身で「これはよろしくないな」と感じているものであって、メンテンナンスも拡張も簡単ではないものです。TO DO コメントを取り敢えずつけておいて後回しにする (したい)、戻ってきて修正する必要があることは承知しているけど結局いつまで経っても修正が加えられずそのまま放っておかれてしまう、といった類のコードです。
より良いコーダー (coder: コーディングをする人) になるためには常に自分が関わるコードに対して責任を負わなければなりません。あなたが関わったコードは、以前よりもクリーンでメンテナンス性が高くより良いものである必要があります。
もし次に、あなたの哀れな後任者に TO DO コメントというブービートラップ (booby trap) をそのまま残していってしまおう、という誘惑に駆られたときには、袖をまくり上げその誘惑をノックアウトしてください。
そういったトラップが仕掛けられたコードに取り組まなければならない「次の」哀れなコーダーはまさに「あなた」の可能性だってあるのです。もしその巡り合わせが「あなた」であって、「あなた」の前任者がほんの少しでも「トラップ」を消し去る努力をしてくれていたら、それは「感謝」の対象以外の何物でもないでしょう?
「自分がやられて嫌なことは他人にもしない」「自分がやってもらって嬉しいことは他人にも積極的にする」というのは日常生活だけではなくコーディングの世界にだってもちろん当てはまることですよね。