DP.14:処理を担当するオブジェクトに届くまで値をたらい回す - Chain of Responsibility パターン -【Python】
【1】Chain of Responsibilityパターン
Chain of Responsibilityパターンは簡単に言うと
というもの。
イメージとしては「IPネットワークにおけるルーティング」に近い。
例えば、ネットワーク上を流れるパケットは、宛先に届くまで中継機器が「転送」していく。
図1:IPネットワークルーティングのイメージ図
▲各ノード(機器)は
・紐づく次のノード(機器)にパケットを転送する
・パケットを破棄する/受信する
・等々
といった動作をする。
【2】使いどころ
Chain of Responsibilityパターンは
に使用できるかもしれない。
例えば、「企業の予算承認システムのようなもの」。
図2:金額によって予算承認の責任者(オブジェクト)が変わる
この例のように『1つのリクエスト(予算額)に対し、その値によってチェックするオブジェクト(責任者:処理担当オブジェクト)が変わってくる』時に利用することができる。
具体的なプログラムの仕方としては、if文などの分岐を並べるのではなく、
というようにする。
図3:Chain of Responsibilityでのイメージ
このようなつくりにすると、
つまり、クライアント側のコードとオブジェクト側のコードが分離され、各オブジェクト同士も分離されるのでソースコードのメンテナンスがしやすくなる。
【3】例:企業の予算承認システムのようなもの
引き続き、「例:企業の予算承認システムのようなもの」を使って「Chain of Responsibility」をプログラムしてみる。実装方法はいくつかあるが、ここではメソッドチェーン風になるようにプログラムを作成していく。
(1):転送(たらい回し)を実現するクラスを作成する
「Chain of Responsibility」の「転送(たらい回し)処理」は「各オブジェクト共通のもの」なので、まずは「ベースクラス」を作成して各オブジェクトにはそれを継承させる。
■例1:【各オブジェクトが継承するベースクラス】
from abc import ABC, abstractmethod
# 抽象クラスで実装漏れを防ぐ
class Handler(ABC):
@abstractmethod
def set_next(self, handler):
pass
@abstractmethod
def handle(self, request):
pass
# たらい回しを実現するクラス(各オブジェクトにこれを継承させる)
class AbstractHandler(Handler):
_next_handler = None # 転送する「隣のオブジェクト」を格納する
def set_next(self, handler):
self._next_handler = handler
return handler
@abstractmethod
def handle(self, request):
if self._next_handler:
return self._next_handler.handle(request)
return None # チェインオブジェクト内で該当なしの場合None
なお今回は「抽象基底クラスの仕組み」として「ABCMeta」ではなく、「ABCMeta をメタクラスとするヘルパークラスABC」を利用している。
詳細は以下参照。
(2):各クラスを作成する
(1)で作成したベースクラスを継承するクラスオブジェクトを作成する
■例2:【ベースクラスを継承する各クラスオブジェクト】
class Handler(ABC):
...(略)...
class AbstractHandler(Handler):
...(略)...
# Managerクラス
class ManagerHandler(AbstractHandler):
def handle(self, request):
if float(request) < 1000:
return "Manager: OK!"
else:
return super().handle(request)
# Directorクラス
class DirectorHandler(AbstractHandler):
def handle(self, request):
if float(request) < 5000:
return "Director: OK!"
else:
return super().handle(request)
# Presidentクラス
class PresidentHandler(AbstractHandler):
def handle(self, request):
if float(request) >= 5000:
return "president: OK!"
else:
return super().handle(request)
# たらい回しの最後のオブジェクトであることを前提に条件判定なしにするパターン
# def handle(self, request):
# return "president: OK!"
(3):動作確認
以下のようにメソッドチェーン風の書き方でオブジェクト同士をつないでいく。
■例3:【オブジェクト同士を紐づけて転送設定をする】
manager = ManagerHandler()
director = DirectorHandler()
president = PresidentHandler()
manager.set_next(director).set_next(president) # メソッドチェーン風につないでいく
あとは、オブジェクトに対してリクエスト(入力値)を投げればよい。
■例4:【実行例】
result = manager.handle(999) # Managerが処理担当
print(result)
result = manager.handle(1000) # Directorが処理担当
print(result)
result = manager.handle(4999) # Directorが処理担当
print(result)
result = manager.handle(5000) # Presidentが処理担当
print(result)
なお、Managerオブジェクトに入力を入れてもらう想定だが、DirectorオブジェクトやPresidentオブジェクトに入力を投げてもいい。
■例5:【紐づけた途中のオブジェクトに値を投げ込む場合】
Directorオブジェクトに値をなげても、必要に応じてPresidentオブジェクトに転送がかかる。
... ...
manager = ManagerHandler()
director = DirectorHandler()
president = PresidentHandler()
manager.set_next(director).set_next(president) # directorがchainの途中にある
... ...
# directorオブジェクトに値を投げ込んだ場合
result = director.handle(999) # Managerを飛ばしているのでDirectorの条件で判定
print(result)
result = director.handle(4999) # Directorの条件で判定
print(result)
result = director.handle(5000) # Presidentに転送される
print(result)
【4】全体コード
from abc import ABC, abstractmethod
# 抽象クラスで実装漏れを防ぐ
class Handler(ABC):
@abstractmethod
def set_next(self, handler):
pass
@abstractmethod
def handle(self, request):
pass
# たらい回しを実現するクラス(各オブジェクトにこれを継承させる)
class AbstractHandler(Handler):
_next_handler = None # 転送する「隣のオブジェクト」を格納する
def set_next(self, handler):
self._next_handler = handler
return handler
@abstractmethod
def handle(self, request):
if self._next_handler:
return self._next_handler.handle(request)
return None # チェインオブジェクト内で該当なしの場合None
### 以下オブジェクト
# Managerクラス
class ManagerHandler(AbstractHandler):
def handle(self, request):
if float(request) < 1000:
return "Manager: OK!"
else:
return super().handle(request)
# Directorクラス
class DirectorHandler(AbstractHandler):
def handle(self, request):
if float(request) < 5000:
return "Director: OK!"
else:
return super().handle(request)
# Presidentクラス
class PresidentHandler(AbstractHandler):
def handle(self, request):
if float(request) >= 5000:
return "president: OK!"
else:
return super().handle(request)
# たらい回しの最後のオブジェクトであることを前提に条件判定なしにするパターン
# def handle(self, request):
# return "president: OK!"
if __name__ == "__main__":
# 各オブジェクトを生成
manager = ManagerHandler()
director = DirectorHandler()
president = PresidentHandler()
manager.set_next(director).set_next(president)
result = manager.handle(999)
print(result)
result = manager.handle(1000)
print(result)
result = manager.handle(4999)
print(result)
result = manager.handle(5000)
print(result)
#print("------")
# directorオブジェクトから値をいれる場合
#result = director.handle(999)
#print(result)
#result = director.handle(4999)
#print(result)
#result = director.handle(5000)
#print(result)
【5】おまけ:継承を重ねていくパターン
別の書き方として継承を重ねていき、たらい回すパターンをあげておく。
この書き方では、
というやり方。ようは、「指定した名前と一致するメソッド」をもつ「オブジェクト」を見つけるということ。
■例6:GUIのイベントに対する処理
この例では「MainWIndow」「SendDialog」「MsgText」という3つのウェジェットを用意して、継承を重ねていく。
基本的には「MsgTextウェジェット」から起動させた「イベント(入力値)」に対し、関連するウェジェット同士で値をたらい回して、担当するウェジェットのメソッドがコールされるようにする。
# たらい回す処理はベースクラスとして分離
class Widget:
def __init__(self, parent = None):
self.parent = parent
def handle(self, event):
# 検索するメソッド名をセット
handler = f'handle_{event}'
# 目的のメソッド名が存在していた実行する
if hasattr(self, handler):
method = getattr(self, handler)
method(event)
elif self.parent is not None: # 多段継承時に再帰的に検索をかける(たらい回し)
self.parent.handle(event)
elif hasattr(self, 'handle_default'): # 見つからなければ最もベースクラスの「handle_default」をコールさせる
print("!! hasattr() can't found method. call handle_default. !!")
self.handle_default(event)
class MainWindow(Widget):
def handle_close(self, event):
print(f'MainWindow: {event}')
def handle_default(self, event):
print(f'called handle_default @ MainWindow. detected event : [{event}] ')
class SendDialog(Widget):
def handle_paint(self, event):
print(f'SendDialog: {event}')
class MsgText(Widget):
def handle_down(self, event):
print(f'MsgText: {event}')
def main():
# 継承を重ねてオブジェクトを作成
# (フロント側) MsgText > SendDialog > MainWindow と多段化
mw = MainWindow() # handle_close, handle_default持ち
sd = SendDialog(mw) # handle_paint(+ handle_close) 持ち
msg = MsgText(sd) # andle_down(+ handle_paint (+ handle_close) )持ち
msg.handle("down") # MsgTextが持つメソッド
msg.handle("paint") # MsgTextの親クラスに転送 → SendDialogが持つメソッドが起動
msg.handle("close") # MsgTextの親クラスに転送 → SendDialogの親クラスに転送→ MainWindowが持つメソッドで処理
msg.handle("open") # MainWindowまで転送されるが該当なし。 → MainWindowの「handle_default()」をコールさせる
if __name__ == '__main__':
main()