Python設計模式之責任鏈模式

責任鏈模式

開發一個應用時,多數時候我們都能預先知道哪個方法能處理某個特定請求。然而,情況并非總是如此。例如,想想任意一種廣播計算機網絡,例如最早的以太網實現(請參考網頁[t.cn/RqrTp0Y])。在廣播計算機網絡中,會將所有請求發送給所有節點(簡單起見,不考慮廣播域),但僅對所發送請求感興趣的節點會處理請求。加入廣播網絡的所有計算機使用一種常見的媒介相互連接,比如,下圖中的三個節點通過光纜連接起來。

如果一個節點對某個請求不感興趣或者不知道如何處理這個請求,可以執行以下兩個操作。

  • 忽略這個請求,什么都不做
  • 將請求轉發給下一個節點

節點對一個請求的反應方式是實現的細節。然而,我們可以使用廣播計算機網絡的類比來理解責任鏈模式是什么。責任鏈(Chain of Responsibility)模式用于讓多個對象來處理單個請求時,或者用于預先不知道應該由哪個對象(來自某個對象鏈)來處理某個特定請求時。其原則如下所示。

(1) 存在一個對象鏈(鏈表、樹或任何其他便捷的數據結構)。
(2) 我們一開始將請求發送給鏈中的第一個對象。
(3) 對象決定其是否要處理該請求。
(4) 對象將請求轉發給下一個對象。
(5) 重復該過程,直到到達鏈尾。

在應用級別,不用討論光纜和網絡節點,而是可以專注于對象以及請求的流程。下圖展示了客戶端代碼如何將請求發送給應用的所有處理元素(又稱為節點或處理程序),經www.sourcema-king.com允許使用(請參考網頁[t.cn/RqrTYuB])。

注意,客戶端代碼僅知道第一個處理元素,而非擁有對所有處理元素的引用;并且每個處理元素僅知道其直接的下一個鄰居(稱為后繼),而不知道所有其他處理元素。這通常是一種單向關系,用編程術語來說是一個單向鏈表,與之相反的是雙向鏈表。單向鏈表不允許雙向地遍歷元素,雙向鏈表則是允許的。這種鏈式組織方式大有用處:可以解耦發送方(客戶端)和接收方(處理元素)(請參考[GOF95,第254頁])。

以下例子來自于GitHub:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""http://www.dabeaz.com/coroutines/"""

from contextlib import contextmanager
import os
import sys
import time


class Handler(object):

    def __init__(self, successor=None):
        self._successor = successor

    def handle(self, request):
        res = self._handle(request)
        if not res:
            self._successor.handle(request)

    def _handle(self, request):
        raise NotImplementedError('Must provide implementation in subclass.')


class ConcreteHandler1(Handler):

    def _handle(self, request):
        if 0 < request <= 10:
            print('request {} handled in handler 1'.format(request))
            return True


class ConcreteHandler2(Handler):

    def _handle(self, request):
        if 10 < request <= 20:
            print('request {} handled in handler 2'.format(request))
            return True


class ConcreteHandler3(Handler):

    def _handle(self, request):
        if 20 < request <= 30:
            print('request {} handled in handler 3'.format(request))
            return True


class DefaultHandler(Handler):

    def _handle(self, request):
        print('end of chain, no handler for {}'.format(request))
        return True


class Client(object):

    def __init__(self):
        self.handler = ConcreteHandler1(
            ConcreteHandler3(ConcreteHandler2(DefaultHandler())))

    def delegate(self, requests):
        for request in requests:
            self.handler.handle(request)


def coroutine(func):
    def start(*args, **kwargs):
        cr = func(*args, **kwargs)
        next(cr)
        return cr
    return start


@coroutine
def coroutine1(target):
    while True:
        request = yield
        if 0 < request <= 10:
            print('request {} handled in coroutine 1'.format(request))
        else:
            target.send(request)


@coroutine
def coroutine2(target):
    while True:
        request = yield
        if 10 < request <= 20:
            print('request {} handled in coroutine 2'.format(request))
        else:
            target.send(request)


@coroutine
def coroutine3(target):
    while True:
        request = yield
        if 20 < request <= 30:
            print('request {} handled in coroutine 3'.format(request))
        else:
            target.send(request)


@coroutine
def default_coroutine():
    while True:
        request = yield
        print('end of chain, no coroutine for {}'.format(request))


class ClientCoroutine:

    def __init__(self):
        self.target = coroutine1(coroutine3(coroutine2(default_coroutine())))

    def delegate(self, requests):
        for request in requests:
            self.target.send(request)


def timeit(func):

    def count(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        count._time = time.time() - start
        return res
    return count


@contextmanager
def suppress_stdout():
    try:
        stdout, sys.stdout = sys.stdout, open(os.devnull, 'w')
        yield
    finally:
        sys.stdout = stdout


if __name__ == "__main__":
    client1 = Client()
    client2 = ClientCoroutine()
    requests = [2, 5, 14, 22, 18, 3, 35, 27, 20]

    client1.delegate(requests)
    print('-' * 30)
    client2.delegate(requests)

    requests *= 10000
    client1_delegate = timeit(client1.delegate)
    client2_delegate = timeit(client2.delegate)
    with suppress_stdout():
        client1_delegate(requests)
        client2_delegate(requests)
    # lets check what is faster
    print(client1_delegate._time, client2_delegate._time)

### OUTPUT ###
# request 2 handled in handler 1
# request 5 handled in handler 1
# request 14 handled in handler 2
# request 22 handled in handler 3
# request 18 handled in handler 2
# request 3 handled in handler 1
# end of chain, no handler for 35
# request 27 handled in handler 3
# request 20 handled in handler 2
# ------------------------------
# request 2 handled in coroutine 1
# request 5 handled in coroutine 1
# request 14 handled in coroutine 2
# request 22 handled in coroutine 3
# request 18 handled in coroutine 2
# request 3 handled in coroutine 1
# end of chain, no coroutine for 35
# request 27 handled in coroutine 3
# request 20 handled in coroutine 2
# (0.2369999885559082, 0.16199994087219238)

現實生活的例子

ATM機以及及一般而言用于接收/返回鈔票或硬幣的任意類型機器(比如,零食自動販賣機)都使用了責任鏈模式。機器上總會有一個放置各種鈔票的槽口,如下圖所示(經www.sourcemaking.com允許使用)。

鈔票放入之后,會被傳遞到恰當的容器。鈔票返回時,則是從恰當的容器中獲取(請參考網頁[t.cn/RqrTYuB]和網頁[t.cn/RqrTnts])。我們可以把這個槽口視為共享通信媒介,不同的容器則是處理元素。結果包含來自一個或多個容器的現金。例如,在上圖中,我們看到在從ATM機取175美元時會發生什么。

軟件的例子

我試過尋找一些使用責任鏈模式的Python應用的好例子,但是沒找到,很可能是因為Python程序員不使用這個名稱。因此,很抱歉,我將使用其他編程語言的例子作為參考。

Java的servlet過濾器是在一個HTTP請求到達H標處理程序之前執行的一些代碼片段。在使用servlet過濾器時,有一個過濾器鏈,其中每個過濾器執行一個不同動作(用戶身份驗證、記H志、數據壓縮等),并且將請求轉發給下一個過濾器直到鏈結束;如果發生錯誤(例如,連續三次身份驗證失敗)則跳出處理流程(請參考網頁[t.cn/RqrTukH])。

Apple的Cocoa和Cocoa Touch框架使用責任鏈來處理事件。在某個視圖接收到一個其并不知道如何處理的事件時,會將事件轉發給其超視圖,直到有個視圖能夠處理這個事件或者視圖鏈結束(請參考網頁[t.cn/RqrTrzK])。

應用案例

通過使用責任鏈模式,我們能讓許多不同對象來處理一個特定請求。在我們預先不知道應該由哪個對象來處理某個請求時,這是有用的。其中一個例子是采購系統。在采購系統中,有許多核準權限。某個核準權限可能可以核準在一定額度之內的訂單,假設為100美元。如果訂單超過了100美元,則會將訂單發送給鏈中的下一個核準權限,比如能夠核準在200美元以下的訂單,等等。

另一個責任鏈可以派上用場的場景是,在我們知道可能會有多個對象都需要對同一個請求進行處理之時。這在基于事件的編程中是常有的事情。單個事件,比如一次鼠標左擊,可被多個事件監聽者捕獲。

不過應該注意,如果所有請求都能被單個處理程序處理,責任鏈就沒那么有用了,除非確實不知道會是哪個程序處理請求。這一模式的價值在于解耦。客戶端與所有處理程序(一個處理程序與所有其他處理程序之間也是如此)之間不再是多對多關系,客戶端僅需要知道如何與鏈的起始節點(標頭)進行通信。

下圖演示了緊耦合與松耦合之間的區別心。松耦合系統背后的考慮是簡化維護,并讓我們易于理解系統的工作原理(請參考網頁https://infomgmt.wordpress.com/2010/02/18/a-visual-respresen-tation-of-coupling/)。

數據耦合(data coupling)、特征耦合(stamp coupling)、控制耦合(control coupling)、共用耦合(common coupling)和內容耦合(content coupling)這幾個概念的含義可參考Wikipedia詞條 https://en.wikipedia.org/wiki/Coupling_(computer_programming)。 ——譯者注

實現

使用Python實現責任鏈模式有許多種方式,但是我最喜歡的實現是Vespe Savikko所提出的(請參考網頁[t.cn/RqruSj1])。Vespe的實現以地道的Python風格使用動態分發來處理請求(請參考網頁[t.cn/RqruWFp])。

我們以Vespe的實現為參考實現一個簡單的事件系統。下面是該系統的UML類圖。

Event類描述一個事件。為了讓它簡單一點,在我們的案例中一個事件只有一個name屬性。

class Event:

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

Widget類是應用的核心類。UML圖中展示的parent聚合關系表明每個控件都有一個到父對象的引用。按照約定,我們假定父對象是一個Widget實例。然而,注意,根據繼承的規則,任何Widget子類的實例(例如,MsgText的實例)也是Widget實例。parent的默認值為None。

class Widget:
    def __init__(self, parent=None):
        self.parent = parent

handle()方法使用動態分發,通過hasattr()和getattr()決定一個特定請求(event)應該由誰來處理。如果被請求處理事件的控件并不支持該事件,則有兩種回退機制。如果控件有parent,則執行parent的handle()方法。如果控件沒有parent,但有handle_default()方法,則執行handle_default()。

def handle(self, event):
    handler = 'handle_{}'.format(event)
    if hasattr(self, handler):
        method = getattr(self, handler)
        method(event)
    elif self.parent:
        self.parent.handle(event)
    elif hasattr(self, 'handle_default'):
        self.handle_default(event)

此時,你可能已明臼為什么UML類圖中Widget與Event類僅是關聯關系而已(不是聚合或組合關系)。關聯關系用于表明Widget類知道Event類,但對其沒有任何嚴格的引用,因為事件僅需要作為參數傳遞給handle()。

MainWindow、MsgText和SendDialog是具有不同行為的控件。我們并不期望這三個控件都能處理相同的事件,即使它們能處理相同事件,表現出來也可能是不同的。MainWindow僅能處理close和default事件。

class MainWindow(Widget):
    def handle_close(self, event):
        print('MainWindow: {}'.format(event))
    def handle_default(self, event):
        print('MainWindow Default: {}'.format(event))

SendDialog僅能處理paint事件。

class SendDialog(Widget):
        def handle_paint(self, event):
            print('SendDialog: {}'.format(event))

最后,MsgText僅能處理down事件。

class MsgText(Widget):
    def handle_down(self, event):
        print('MsgText: {}'.format(event))

main()函數展示如何創建一些控件和事件,以及控件如何對那些事件作出反應。所有事件都會被發送給所有控件。注意其中每個控件的父子關系。sd對象(SendDialog的一個實例)的父對象是mw(MainWindow的一個實例)。然而,并不是所有對象都需要一個MainWindow實例的父對象。例如,msg對象(MsgText的一個實例)是以sd作為父對象。

def main(): 5 mw = MainWindow()
    sd = SendDialog(mw)
    msg = MsgText(sd)
    for e in ('down', 'paint', 'unhandled', 'close'):
        evt = Event(e)
        print('\nSending event -{}- to MainWindow'.format(evt))
        mw.handle(evt)
        print('Sending event -{}- to SendDialog'.format(evt))
        sd.handle(evt)
        print('Sending event -{}- to MsgText'.format(evt))
        msg.handle(evt)

以下是示例的完整代碼(chain.py)。

class Event:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

class Widget:
    def __init__(self, parent=None):
        self.parent = parent

    def handle(self, event):
        handler = 'handle_{}'.format(event)
        if hasattr(self, handler):
            method = getattr(self, handler)
            method(event)
        elif self.parent:
            self.parent.handle(event)
        elif hasattr(self, 'handle_default'):
            self.handle_default(event)

class MainWindow(Widget):
    def handle_close(self, event):
        print('MainWindow: {}'.format(event))

    def handle_default(self, event):
        print('MainWindow Default: {}'.format(event))

class SendDialog(Widget):
    def handle_paint(self, event):
        print('SendDialog: {}'.format(event))

class MsgText(Widget):
    def handle_down(self, event):
        print('MsgText: {}'.format(event))

def main():
    mw = MainWindow()
    sd = SendDialog(mw)
    msg = MsgText(sd)

    for e in ('down', 'paint', 'unhandled', 'close'):
        evt = Event(e)
        print('\nSending event -{}- to MainWindow'.format(evt))
        mw.handle(evt)
        print('Sending event -{}- to SendDialog'.format(evt))
        sd.handle(evt)
        print('Sending event -{}- to MsgText'.format(evt))
        msg.handle(evt)

if __name__ == '__main__':
    main()
Sending event -down- to MainWindow
MainWindow Default: down
Sending event -down- to SendDialog
MainWindow Default: down
Sending event -down- to MsgText
MsgText: down

Sending event -paint- to MainWindow
MainWindow Default: paint
Sending event -paint- to SendDialog
SendDialog: paint
Sending event -paint- to MsgText
SendDialog: paint

Sending event -unhandled- to MainWindow
MainWindow Default: unhandled
Sending event -unhandled- to SendDialog
MainWindow Default: unhandled
Sending event -unhandled- to MsgText
MainWindow Default: unhandled

Sending event -close- to MainWindow
MainWindow: close
Sending event -close- to SendDialog
MainWindow: close
Sending event -close- to MsgText
MainWindow: close

從輸出中我們能看到一些有趣的東西。例如,發送一個down事件給MainWindow,最終被MainWindow默認處理函數處理。另一個不錯的用例是,雖然close事件不能被SendDialog和MsgText直接處理,但所有close事件最終都能被MainWindow正確處理。這正是使用父子關系作為一種回退機制的優美之處。

如果你想在這個事件例子上花費更多時間發揮自己的創意,可以替換這些愚蠢的print語旬,針對羅列出來的事件添加一些實際的行為。當然,并不限于羅列出來的事件。隨意添加一些你喜歡的事件,做一些有用的事情!

另一個練習是在運行時添加一個MsgText實例,以MainWindow為其父。這個有難度嗎?也挑個事件類型來試試(為一個已有控件添加一個新的事件),哪個更難?

小結

本章中,我們學習了責任鏈設計模式。在尤法預先知道處理程序的數量和類型時,該模式有助于對請求/處理事件進行建模。適合使用責任鏈模式的系統例子包括基于事件的系統、采購系統和運輸系統。

在責任鏈模式中,發送方可直接訪問鏈中的首個節點。若首個節點不能處理請求,則轉發給下一個節點,如此直到請求被某個節點處理或者整個鏈遍歷結束。這種設計用于實現發送方與接收方(多個)之間的解耦。

ATM機是責任鏈的一個例子。用于取放鈔票的槽口可看作是鏈的頭部。從這里開始,根據具體交易,一個或多個容器會被用于處理交易。這些容器可看作是鏈中的處理程序。

Java的servlet過濾器使用責任鏈模式對一個HTTP請求執行不同的動作(例如,壓縮和身份驗證)。Apple的Cocoa框架使用相同的模式來處理事件,比如,按鈕和手勢。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,677評論 25 708
  • 職責鏈 1.概述 你去政府部門求人辦事過嗎?有時候你會遇到過官員踢球推責,你的問題在我這里能解決就解決,不能...
    derivator_2閱讀 445評論 0 0
  • 第一章 Nginx簡介 Nginx是什么 沒有聽過Nginx?那么一定聽過它的“同行”Apache吧!Ngi...
    JokerW閱讀 32,740評論 24 1,002
  • 寫在前面的碎碎念 雖然對韓國并沒有如果追星族一樣的狂熱。因為離京較近,再加上目前便宜的出境費用,和放心大膽的買買買...
    桃小圈閱讀 714評論 0 2
  • 引言 就在我作為人母自我感覺良好,覺得自己是一個對孩子很有耐心、愛心的媽媽的時候,我今天打了孩子。 打孩子時,我自...
    松樹愛姜姜閱讀 239評論 1 0