責任鏈模式
開發一個應用時,多數時候我們都能預先知道哪個方法能處理某個特定請求。然而,情況并非總是如此。例如,想想任意一種廣播計算機網絡,例如最早的以太網實現(請參考網頁[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框架使用相同的模式來處理事件,比如,按鈕和手勢。