Practical Python Design Patterns - The Chain of Responsibility Pattern 編
Practical Python Design Patterns - Python で学ぶデザインパターン: The Chain of Responsibility Pattern - Part. 4 「The Chain of Responsibility パターン」の巻 投稿一覧へ戻る
Published 2022年6月15日18:22 by mootaro23
SUPPORT UKRAINE
- Your indifference to the act of cruelty can thrive rogue nations like Russia -
The Chain of Responsibility Pattern(The Chain of Responsibility パターン)
あるコードのひと塊 (関数等) はたった1つの機能のみを果たすべきである、という考えは、the single responsibility principle (単一責任の原則) と呼ばれ、より良いコードを記述する際に考慮すべき有用なガイドラインの1つになっています。
この「単一責任の原則」に基づいて前セクションのコードを考えてみると、以下のような4つの部品にコードを分ける必要があることになります:
リクエストヘッダーからのユーザー情報の取り出し
ユーザー情報の検証、User オブジェクトの作成
リクエストエンドポイント URL に基づいたメッセージの作成
メッセージのエンコード、レスポンスの送信
この考えをサーバーアプリケーションコードに適用する前に、この実装方法の「直感」を得るために簡単なサンプルコードを記述してみたいと思います。
このサンプルアプリケーションは、簡単なメッセージを出力する4つの関数から成り立っています:
sample.py
def function_1():
print('function_1')
def function_2():
print('function_2')
def function_3():
print('function_3')
def function_4():
print('function_4')
def main_function():
function_1()
function_2()
function_3()
function_4()
if __name__ == '__main__':
main_function()
print('function_1')
def function_2():
print('function_2')
def function_3():
print('function_3')
def function_4():
print('function_4')
def main_function():
function_1()
function_2()
function_3()
function_4()
if __name__ == '__main__':
main_function()
しかしここまでの学習で、ここで記述した main_function() のように、「システムを構成する多くのサブシステムの中から必要な機能を取捨選択しつつ、処理する順番を考慮して実行する」というコードが、現実世界では非常に煩雑なものになり同時にエラーの発生を招きやすく、かつ、それぞれの機能同士の結び付きを強めてしまう (tightly-coupled system)、ということを見てきました。
ここで必要なのは、一度の呼び出し後複数の関数が次々と動的に呼び出されていく仕組みです:
chain_sample.py
class CatchAll:
def __init__(self):
self.next_to_execute = None
def execute(self):
print('最後です')
class Function1Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self):
print('function_1')
self.next_to_execute.execute()
class Function2Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self):
print('function_2')
self.next_to_execute.execute()
class Function3Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self):
print('function_3')
self.next_to_execute.execute()
class Function4Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self):
print('function_4')
self.next_to_execute.execute()
def main_function(head):
head.execute()
if __name__ == '__main__':
hd = Function1Class()
current = hd
current.next_to_execute = Function2Class()
current = current.next_to_execute
current.next_to_execute = Function3Class()
current = current.next_to_execute
current.next_to_execute = Function4Class()
main_function(hd)
def __init__(self):
self.next_to_execute = None
def execute(self):
print('最後です')
class Function1Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self):
print('function_1')
self.next_to_execute.execute()
class Function2Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self):
print('function_2')
self.next_to_execute.execute()
class Function3Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self):
print('function_3')
self.next_to_execute.execute()
class Function4Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self):
print('function_4')
self.next_to_execute.execute()
def main_function(head):
head.execute()
if __name__ == '__main__':
hd = Function1Class()
current = hd
current.next_to_execute = Function2Class()
current = current.next_to_execute
current.next_to_execute = Function3Class()
current = current.next_to_execute
current.next_to_execute = Function4Class()
main_function(hd)
先程と比較するとコード量は明らかに増加しています。しかしこの実装の方が、それぞれの関数の独立性が高まっているのも一目瞭然でしょう。
この「独立性」の問題をさらによく理解するために、main_function() に引数として文字列を渡すように変更します。この文字列は「数字の並び」で、各関数名に含まれている数字がこの文字列に含まれている場合該当する関数を呼び出します。関数側では自身の名前と受け取った文字列を出力し、その文字列から自分の番号を除いた残りの文字列を返します。main_function() では返された文字列について同様の処理を繰り返していきます。
まずは sample.py にこの機能を実装します:
sample_with_more_functions.py
def function_1(in_string):
print('function_1')
print(in_string)
return "".join([x for x in in_string if x != '1'])
def function_2(in_string):
print('function_2')
print(in_string)
return "".join([x for x in in_string if x != '2'])
def function_3(in_string):
print('function_3')
print(in_string)
return "".join([x for x in in_string if x != '3'])
def function_4(in_string):
print('function_4')
print(in_string)
return "".join([x for x in in_string if x != '4'])
def main_function(input_string):
if '1' in input_string:
input_string = function_1(input_string)
if '2' in input_string:
input_string = function_2(input_string)
if '3' in input_string:
input_string = function_3(input_string)
if '4' in input_string:
input_string = function_4(input_string)
print(input_string)
if __name__ == '__main__':
main_function("1221345439")
print('function_1')
print(in_string)
return "".join([x for x in in_string if x != '1'])
def function_2(in_string):
print('function_2')
print(in_string)
return "".join([x for x in in_string if x != '2'])
def function_3(in_string):
print('function_3')
print(in_string)
return "".join([x for x in in_string if x != '3'])
def function_4(in_string):
print('function_4')
print(in_string)
return "".join([x for x in in_string if x != '4'])
def main_function(input_string):
if '1' in input_string:
input_string = function_1(input_string)
if '2' in input_string:
input_string = function_2(input_string)
if '3' in input_string:
input_string = function_3(input_string)
if '4' in input_string:
input_string = function_4(input_string)
print(input_string)
if __name__ == '__main__':
main_function("1221345439")
各関数がほとんど同じ作業を行っている、という面ではあまりうまい例とは言えないかもしれませんが、まずここで注目していただきたいのは、main_function() における if 文です。この if 文は、作業を振り分ける関数の数が多くなればそれに比例して膨れ上がっていきます。
そして、main_function() とそれぞれの関数、および、それぞれの関数同士が如何に密に結合してしまっているか、ということです。
Considering the Chain of Responsibility Pattern more deeply(The Chain of Responsibility パターンの更なる理解のために)
では同じ機能を chain_sample.py にも追加してみましょう:
chain_sample_with_more_functions.py
class CatchAll:
def __init__(self):
self.next_to_execute = None
def execute(self, request):
print(request)
class Function1Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
print('function_1')
print(request)
request = "".join([x for x in request if x != '1'])
self.next_to_execute.execute(request)
class Function2Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
print('function_2')
print(request)
request = "".join([x for x in request if x != '2'])
self.next_to_execute.execute(request)
class Function3Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
print('function_3')
print(request)
request = "".join([x for x in request if x != '3'])
self.next_to_execute.execute(request)
class Function4Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
print('function_4')
print(request)
request = "".join([x for x in request if x != '4'])
self.next_to_execute.execute(request)
def main_function(head, request):
head.execute(request)
if __name__ == '__main__':
hd = Function1Class()
current = hd
current.next_to_execute = Function2Class()
current = current.next_to_execute
current.next_to_execute = Function3Class()
current = current.next_to_execute
current.next_to_execute = Function4Class()
main_function(hd, "1221345439")
def __init__(self):
self.next_to_execute = None
def execute(self, request):
print(request)
class Function1Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
print('function_1')
print(request)
request = "".join([x for x in request if x != '1'])
self.next_to_execute.execute(request)
class Function2Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
print('function_2')
print(request)
request = "".join([x for x in request if x != '2'])
self.next_to_execute.execute(request)
class Function3Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
print('function_3')
print(request)
request = "".join([x for x in request if x != '3'])
self.next_to_execute.execute(request)
class Function4Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
print('function_4')
print(request)
request = "".join([x for x in request if x != '4'])
self.next_to_execute.execute(request)
def main_function(head, request):
head.execute(request)
if __name__ == '__main__':
hd = Function1Class()
current = hd
current.next_to_execute = Function2Class()
current = current.next_to_execute
current.next_to_execute = Function3Class()
current = current.next_to_execute
current.next_to_execute = Function4Class()
main_function(hd, "1221345439")
新たに引数を渡すことにした以外、呼び出しの大元である main_function() にも、実行母体である各クラスの execute() にも、そしてそれぞれのクラスが自分の次のクラスへ処理をつなげていく方法にも何ら変更がないことがお分かりでしょうか?これは各機能 (クラス、関数 etc) がそれぞれ明確に独立していることの証です。もしこの「機能チェーン」に新たな機能を組み込みたい場合、もしくは、この「機能チェーン」からある機能を取り外したい場合、この「機能チェーン」のスターターである main_function() にも、勿論それぞれの「機能」にも何1つ変更を加える必要はありません。
それぞれの操作を担当するハンドラー (handler) は自分の処理だけに集中します。自分が引き渡した結果によって他のハンドラーがどのように動作するのか、などといったことには一切関知しません。ハンドラーはリクエストを受け取り、すべきことがあればこなし、その結果を返すだけです。この実装方法の最大の長所は、「機能チェーン」をプログラム実行時に自由に組み替えることができることです。また、各処理の順番が重要でないのであれば、各ハンドラーを好き勝手シャッフルすることさえ可能です。それぞれが「緩く」結合しているコード (loosely coupled code) が「密に」結合しているコード (tightly coupled code) よりも如何に自由度が高いか、ということがこの実装パターンでも確認できると思います。
この自由度の高さを確認するために各クラスの機能を拡張し、渡されてきた文字列に自分のクラス番号が含まれている場合のみ「何か」を実行するように変更しましょう:
class CatchAll:
def __init__(self):
self.next_to_execute = None
def execute(self, request):
print(request)
class Function1Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
if '1' in request:
print(f'Function 1 クラスを実行します [引数]{request}')
request = "".join([x for x in request if x != '1'])
self.next_to_execute.execute(request)
class Function2Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
if '2' in request:
print(f'Function 2 クラスを実行します [引数]{request}')
request = "".join([x for x in request if x != '2'])
self.next_to_execute.execute(request)
class Function3Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
if '3' in request:
print(f'Function 3 クラスを実行します [引数]{request}')
request = "".join([x for x in request if x != '3'])
self.next_to_execute.execute(request)
class Function4Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
if '4' in request:
print(f'Function 4 クラスを実行します [引数]{request}')
request = "".join([x for x in request if x != '4'])
self.next_to_execute.execute(request)
def main_function(head, request):
head.execute(request)
if __name__ == '__main__':
hd = Function1Class()
current = hd
current.next_to_execute = Function2Class()
current = current.next_to_execute
current.next_to_execute = Function3Class()
current = current.next_to_execute
current.next_to_execute = Function4Class()
main_function(hd, "12214549")
def __init__(self):
self.next_to_execute = None
def execute(self, request):
print(request)
class Function1Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
if '1' in request:
print(f'Function 1 クラスを実行します [引数]{request}')
request = "".join([x for x in request if x != '1'])
self.next_to_execute.execute(request)
class Function2Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
if '2' in request:
print(f'Function 2 クラスを実行します [引数]{request}')
request = "".join([x for x in request if x != '2'])
self.next_to_execute.execute(request)
class Function3Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
if '3' in request:
print(f'Function 3 クラスを実行します [引数]{request}')
request = "".join([x for x in request if x != '3'])
self.next_to_execute.execute(request)
class Function4Class:
def __init__(self):
self.next_to_execute = CatchAll()
def execute(self, request):
if '4' in request:
print(f'Function 4 クラスを実行します [引数]{request}')
request = "".join([x for x in request if x != '4'])
self.next_to_execute.execute(request)
def main_function(head, request):
head.execute(request)
if __name__ == '__main__':
hd = Function1Class()
current = hd
current.next_to_execute = Function2Class()
current = current.next_to_execute
current.next_to_execute = Function3Class()
current = current.next_to_execute
current.next_to_execute = Function4Class()
main_function(hd, "12214549")
今回「機能チェーン」のスターターである main_function() に渡すリクエスト文字列には '3' を含んでいません。その結果 Function3Class クラスオブジェクトの実行は「素通り」し、チェーンを構成する次の Function4Class クラスオブジェクトへ処理が流れています。注目していただきたいのは、この「機能チェーン」のセットアップにも、入り口である main_function() にも一切変更を加えていないにもかかわらず、これだけ自由度の高い処理の流れが生まれている、という点です。
この「機能チェーン (ハンドラーチェーン: chain of handlers)」が Chain of Responsibility パターンの真髄です。
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -