Chain of Responsibilityパターン

概要

処理のたらい回しをする」パターンです。

たらい回しと聞くと非効率にも見えますが、「役割分担が明確となる」という点が明確になるメリットがあります。

例題

GUIのソフトでイベントが起きている状況を想定します。

  • eventsという配列に入っている文字列に対し、以下の反応を返したいとします。
    • eventが”key” => KeyHandler() が Press: key
    • eventが”mouse” => MouseHandler() が Click: mouse
    • eventが”timer” => TimerHandler() が Timeout: timer

→ 方針として、
TimerHandler → KeyHandler → MouseHandler という順で処理をたらい回しをします。

なお、本例題は実践Python3を簡易化したものです。

サンプルコード

events = ["timer", "key", "mouse", "key", "mouse", "terminate"]


def main():
    print("Handler Chain #1")
    handler1 = TimerHandler(KeyHandler(MouseHandler(NullHandler())))
    i = 0
    while True:
        event = events[i]
        if event == "terminate":
            break
        handler1.handle(event)
        print("---")  # => 処理の切れ目用
        i += 1


class NullHandler:
    def __init__(self, successor=None):
        self.__successor = successor

    def handle(self, event):
        if self.__successor is not None:
            self.__successor.handle(event)


class MouseHandler(NullHandler):
    def handle(self, event):
        print("(MouseHandler)")
        if event == "mouse":
            print("Click: {}".format(event))
        else:
            super().handle(event)


class KeyHandler(NullHandler):
    def handle(self, event):
        print("(KeyHandler)")
        if event == "key":
            print("Press: {}".format(event))
        else:
            super().handle(event)


class TimerHandler(NullHandler):
    def handle(self, event):
        print("(TimerHandler)")
        if event == "timer":
            print("Timeout: {}".format(event))
        else:
            super().handle(event)


if __name__ == "__main__":
    main()

# 出力

Handler Chain #1 (TimerHandler) Timeout: timer --- (TimerHandler) (KeyHandler) Press: key --- (TimerHandler) (KeyHandler) (MouseHandler) Click: mouse --- (TimerHandler) (KeyHandler) Press: key --- (TimerHandler) (KeyHandler) (MouseHandler) Click: mouse ---

TimerHandler → KeyHandler → MouseHandlerという順で処理が行われ、
各条件にマッチするとHandlerがログを出してくれるのがわかると思います。

特徴

上のサンプルコードの内、大事な部分を抽出しました。

class NullHandler:
    def __init__(self, successor=None):
        self.__successor = successor

    def handle(self, event):
        if self.__successor is not None:
            self.__successor.handle(event)


class KeyHandler(NullHandler):
    ...


class TimerHandler(NullHandler):
    def handle(self, event):
        print("(TimerHandler)")
        if event == "timer":
            print("Timeout: {}".format(event))
        else:
            super().handle(event)

if __name__ == "__main__":
    handler2 = TimerHandler(KeyHandler(...))
    handler2.handle("timer")

ポイントは2点

① Handlerを作る時にsuccessorを定義している

handler2 = TimerHandler(KeyHandler(...))

引数として与えることで、successorとして定義しています。 (定義はNullHandlerのinitの部分)

super().handle(event)によって、①で定義したsuccessorのhandleを動かしている

class TimerHandler(NullHandler):
    def handle(self, event):
        ...
        super().handle(event)

このように書くことで、親クラス(NullHandler)のhandleを動かし、
親クラスのhandleが①で定義したsuccessorのhandleを動かします

Pythonらしく書くためのコツ

コルーチンによる書き方があります。

コルーチンはざっくり言うと「関数の途中で処理を止め(再開する)」ことができる機能です。

よくわからないのでサンプルコードを書きます。

# 参考: https://qiita.com/koshigoe/items/054383a89bd51d099f10
import functools


def main():
    receiver = test_coroutine()
    # next(receiver) # coroutine関数が代理でやってくれている
    receiver.send(10)
    receiver.send(20)
    receiver.send(30)


def coroutine(func):
    @functools.wraps(func)
    def wrapper(*args, **kargs):
        g = func(*args, **kargs)
        next(g)
        return g
    return wrapper


@coroutine
def test_coroutine():
    while True:
        print("---")
        v = yield
        print("v: {}".format(v))


if __name__ == "__main__":
    main()

# 出力

--- v: 10 --- v: 20 --- v: 30 ---

大事なポイントは

  • (main文から見て)yield文で関数の処理が止まっていること
  • send(引数)で関数の処理を再開でき、yieldの部分がsendの引数になること </span>

の2点です。これを活かしてChain of ResponsibilityパターンをPythonicに書き換えます。

# サンプルコード(Pythonicなサンプルコード)

import functools

EVENT = ["timer", "key", "mouse", "key", "mouse", "terminate"]


def main():
    pipeline = key_handler(mouse_handler(timer_handler()))
    i = 0
    while True:
        event = EVENT[i]
        if event == "terminate":
            break
        pipeline.send(event)
        print("---")
        i += 1


def coroutine(function):
    @functools.wraps(function)
    def wrapper(*args, **kargs):
        generator = function(*args, **kargs)
        next(generator)
        return generator
    return wrapper


@coroutine
def mouse_handler(successor=None):
    while True:
        event = (yield)
        if event == "mouse":
            print("Click: {}".format(event))
        elif successor is not None:
            print("(Click: pass)")
            successor.send(event)


@coroutine
def key_handler(successor=None):
    while True:
        event = (yield)
        if event == "key":
            print("Press: {}".format(event))
        elif successor is not None:
            print("(Press: pass)")
            successor.send(event)


@coroutine
def timer_handler(successor=None):
    while True:
        event = (yield)
        if event == "timer":
            print("Timeout: {}".format(event))
        elif successor is not None:
            print("(Timeout: pass)")
            successor.send(event)


if __name__ == "__main__":
    main()

# 出力

(Press: pass) (Click: pass) Timeout: timer --- Press: key --- (Press: pass) Click: mouse --- Press: key --- (Press: pass) Click: mouse ---

解説 - Pythonicなサンプルコード

大事な部分を抽出しました。

def main():
    pipeline = key_handler(mouse_handler(timer_handler()))
    ...
    pipeline.send(event)

...

@coroutine
def key_handler(successor=None):
    while True:
        event = (yield)
        if event == "key":
            print("Press: {}".format(event))
        elif successor is not None:
            print("(Press: pass)")
            successor.send(event)

...

key_handlerはdecoratorにより、

  • sendされた引数がif文に一致すれば文字列を出力
  • sendされた引数がif文に一致しなければ次のsuccessorにそのままsend ということを無限に繰り返す関数となっています。

同じ仕組みがmouse_handlerやtimer_handlerにも施されているため、
pipeline.send(event)によって、eventをたらい回すことができるようになっています。

クラス図

# 一般的なクラス図

一般的なクラス図(Chain of Responsibility パターン)

# サンプルコードにおけるクラス図

関数しかないため省略。

参考URL

実践Python3