python设计模式11责任链(Chain of Responsibility)

在开发应用程序时,大多数时候我们都知道哪个方法应该提前满足某个特定的请求。然而,情况并不总是这样的。例如,想想任何广播计算机网络,如原始的以太网实现。在广播计算机网络中,所有的请求都被发送到所有的节点(为了简单起见,广播域被排除在外),但只有对所发请求感兴趣的节点才会处理它。

所有参与广播网络的计算机都是通过共同的媒介(如连接所有节点的电缆)相互连接的。如果一个节点不感兴趣或不知道如何处理请求,它可以执行以下行动。

  • 忽略该请求,什么也不做
  • 将请求转发给下一个节点

当我们想给多个对象机会来满足一个请求时,或者当我们事先不知道哪个对象(来自一个对象链)应该处理一个特定的请求时,就会用到责任链模式。

  • 首先向链上的第一个对象发送一个请求
  • 该对象决定它是否应该满足该请求
  • 该对象将请求转发给下一个对象
  • 这个过程重复进行,直到我们到达链的末端。

客户端代码只知道第一个处理元素,而不是对所有处理元素的引用,每个处理元素只知道其紧邻的下一个邻居(称为继承者),而不知道其他每个处理元素。这通常是一种单向的关系,在编程方面,这意味着单链列表与双链列表的对比;单链列表不允许双向导航,而双链列表则允许。这种链式组织的使用是有原因的。它实现了发送者(客户端)和接收者(处理元素)之间的解耦。

现实世界的例子

在软件中,Java的servlet过滤器是在HTTP请求到达目标之前执行的代码片段。在使用servlet过滤器时,有一连串的过滤器。每个过滤器执行不同的动作(用户认证、记录、数据压缩等等),并将请求转发给下一个过滤器,直到链子用完为止,或者在出现错误时中断流程--例如,认证连续三次失败。

苹果的Cocoa和Cocoa Touch框架,使用责任链来处理事件。当视图收到不知道如何处理的事件时,它会把事件转发给它的超级视图。这样一直持续到一个视图有能力处理该事件或视图链用完为止。

应用

通过使用责任链模式,我们为一些不同的对象提供机会来满足特定的请求。当我们不知道哪个对象应该事先满足一个请求时,这很有用。比如采购系统。在采购系统中,有许多审批机构。一个审批机构可能能够批准一定价值以内的订单,比方说100美元。如果订单的金额超过100美元,该订单就会被送到链上的下一个审批机构,该机构可以审批高达200美元的订单,以此类推。

责任链有用的另一种情况是,当我们知道一个以上的对象可能需要处理一个请求时。这就是基于事件的编程中的情况。一个单一的事件,比如鼠标左键,可以被一个以上的监听器捕获。

需要注意的是,如果所有的请求都可以由一个处理元素来处理,那么责任链模式就不是很有用,除非我们真的不知道是哪个元素。这种模式的价值在于它所提供的解耦性。客户端和所有处理元素之间不是多对多的关系(处理元素和所有其他处理元素之间的关系也是如此),而是只需要知道如何与链的起点(头部)通信。

下图说明了紧耦合和松耦合的区别。松散耦合系统背后的想法是简化维护,使我们更容易理解它们的功能。

实现

有很多方法可以在Python中实现责任链,但我最喜欢的实现是Vespe Savikko。Vespe的实现使用Pythonic风格的动态调度来处理请求(http://j.mp/ddispatch)。

让我们以Vespe的实现为指导,实现一个简单的、基于事件的系统。下面是该系统的UML类图。

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 = f'handle_{event}' 
        if hasattr(self, handler): 
            method = getattr(self, handler) 
            method(event) 
        elif self.parent is not None: 
            self.parent.handle(event) 
        elif hasattr(self, 'handle_default'): 
            self.handle_default(event) 

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

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

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

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

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

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

if __name__ == '__main__': 
    main()

事件类描述了事件。我们将保持简单,所以在我们的案例中,一个事件只有一个名字。

Widget类是应用程序的核心类。在UML图中显示的父集合表明每个Widget可以有一个对父对象的引用,按照惯例,我们假定它是一个Widget实例。然而,请注意,根据继承规则,Widget的任何子类的实例(例如,MsgText的实例)也是Widget的实例。parent的默认值是None。

handle()方法通过hasattr()和getattr()使用动态调度来决定谁是一个特定请求(事件)的处理者。如果被要求处理一个事件的部件不支持它,有两个回退机制。如果该部件有一个父部件,那么父部件的 handle() 方法被执行。如果该小组件没有父级但有handle_default()方法,则执行handle_default()。

在这一点上,你可能已经意识到为什么Widget和Event类在UML类图中只有关联(没有聚合或组合关系)。关联是用来表明Widget类知道Event类,但对它没有任何严格的引用,因为一个事件只需要作为参数传递给handle()。

MainWIndow、MsgText和SendDialog都是具有不同行为的窗口部件。并非所有这三个窗口部件都要能处理相同的事件,即使它们能处理相同的事件,它们的行为也可能不同。MainWindow只能处理关闭和默认事件。

main()函数展示了我们如何创建一些小部件和事件,以及小部件如何对这些事件做出反应。所有的事件都被发送到所有的小部件上。注意每个widget的父子关系。sd对象(SendDialog的一个实例)的父对象是mw对象(MainWindow的一个实例)。然而,并不是所有的对象都需要有一个父对象。

在输出中我们可以看到一些有趣的东西。例如,向MainWindow发送一个down事件,最终被默认的MainWindow处理程序处理了。另一个很好的例子是,尽管关闭事件不能被SendDialog和MsgText直接处理,但所有的关闭事件最终都被MainWindow正确处理了。这就是使用父子关系作为回退机制的好处。

如果你想在事件的例子上花更多的创造性时间,你可以替换掉那些愚蠢的打印语句,为列出的事件添加一些实际的行为。当然,你并不局限于所列的事件。只要添加你喜欢的事件,并让它做一些有用的事情就可以了

小结

在本章中,我们介绍了责任链设计模式。当处理程序的数量和类型事先并不清楚时,这种模式对于建模请求和/或处理事件是非常有用的。适合使用责任链的系统的例子是基于事件的系统、采购系统和运输系统。

在责任链模式中,发送者可以直接访问责任链的第一个节点。如果第一个节点不能满足请求,它就把请求转发给下一个节点。这种情况一直持续到请求被某个节点满足或整个链条被穿越为止。这种设计被用来实现发送方和接收方之间的松散耦合。

ATM是责任链的一个例子。用于所有纸币的单一插槽可以被认为是链的头部。从这里开始,根据不同的交易,一个或多个贮藏器被用来处理交易。贮藏器可以被认为是链的处理元素。

Java的servlet过滤器使用责任链模式对HTTP请求进行不同的操作(例如,压缩和认证)。苹果的Cocoa框架使用同样的模式来处理事件,如按下按钮和手指的手势。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容