从抽象类开始,详解责任链模式
大家好,欢迎大家阅读设计模式专题。
今天我们继续介绍新的设计模式,和上次的链式模式不同,这一次要介绍的责任链模式不仅仅在Python当中有,在很多其他的语言当中同样支持,比如Java。Python和Java当中的这个设计模式是完全一样的,没有任何区别。
和之前介绍的设计模式相比,今天介绍的设计模式同样不难。我们非常容易想明白它的原理。
责任链
在我们日常的开发当中,经常会有上下游的依赖,比如当前我们需要开发一份新的数据,需要用到上游的某一份老数据才能生成。这时候需要我们写一个接口,然后上游也随之改造,把它们的新数据写到我们这个接口来。
这就带来了一个问题,也就是上下游耦合的问题,如果下游有一天变动了,上游也需要随着变动。如果是一对一的可能还好,如果一个上游存在多个下游,那么这会是一件很麻烦的事。每一个下游变动了,上游都需要调整。举个最简单的例子,比如下游想要做一个AB测试,想要一半流量进入A机器,一半流量进入B机器。这都需要上游配合。
这当然是很不合理的,在实际当中上游的数据可能是另外一个团队负责的,他们只对数据质量负责,并不一定会愿意配合。并且这样的配合也会导致效率低下。
为了解决这个问题,有了责任链的设计模式。我们把下游的响应方存入一条链路当中,上游在发送请求时不感知下游的接收情况以及相应细节。
说白了,用一句话来概括,就是用链路把下游串起来。我们来看一张图就明白了:
我们把图中的采购人员看成是数据上游的话,它只管提出请求,具体响应的人他是不知道的。根据申请金额的不同,审批的人员也会不一样。但是只会有一个人审批,如果当前人员审批不了就会上报,也就是交由后面一个节点处理。不得不说是非常形象了。
代码实现
那怎么把下游用一条链路串起来呢,其实也很简单,我们可以利用抽象类来实现。比如说我们可以这样:
class Handler:
def __init__(self, next=None):
self.next = next
def handle(self, request):
res = self.do_handle(request)
if not res and self.next:
self.next.handle(request)
def do_handle(self, request):
pass
这里的handler就是实现链式调用的关键,在handle当中会首先自己进行处理,如果处理失败那么会调用下一个handle来进行。Handle类中do_handle的方法是抽象方法,我们希望每一个继承Handle的子类都能实现自己的do_handle逻辑。
所以如果是用Java来实现的话,会非常清晰,因为在Java当中有抽象类的相关概念。由于Python原生没有抽象类的相关设定,所以会稍微隐晦一些,但是逻辑是也一样的。
在我们代码实现之前,我们先来介绍一个无关紧要的包。这个包就是Python当中的six,其实这个包也不算是无关紧要,通过它可以写出兼容Python2和Python3的代码。但是实际上由于现在Python2已经快扫进历史的垃圾堆了,所以有没有这个包都没有关系,这里我们还是尊重原作者的代码,保留了这么一个包。我们用这个包作为注解,完成了抽象基类的构建。
抽象基类
首先,我们先来看这个抽象基类。在原生Python当中,其实是没有抽象基类这么一个概念的。抽象基类其实就是含有抽象方法的基类。
我们之前在介绍golang当中interface用法的时候曾经介绍过,我们来简单回忆一下。拿比较常见的Java语言举例。
比如说这是一个抽象基类:
abstract class Mammal {
abstract void say();
String name() {
return "chengzhi";
}
}
对于这个类而言它是不可以直接创建实例的,因为我们可以看出来它有一个什么也没实现的方法say。这个方法前面加了一个关键字abstract即抽象的意思,表示这是一个抽象方法。类名的前面同样加了这个关键字,表示这是一个抽象类。
对于抽象类我们不能直接创建它的实例,我们只能创建实现了抽象类中抽象方法的子类的实例。
比如我们创建一个类实现它:
class Human extends Mammal{
@override
public void say() {
System.out.println("perfect");
}
}
我们可以看出来Human这个类继承了Mammal这个抽象类,并且实现了抽象类当中的方法say。所以我们可以创建它的实例。抽象类是实现多态的一种重要的方法,在强变量类型的语言当中,我们通过抽象类抽象出了多个子类共同的结构。这样我们就可以通过父类的指针调用各种子类的对象了,这是非常方便的。
但是Python当中不支持,为什么Python不支持呢?其实也很简单,因为Python是弱变量类型语言。变量赋值的时候对于类型根本没有限制,我们可以做任何调用。
举个例子,比如我们当下有A、B和C这三个类的实例。哪怕这三个类毫无关系,我们也可以用一个变量去分别接收这些实例然后调用同名的方法。
a = [A(), B(), C()]
for i in a:
i.say()
只要A、B和C这三个类当中都有叫做say的方法,那么上面这段逻辑就不会报错。因此Python非常灵活,可以说是几乎没有限制,那么当然也就没有必要设计这么一个抽象类的设定了。
但问题是我们开发的时候并不是这样的,比如说我们在设计的时候希望能够让类之间有继承的关系,并且希望继承了某个父类的类都能实现某一个方法。由于Python原生没有相关的设定,导致我们很难在编码的排查错误。比如说我们忘记实现了某个方法,由于Python没有编译的过程,我们只有运行到报错的地方才会遇到问题,这显然是不行的,会有很大的隐患。
所以Python开发者想了一个办法就是设计了一个装饰器,叫做abc。这里的abc不是随意起的名字,其实是abstract basement class的缩写。加上了这个装饰器之后,它会替我们去检查所有子类有没有实现抽象方法。如果没有的话,那么会报错提示我们。
我们用上six和abc这两个包之后实现出来的基类是这样的:
import abc
import six
@six.add_metaclass(abc.ABCMeta)
class Handler(object):
def __init__(self, successor=None):
self.successor = successor
def handle(self, request):
res = self.check_range(request)
if not res and self.successor:
self.successor.handle(request)
# 抽象方法
@abc.abstractmethod
def check_range(self, request):
pass
实现类
我们把抽象类相关的概念以及责任链的原理理解了之后,就可以很简单地写出来实现类了。
所谓的实现类也就是抽象类的子类,实现了各自的抽象方法。这样我们在每个节点当中调用self.successor.handle的时候才可以执行。因为每一个successor都是继承了同样一个抽象类的实现类,它们都必然实现了handle这个抽象方法。所以整个调用的过程就像是链条一样串联了起来。
在这个例子当中,我们用数字表示每个handler的处理范围。只有落在范围里的请求才会被响应,如果当前handler无法响应,那么就会调用successor的handle继续尝试。
class ConcreteHandler(Handler):
@staticmethod
def check_range(request):
if 0 <= request < 10:
print('request {} handled in handler 0'.format(request))
return True
class ConcreteHandler1(Handler):
start, end = 10, 20
def check_range(self, request):
if self.start <= request < self.end:
print('request {} handled in handler 1'.format(request))
return True
class ConcreteHandler2(Handler):
def check_range(self, request):
start, end = self.get_interval()
if start <= request < end:
print('request {} handled in handler 2'.format(request))
return True
@staticmethod
def get_interval():
return (20, 30)
class FallBackHandler(Handler):
@staticmethod
def check_range(request):
print('end of chain, no handler for {}'.format(request))
return False
这里要注意一下FallBackHandler这个类,这个类表示责任链的结尾,也就是它是最后一个节点。如果请求一直穿过前面的节点送到了它这里,就表示前面没有相应的handler,这个请求无法响应。
最后我们来看下实际运行的例子:
if __name__ == '__main__':
h0 = ConcreteHandler()
h1 = ConcreteHandler1()
h2 = ConcreteHandler2(FallBackHandler())
h0.successor = h1
h1.successor = h2
requests = [2, 5, 14, 22, 18, 3, 35, 27, 20]
for x in requests:
h0.handle(x)
我们可以看到我们创建了一系列handler的实例,然后将它们串联了起来。如果我们运行这段代码的话会发现我们不同的请求被不同的handler响应了。
到这里关于责任链这个设计模式的介绍就结束了,这个设计模式非常巧妙,将上游和下游分裂开来,彼此之间消除了耦合。但是这也并不是完美的,它也有自己的缺点,一个很明显的缺点就是这样的效率会比较低。因为一层层链路传递都是需要时间的,尤其是这些链路如果还要远程调用的话,那么每一层转换都会有延迟以及相应时间,所以整个系统的效率可能会不高。
还有呢就是我们调试的时候可能会不太方便,错误不太容易排查。除此之外还有一些其他的优缺点,大家可以自己思考一下,在下方留言。
衷心祝愿大家每天都有所收获。如果还喜欢今天的内容的话,请来一个三连支持吧~(点赞、关注、转发)
本文使用 mdnice 排版
- END -