大数据

Python设计模式 – 责任链模式

1 什么是责任链模式?

现在模拟一个场景:你是个高中生,有4个小弟A、B、C、D,分别擅长语文、数学、英语、物理,在每次考试中,他们都正好坐在你后面一排,你根据不同的科目把试卷传给相应的小弟,他就会帮你答题,就像下面这样:

这种方式的特点是:(1) 你得记住每个小弟擅长哪一科;(2)你和每个小弟都得有联系。放到代码里,“你”和“小弟”之间的耦合度就太高了,如果小弟流动性比较高,岂不是要经常修改“你”,如果项目大一点儿,这种维护工作是很费力不讨好的。

我们用责任链来优化一下,哪有老大这么操心的,这种事完全可以交给秘书去做嘛,你可以从四个小弟中挑一个秘书(比如你看上了A),以后不管考什么,你都只管把卷子递给A,他如果处理不了,就转交给B,以此类推,直到有人能处理或所有人都处理不了。
或者再优化一下,添加两个专职秘书DebugHandler(放在责任链的开始,专门负责传递试卷并写日志)和FinalHandler(放在责任链末尾,如果有未处理的任务,就抛异常)。为了使用尽量简单的代码说明问题,下文中的代码均只包含图中的实线路径。

我们观察一下上图,责任链模式的优缺点就显而易见了:

  • 优点:解藕了请求发送者和N个接收者之间的关系,请求发送者只需要记住第一个节点即可。
  • 缺点:每发送一次请求,都只有一个节点有实质作用,它之前节点的作用仅仅是让请求传递下去,从性能方面考虑,我们要避免过长的责任链带来的性能损耗。

2 常规责任链

有了思路,代码就好说了,常规责任链是酱婶儿的,要说的都在注释里了:

class BaseBro:
    """小弟的基类,将小弟共同的功能提取出来
    """
    def __str__(self):
        """演示传递过程用,你也可以给每个类定义一个名字属性"""
        return '竟然没人会做,丢掉它'

    def __init__(self, successor=None):
        """在初始化时可以指定继任者(链条的下一个节点)。
        如果没指定,那就是最后一个节点
        """
        self._successor = successor

    def handle(self, paper):
        """将试卷传递下去。
        每个小弟都有自己的handle,如果自己处理不了,就调用这个基类的handle传递试卷。
        """
        if self._successor is not None:
            self._successor.handle(paper)


class ABro(BaseBro):
    """小弟A,会做语文"""
    def __str__(self):
        return 'A'

    def handle(self, paper):
        if paper == '语文':
            print('小弟A把语文试卷做完啦~')
        else:
            print('A -->', str(self._successor))
            super().handle(paper)

# 其他小弟功能完全一样,只是科目不一样,代码略,对应的类名分别为:BBro, CBro, DBro

# 创建链条,小弟的顺序无关紧要
bro = ABro(BBro(CBro(DBro(BaseBro()))))
# 开始考试(map返回值是Iterator,需要包一层list才能执行)
list(map(bro.handle, ['物理', '历史', '数学', '语文', '英语']))

打印结果如下,注意第二科历史,谁都不会做,就什么都没做:

3 异步责任链

如果你的项目用到了异步,我们也可以改造一下,基于协程实现一个异步责任链(关于协程,请参考《廖雪峰的官方网站》)。这里只对协程做个极简的介绍。

3.1 协程极简介绍

协程就是一个特殊的生成器,它比生成器多出如下两个特点:

  • 这个生成器内是个无限循环
  • 这个生成器对象已前进到首个yield表达式(相当于启动协程)

定义一个无限循环的生成器好说,那怎么让生成器自己前进到首个yield表达式呢?当然你可以在用协程之前手动next(generator)一下,启动协程,我们这儿稍微高级一点儿,让协程自己启动,可以定义一个修饰器:

P.S 先不考虑3.4引进的asyncio和3.5引进的async/await,在说明一个问题的时候尽量不引入额外的概念,想着怎么把协程说明白就够头疼了。

def coroutine(func):
    """
    首先你得保证func是个无限循环的生成器。
    然后这个修饰器可以帮你启动生成器使其变成协程。
    """
    @functools.wraps(func)
    def wrapper(*args, **kw):
        generator = func(*args, **kw)
        next(generator)
        return generator
    return wrapper

3.2 基于协程的责任链

先捋一下思路:责任链的关键在于处理传递

  • 怎么处理:函数内部写处理逻辑就行了,在本例中就是一条print
  • 怎么传递:协程内的yieldsend(value)是关键,yield可以接收由自己.send(value)发过来的值,在一个协程内调用下个节点.send(value)可以向下个节点传递值。一条天然的管道有木有!

还是老样子,理清了思路,代码就简单了,想说的都在注释里:

# coroutine修饰器就是上小节中的代码

@coroutine
def a_bro(successor=None):
    """小弟A: 会语文"""
    while True:
        # yield负责接收数据
        paper = (yield)
        if paper == '语文':
            print('--> A:做语文')
        elif successor is not None:
            # send()负责发送数据
            print('A:不会做 -->')
            successor.send(paper)
        else:
            print('A: 没下一个小弟了,扔掉它~')

# 其他小弟的代码逻辑一样,只是科目不一样,略。。。

# 创建链条,顺序无关紧要
pipeline = a_bro(b_bro(c_bro(d_bro())))
# 考试啦,通过pipeline.send()给协程发送数据
list(map(pipeline.send, ['物理', '数学', '语文', '历史', '英语']))

有没有发现,代码清爽了很多,不用在定义传送机制了(常规责任链中的基类),协程天然可以传送接收数据。执行结果如下,同样注意试卷的传递过程,历史没人会做,就直接丢掉了:

(全文完)


参考:
(1)《Python编程实战-运用设计模式、并发和程序库创建高质量程序》
(2)《JavaScript设计模式与开发实战》