見出し画像

DP.14:処理を担当するオブジェクトに届くまで値をたらい回す - Chain of Responsibility パターン -【Python】

【1】Chain of Responsibilityパターン

Chain of Responsibilityパターンは簡単に言うと

「入力データに対して処理を担当すべきオブジェクト」に値が届くまで、オブジェクト間で値を転送していく(たらい回す)

というもの。

イメージとしては「IPネットワークにおけるルーティング」に近い。
例えば、ネットワーク上を流れるパケットは、宛先に届くまで中継機器が「転送」していく。

図1:IPネットワークルーティングのイメージ図

画像2

▲各ノード(機器)は
 ・紐づく次のノード(機器)にパケットを転送する
 ・パケットを破棄する/受信する
 ・等々
といった動作をする。

【2】使いどころ

Chain of Responsibilityパターン

1つのリクエスト(入力値)に対し、
・複数のオブジェクトに「値チェック処理や必要な処理」等を与えたい時
・どのオブジェクトが特定のリクエストを処理するかわからない時

に使用できるかもしれない。

例えば、「企業の予算承認システムのようなもの」。

『予算額が1000ドルを超えないならマネージャー承認で予算承認にする。
それ以上の予算だが5000ドルを超えないならディレクターが予算承認する。さらに高額ならプレジデント(社長)の予算承認が必要になる』

図2:金額によって予算承認の責任者(オブジェクト)が変わる

画像2

この例のように『1つのリクエスト(予算額)に対し、その値によってチェックするオブジェクト(責任者:処理担当オブジェクト)が変わってくる』時に利用することができる。

具体的なプログラムの仕方としては、if文などの分岐を並べるのではなく、

(1):最初のオブジェクトにリクエストを投げる
(2):リクエストを受け取ったオブジェクトが処理対象か判定する
(3):オブジェクトが紐づく次のオブジェクトにリクエストを転送する
(4):次のオブジェクトが(2)や(3)を行う

というようにする。

図3:Chain of Responsibilityでのイメージ

画像3

このようなつくりにすると、

・クライアントコード側(リクエストを投げる側のプログラム)は「最初のあて先」だけを知っていればよい
・各オブジェクト側も接続する次のオブジェクトだけをしっていれば十分になる

つまり、クライアント側のコードとオブジェクト側のコードが分離され、各オブジェクト同士も分離されるのでソースコードのメンテナンスがしやすくなる。

【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: OK!
Director: OK!
Director: OK!
president: OK!

なお、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)

# [ 実行結果 ]
Director: OK!
Director: OK!
president: OK!  #転送がかかってPresidentオブジェクトが処理する

【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】おまけ:継承を重ねていくパターン

別の書き方として継承を重ねていき、たらい回すパターンをあげておく。
この書き方では、

・「hasattr()」を活用し、該当する属性(クラス変数、クラスメソッド)が存在するかをチェックする。

・該当する属性がなければ、継承している親クラスに処理を回して、同じように「hasattr()」でチェックを繰り返していく。

というやり方。ようは、「指定した名前と一致するメソッド」をもつ「オブジェクト」を見つけるということ。

■例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()

# [ 実行結果例 ]
MsgText: down
SendDialog: paint
MainWindow: close
!! hasattr() can't found method. call handle_default. !!
called handle_default @ MainWindow. detected event : [open]



もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。