2
8

More than 3 years have passed since last update.

Pythonで、デザインパターン「Chain of Responsibility」を学ぶ

Last updated at Posted at 2020-01-26

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。ただ、取り上げられている実例は、JAVAベースのため、自分の理解を深めるためにも、Pythonで同等のプラクティスに挑んでみました。

■ Chain of Responsibility(チェーン オブ レスポンシビリティ・パターン)

「Chain of Responsibility」という英単語は、「責任の連鎖」を意味します。
このパターンは、ある要求の受取り対象となる複数のオブジェクトに鎖状の関係を構築し、要求を処理する事が可能なオブジェクトに渡るまで、順次、構築した鎖状の関係に沿って要求を受流していくパターンです。
このパターンを適用すると、「この要求はこのオブジェクトが処理する」などという司令塔的な役割り(結び付き)を利用者側が意識しなくて良くなり、利用者側は「連鎖関係にある任意のオブジェクトに要求を投げるだけ」、処理側は「流れてきた要求が自身で処理できる場合は処理し、できない場合は、その要求を次のオブジェクトに渡すだけ」という役割分担が明確となります。(利用者側は、処理の詳細まで意識する必要はない)

UML class and sequence diagram

W3sDesign_Chain_of_Responsibility_Design_Pattern_UML.jpg

UML class diagram

designpattern-chain_of_responsibility01.gif
(以上、「ITエンジニアのための技術支援サイト by IT専科」より引用)

□ 備忘録

Chain of Responsibilityパターンは、昔のお役所仕事的な「たらい回し」な業務スタイルを連想できます。
人に要求がやってくる。その人がそれを処理できれば処理をする。処理できないなら、その要求を「次の人」にたらい回しする。次の人がそれを処理できるなら処理する。処理できなければ、その要求を「さらに次に人」にたらい回しする。
Chain of Responsibilityパターンは、所謂、線形リストのような処理モデルと理解しました。

Chain_of_responsibility.png

■ "Chain of Responsibility"のサンプルプログラム

実際に、Chain of Responsibilityパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。

  • トラブル解決に必要な案件に対して、トラブル番号を採番して、以下のトラブル解決のたらい回しな業務フローに沿って、トラブルを解決する
  • トラブルが解決した場合は、その解決結果を出力する
  • トラブルが解決できなければ、最終的に、"解決できず"で終了する

<トラブル解決のたらい回しな業務フロー>
(1) 事前に、たらい回しの順番を決めておく("Alice"->"Bob"->"Charlie"->"Diana"->"Elmo"->"Fred")
(2) まずは、Aliceがトラブル解決の要求を受け付ける。Aliceは、サポートしない役割なので、次のBodに要求をたらい回しする
(3) Bodが要求を受け付ける。Bodは、制限付きサポート(Trouble番号の上限"100")の役割であり、その役割範囲で頑張り、解決できれば業務を終了する。解決できなければ、次のCharlieに要求をたらい回しする
(4) Charlieが要求を受け付ける。Charlieは、特別サポート(Trouble番号が"429"のみ)の役割であり、その役割範囲で頑張り、解決できれば業務を終了する。解決できなければ、次のDianaに要求をたらい回しする
(5) Dianaが要求を受け付ける。Dianaは、制限付きサポート(Trouble番号の上限"200")の役割であり、その役割範囲で頑張り、解決できれば業務を終了する。解決できなければ、次のElmoに要求をたらい回しする
(6) Elmoが要求を受け付ける。Elmoは、Trouble番号が奇数のみサポートの役割であり、その役割範囲で頑張り、解決できれば業務を終了する。解決できなければ、次のFredに要求をたらい回しする
(7) Fredが要求を受け付ける。Fredは、制限付きサポート(Trouble番号の上限"300")の役割であり、その役割範囲で頑張り、解決できれば業務を終了する。解決できなければ、最終的に、"解決できず"で終了する

$ python Main.py 
[Trouble 0] is resolved by [Bob].
[Trouble 33] is resolved by [Bob].
[Trouble 66] is resolved by [Bob].
[Trouble 99] is resolved by [Bob].
[Trouble 132] is resolved by [Diana].
[Trouble 165] is resolved by [Diana].
[Trouble 198] is resolved by [Diana].
[Trouble 231] is resolved by [Elmo].
[Trouble 264] is resolved by [Fred].
[Trouble 297] is resolved by [Elmo].
[Trouble 330] cannot be resolved.
[Trouble 363] is resolved by [Elmo].
[Trouble 396] cannot be resolved.
[Trouble 429] is resolved by [Charlie].
[Trouble 462] cannot be resolved.
[Trouble 495] is resolved by [Elmo].

なんか、よくわかりづらい出力結果になりました。
サンプルプログラムの出力結果を理解するには、サンプルプログラムの詳細を確認する方が早そうです。

■ サンプルプログラムの詳細

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern/tree/master/Chain_of_Responsibility

  • ディレクトリ構成
.
├── Main.py
├── support.py
└── trouble.py

(1) Handler(処理者)の役

Handler役は、要求を処理するインタフェースを定める役です。
「次の人」を保持しておき、自分で処理ができない要求がきたら、その人にたらい回しします。もちろん、「次の人」もHandler役です。
サンプルプログラムでは、Supportクラスが、この役を努めます。
要求を処理するメソッドは、supportメソッドでした。

support.py
from abc import ABCMeta, abstractmethod

class Support(metaclass=ABCMeta):
    def __init__(self, name):
        self.__name = name
        self.__next = None

    def setNext(self, next):
        self.__next = next
        return next

    def support(self, trouble):
        if self.resolve(trouble):
            self.done(trouble)
        elif self.__next is not None:
            self.__next.support(trouble)
        else:
            self.fail(trouble)

    def __str__(self):
        return "[{0}]".format(self.__name)

    @abstractmethod
    def resolve(self, trouble):
        pass

    def done(self, trouble):
        print("{0} is resolved by {1}.".format(trouble, self))

    def fail(self, trouble):
        print("{0} cannot be resolved.".format(trouble))

(2) ConcreteHandler(具体的処理者)の役

ConcreteHandler役は、要求を処理する具体的な役です。
サンプルプログラムでは、NoSupport, LimitSupport, OddSupport, SpecialSupportの各クラスが、この役を努めます。

support.py
class NoSupport(Support):
    def __init__(self, name):
        super(NoSupport, self).__init__(name)

    def resolve(self, trouble):
        return False

class LimitSupport(Support):
    def __init__(self, name, limit):
        super(LimitSupport, self).__init__(name)
        self.__limit = limit

    def resolve(self, trouble):
        return True if trouble.getNumber() < self.__limit else False

class OddSupport(Support):
    def __init__(self, name):
        super(OddSupport, self).__init__(name)

    def resolve(self, trouble):
        return True if trouble.getNumber() % 2 == 1 else False

class SpecialSupport(Support):
    def __init__(self, name, number):
        super(SpecialSupport, self).__init__(name)
        self.__number = number

    def resolve(self, trouble):
        return True if trouble.getNumber() == self.__number else False

(3) Client(依頼人)の役

Client役は、最初のConcreteHandler役に要求を出す役です。
サンプルプログラムでは、startMainメソッドが、この役を努めます。

Main.py
from support import NoSupport, LimitSupport, SpecialSupport, OddSupport
from trouble import Trouble

def startMain():
    alice = NoSupport("Alice")
    bob = LimitSupport("Bob", 100)
    charlie = SpecialSupport("Charlie", 429)
    diana = LimitSupport("Diana", 200)
    elmo = OddSupport("Elmo")
    fred = LimitSupport("Fred", 300)

    alice.setNext(bob).setNext(charlie).setNext(diana).setNext(elmo).setNext(fred)

    for i in range(0, 500, 33):
        alice.support(Trouble(i))

if __name__ == '__main__':
    startMain()

(4) その他

トラブル番号を一元管理します。

trouble.py
class Trouble:
    def __init__(self, number):
        self.__number = number

    def getNumber(self):
        return self.__number

    def __str__(self):
        return '[Trouble {0}]'.format(self.__number)

■ 参考URL

2
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
8