責(zé)任鏈模式
開發(fā)一個(gè)應(yīng)用時(shí),多數(shù)時(shí)候我們都能預(yù)先知道哪個(gè)方法能處理某個(gè)特定請(qǐng)求居暖。然而乃秀,情況并非總是如此肛著。例如,想想任意一種廣播計(jì)算機(jī)網(wǎng)絡(luò)跺讯,例如最早的以太網(wǎng)實(shí)現(xiàn)(請(qǐng)參考網(wǎng)頁[t.cn/RqrTp0Y])枢贿。在廣播計(jì)算機(jī)網(wǎng)絡(luò)中,會(huì)將所有請(qǐng)求發(fā)送給所有節(jié)點(diǎn)(簡(jiǎn)單起見刀脏,不考慮廣播域)局荚,但僅對(duì)所發(fā)送請(qǐng)求感興趣的節(jié)點(diǎn)會(huì)處理請(qǐng)求。加入廣播網(wǎng)絡(luò)的所有計(jì)算機(jī)使用一種常見的媒介相互連接,比如耀态,下圖中的三個(gè)節(jié)點(diǎn)通過光纜連接起來轮傍。
如果一個(gè)節(jié)點(diǎn)對(duì)某個(gè)請(qǐng)求不感興趣或者不知道如何處理這個(gè)請(qǐng)求,可以執(zhí)行以下兩個(gè)操作首装。
- 忽略這個(gè)請(qǐng)求创夜,什么都不做
- 將請(qǐng)求轉(zhuǎn)發(fā)給下一個(gè)節(jié)點(diǎn)
節(jié)點(diǎn)對(duì)一個(gè)請(qǐng)求的反應(yīng)方式是實(shí)現(xiàn)的細(xì)節(jié)。然而仙逻,我們可以使用廣播計(jì)算機(jī)網(wǎng)絡(luò)的類比來理解責(zé)任鏈模式是什么驰吓。責(zé)任鏈(Chain of Responsibility)模式用于讓多個(gè)對(duì)象來處理單個(gè)請(qǐng)求時(shí),或者用于預(yù)先不知道應(yīng)該由哪個(gè)對(duì)象(來自某個(gè)對(duì)象鏈)來處理某個(gè)特定請(qǐng)求時(shí)系奉。其原則如下所示檬贰。
(1) 存在一個(gè)對(duì)象鏈(鏈表、樹或任何其他便捷的數(shù)據(jù)結(jié)構(gòu))缺亮。
(2) 我們一開始將請(qǐng)求發(fā)送給鏈中的第一個(gè)對(duì)象翁涤。
(3) 對(duì)象決定其是否要處理該請(qǐng)求。
(4) 對(duì)象將請(qǐng)求轉(zhuǎn)發(fā)給下一個(gè)對(duì)象萌踱。
(5) 重復(fù)該過程葵礼,直到到達(dá)鏈尾。
在應(yīng)用級(jí)別虫蝶,不用討論光纜和網(wǎng)絡(luò)節(jié)點(diǎn)章咧,而是可以專注于對(duì)象以及請(qǐng)求的流程。下圖展示了客戶端代碼如何將請(qǐng)求發(fā)送給應(yīng)用的所有處理元素(又稱為節(jié)點(diǎn)或處理程序)能真,經(jīng)www.sourcema-king.com允許使用(請(qǐng)參考網(wǎng)頁[t.cn/RqrTYuB])赁严。
注意,客戶端代碼僅知道第一個(gè)處理元素粉铐,而非擁有對(duì)所有處理元素的引用疼约;并且每個(gè)處理元素僅知道其直接的下一個(gè)鄰居(稱為后繼),而不知道所有其他處理元素蝙泼。這通常是一種單向關(guān)系程剥,用編程術(shù)語來說是一個(gè)單向鏈表,與之相反的是雙向鏈表汤踏。單向鏈表不允許雙向地遍歷元素织鲸,雙向鏈表則是允許的。這種鏈?zhǔn)浇M織方式大有用處:可以解耦發(fā)送方(客戶端)和接收方(處理元素)(請(qǐng)參考[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)
現(xiàn)實(shí)生活的例子
ATM機(jī)以及及一般而言用于接收/返回鈔票或硬幣的任意類型機(jī)器(比如,零食自動(dòng)販賣機(jī))都使用了責(zé)任鏈模式哗脖。機(jī)器上總會(huì)有一個(gè)放置各種鈔票的槽口瀑踢,如下圖所示(經(jīng)www.sourcemaking.com允許使用)扳还。
鈔票放入之后,會(huì)被傳遞到恰當(dāng)?shù)娜萜鞒髫病bn票返回時(shí)氨距,則是從恰當(dāng)?shù)娜萜髦蝎@取(請(qǐng)參考網(wǎng)頁[t.cn/RqrTYuB]和網(wǎng)頁[t.cn/RqrTnts])棘劣。我們可以把這個(gè)槽口視為共享通信媒介俏让,不同的容器則是處理元素。結(jié)果包含來自一個(gè)或多個(gè)容器的現(xiàn)金呈础。例如舆驶,在上圖中橱健,我們看到在從ATM機(jī)取175美元時(shí)會(huì)發(fā)生什么而钞。
軟件的例子
我試過尋找一些使用責(zé)任鏈模式的Python應(yīng)用的好例子,但是沒找到拘荡,很可能是因?yàn)镻ython程序員不使用這個(gè)名稱臼节。因此,很抱歉珊皿,我將使用其他編程語言的例子作為參考网缝。
Java的servlet過濾器是在一個(gè)HTTP請(qǐng)求到達(dá)H標(biāo)處理程序之前執(zhí)行的一些代碼片段。在使用servlet過濾器時(shí)蟋定,有一個(gè)過濾器鏈粉臊,其中每個(gè)過濾器執(zhí)行一個(gè)不同動(dòng)作(用戶身份驗(yàn)證、記H志驶兜、數(shù)據(jù)壓縮等)扼仲,并且將請(qǐng)求轉(zhuǎn)發(fā)給下一個(gè)過濾器直到鏈結(jié)束;如果發(fā)生錯(cuò)誤(例如抄淑,連續(xù)三次身份驗(yàn)證失斖佬住)則跳出處理流程(請(qǐng)參考網(wǎng)頁[t.cn/RqrTukH])。
Apple的Cocoa和Cocoa Touch框架使用責(zé)任鏈來處理事件肆资。在某個(gè)視圖接收到一個(gè)其并不知道如何處理的事件時(shí)矗愧,會(huì)將事件轉(zhuǎn)發(fā)給其超視圖,直到有個(gè)視圖能夠處理這個(gè)事件或者視圖鏈結(jié)束(請(qǐng)參考網(wǎng)頁[t.cn/RqrTrzK])郑原。
應(yīng)用案例
通過使用責(zé)任鏈模式唉韭,我們能讓許多不同對(duì)象來處理一個(gè)特定請(qǐng)求。在我們預(yù)先不知道應(yīng)該由哪個(gè)對(duì)象來處理某個(gè)請(qǐng)求時(shí)犯犁,這是有用的属愤。其中一個(gè)例子是采購系統(tǒng)。在采購系統(tǒng)中栖秕,有許多核準(zhǔn)權(quán)限春塌。某個(gè)核準(zhǔn)權(quán)限可能可以核準(zhǔn)在一定額度之內(nèi)的訂單,假設(shè)為100美元。如果訂單超過了100美元只壳,則會(huì)將訂單發(fā)送給鏈中的下一個(gè)核準(zhǔn)權(quán)限俏拱,比如能夠核準(zhǔn)在200美元以下的訂單,等等吼句。
另一個(gè)責(zé)任鏈可以派上用場(chǎng)的場(chǎng)景是锅必,在我們知道可能會(huì)有多個(gè)對(duì)象都需要對(duì)同一個(gè)請(qǐng)求進(jìn)行處理之時(shí)。這在基于事件的編程中是常有的事情惕艳。單個(gè)事件搞隐,比如一次鼠標(biāo)左擊,可被多個(gè)事件監(jiān)聽者捕獲远搪。
不過應(yīng)該注意劣纲,如果所有請(qǐng)求都能被單個(gè)處理程序處理,責(zé)任鏈就沒那么有用了谁鳍,除非確實(shí)不知道會(huì)是哪個(gè)程序處理請(qǐng)求癞季。這一模式的價(jià)值在于解耦√惹保客戶端與所有處理程序(一個(gè)處理程序與所有其他處理程序之間也是如此)之間不再是多對(duì)多關(guān)系绷柒,客戶端僅需要知道如何與鏈的起始節(jié)點(diǎn)(標(biāo)頭)進(jìn)行通信。
下圖演示了緊耦合與松耦合之間的區(qū)別心涮因。松耦合系統(tǒng)背后的考慮是簡(jiǎn)化維護(hù)废睦,并讓我們易于理解系統(tǒng)的工作原理(請(qǐng)參考網(wǎng)頁https://infomgmt.wordpress.com/2010/02/18/a-visual-respresen-tation-of-coupling/)。
數(shù)據(jù)耦合(data coupling)养泡、特征耦合(stamp coupling)嗜湃、控制耦合(control coupling)、共用耦合(common coupling)和內(nèi)容耦合(content coupling)這幾個(gè)概念的含義可參考Wikipedia詞條 https://en.wikipedia.org/wiki/Coupling_(computer_programming)瓤荔。 ——譯者注
實(shí)現(xiàn)
使用Python實(shí)現(xiàn)責(zé)任鏈模式有許多種方式净蚤,但是我最喜歡的實(shí)現(xiàn)是Vespe Savikko所提出的(請(qǐng)參考網(wǎng)頁[t.cn/RqruSj1])。Vespe的實(shí)現(xiàn)以地道的Python風(fēng)格使用動(dòng)態(tài)分發(fā)來處理請(qǐng)求(請(qǐng)參考網(wǎng)頁[t.cn/RqruWFp])输硝。
我們以Vespe的實(shí)現(xiàn)為參考實(shí)現(xiàn)一個(gè)簡(jiǎn)單的事件系統(tǒng)今瀑。下面是該系統(tǒng)的UML類圖。
Event類描述一個(gè)事件点把。為了讓它簡(jiǎn)單一點(diǎn)橘荠,在我們的案例中一個(gè)事件只有一個(gè)name屬性。
class Event:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
Widget類是應(yīng)用的核心類郎逃。UML圖中展示的parent聚合關(guān)系表明每個(gè)控件都有一個(gè)到父對(duì)象的引用哥童。按照約定,我們假定父對(duì)象是一個(gè)Widget實(shí)例褒翰。然而贮懈,注意匀泊,根據(jù)繼承的規(guī)則,任何Widget子類的實(shí)例(例如朵你,MsgText的實(shí)例)也是Widget實(shí)例各聘。parent的默認(rèn)值為None。
class Widget:
def __init__(self, parent=None):
self.parent = parent
handle()方法使用動(dòng)態(tài)分發(fā)抡医,通過hasattr()和getattr()決定一個(gè)特定請(qǐng)求(event)應(yīng)該由誰來處理躲因。如果被請(qǐng)求處理事件的控件并不支持該事件,則有兩種回退機(jī)制忌傻。如果控件有parent大脉,則執(zhí)行parent的handle()方法。如果控件沒有parent水孩,但有handle_default()方法镰矿,則執(zhí)行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)
此時(shí)荷愕,你可能已明臼為什么UML類圖中Widget與Event類僅是關(guān)聯(lián)關(guān)系而已(不是聚合或組合關(guān)系)衡怀。關(guān)聯(lián)關(guān)系用于表明Widget類知道Event類,但對(duì)其沒有任何嚴(yán)格的引用安疗,因?yàn)槭录H需要作為參數(shù)傳遞給handle()。
MainWindow够委、MsgText和SendDialog是具有不同行為的控件荐类。我們并不期望這三個(gè)控件都能處理相同的事件,即使它們能處理相同事件茁帽,表現(xiàn)出來也可能是不同的玉罐。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()函數(shù)展示如何創(chuàng)建一些控件和事件铁追,以及控件如何對(duì)那些事件作出反應(yīng)季蚂。所有事件都會(huì)被發(fā)送給所有控件。注意其中每個(gè)控件的父子關(guān)系琅束。sd對(duì)象(SendDialog的一個(gè)實(shí)例)的父對(duì)象是mw(MainWindow的一個(gè)實(shí)例)扭屁。然而,并不是所有對(duì)象都需要一個(gè)MainWindow實(shí)例的父對(duì)象涩禀。例如料滥,msg對(duì)象(MsgText的一個(gè)實(shí)例)是以sd作為父對(duì)象。
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
從輸出中我們能看到一些有趣的東西葵腹。例如高每,發(fā)送一個(gè)down事件給MainWindow,最終被MainWindow默認(rèn)處理函數(shù)處理践宴。另一個(gè)不錯(cuò)的用例是觉义,雖然close事件不能被SendDialog和MsgText直接處理,但所有close事件最終都能被MainWindow正確處理浴井。這正是使用父子關(guān)系作為一種回退機(jī)制的優(yōu)美之處晒骇。
如果你想在這個(gè)事件例子上花費(fèi)更多時(shí)間發(fā)揮自己的創(chuàng)意,可以替換這些愚蠢的print語旬磺浙,針對(duì)羅列出來的事件添加一些實(shí)際的行為洪囤。當(dāng)然,并不限于羅列出來的事件撕氧。隨意添加一些你喜歡的事件匪蝙,做一些有用的事情!
另一個(gè)練習(xí)是在運(yùn)行時(shí)添加一個(gè)MsgText實(shí)例蒙谓,以MainWindow為其父芝雪。這個(gè)有難度嗎?也挑個(gè)事件類型來試試(為一個(gè)已有控件添加一個(gè)新的事件)不脯,哪個(gè)更難府怯?
小結(jié)
本章中,我們學(xué)習(xí)了責(zé)任鏈設(shè)計(jì)模式防楷。在尤法預(yù)先知道處理程序的數(shù)量和類型時(shí)牺丙,該模式有助于對(duì)請(qǐng)求/處理事件進(jìn)行建模。適合使用責(zé)任鏈模式的系統(tǒng)例子包括基于事件的系統(tǒng)复局、采購系統(tǒng)和運(yùn)輸系統(tǒng)冲簿。
在責(zé)任鏈模式中,發(fā)送方可直接訪問鏈中的首個(gè)節(jié)點(diǎn)亿昏。若首個(gè)節(jié)點(diǎn)不能處理請(qǐng)求峦剔,則轉(zhuǎn)發(fā)給下一個(gè)節(jié)點(diǎn),如此直到請(qǐng)求被某個(gè)節(jié)點(diǎn)處理或者整個(gè)鏈遍歷結(jié)束角钩。這種設(shè)計(jì)用于實(shí)現(xiàn)發(fā)送方與接收方(多個(gè))之間的解耦吝沫。
ATM機(jī)是責(zé)任鏈的一個(gè)例子。用于取放鈔票的槽口可看作是鏈的頭部彤断。從這里開始野舶,根據(jù)具體交易,一個(gè)或多個(gè)容器會(huì)被用于處理交易宰衙。這些容器可看作是鏈中的處理程序平道。
Java的servlet過濾器使用責(zé)任鏈模式對(duì)一個(gè)HTTP請(qǐng)求執(zhí)行不同的動(dòng)作(例如,壓縮和身份驗(yàn)證)供炼。Apple的Cocoa框架使用相同的模式來處理事件一屋,比如窘疮,按鈕和手勢(shì)。