责任链模式探究

ES2049
English

背景

责任链模式(又称职责链模式,The Chain of Responsibility Pattern),作为开发设计中常用的代码设计模式之一,属于行为模式中的一种,历来被我们开发所熟悉。

责任链模式也是实人 SDK 使用的主要设计模式之一,从通过 start 接口获取相关配置信息,到 upload 接口上传认证材料,后续通过 verify 接口提交认证获取认证结果,可以说将整个实人业务的逻辑以链的方式实现,上一个业务节点的结果作为下一个业务的开始,从而串起了整个SDK的核心逻辑。我们虽然在日常开发过程中看过很多设计模式,同时也或多或少应用在工程中,但像老话说的一样,想到不一定知道,知道不一定能做到,做出来又不代表能说出来,说的出来还不一定能写出来。如何将自己写过的代码,用到的设计模式翻译成文字,对开发者来说也是一个很有意思的事情和小挑战。

所以本篇旨在重新梳理一下自己印象中的设计模式,并诉诸文字,温故而知新。

什么是责任链模式

如上所述,责任链模式是一种理解上比较简单的行为设计模式,它允许开发者通过处理者链进行顺序发送,每个链节点在收到请求后,具备两种能力:

  1. 对其进行处理(消费)
  2. 将其传递给链上的下个处理者

当你想要让一个以上的对象有机会能处理某个请求时,就可以使用责任链模式。通过责任链模式,为某个请求创建一个对象链,每个对象链依序检查此请求,并对其进行处理,或者将它传给链中的下一个对象。

image.png

从某种生活场景中理解,就像患者去医院看病,传统上可能会经历从挂号到医生问诊再到抽血拍片再到医生复诊以及最终药房拿药的过程。

image.png

从生活经验上可以看出,责任链上每个节点的产物是不同的(当然也可以相同,但相同的话就没必要通过责任链去处理了,可能放在单个对象中会更合适),像链表结构一样,每个节点除了需要包含指向下一个链节点的索引以及必要时终止传递的能力外,还需要具备传递给下一个节点的产物的能力。如何针对不同的业务场景对链节点的产物进行抽象,也成为了代码编写中的一个问题,为什么会成为一个问题?因为使用责任链的一大优势就是我们可以动态地去新增或删除链节点以达到业务能力的扩展,如果我们对输出的产物定义的不够清晰,就会导致在做链式扩展时,相关的产物代码会变得更加复杂导致代码的可读性降低。

举个例子,在实人 SDK 的工程中,通过创建一个对象将业务链中所有的过程产物都包含进了该类中,类似如下代码:

RealIdentityChainOutputs {
        // start 过程产物
    public StartOutput mStartOutput;
        // upload 过程产物
    public UploadOutput mUploadOutput;
        // verify 过程产物
    public VerifyOutput mVerifyOutput;
        // submit 最终结果产物
    public SubmitOutput mSubmitOutput;
    
}

这样写的好处是,可以通过传递一个对象的方式,将过程产物统一通过一个类对象的方式传递,就像是我在医院拿了一个包含各种单据的文件袋,每次走完一个流程就将其中一个单据填满,进入下一个流程,简单方便。但存在的问题也很明显。

首先,会带来代码的可见性风险,最开始的几个链节点已经知道了后面几个链节点产物的数据结构,那是否就存在前几个节点有能力修改后面节点产物的可能?其次,如果在链传递过程中出现两个相同的产物对象,那按照目前的产物包装类,是很难「优雅」地去创建两个相同数据的对象的,是建一个List还是再新建一个相同类的对象?其三,每个节点都有结束当前流转流程的能力,也属于链流转最终产物中的一种结果,但放到上述包装类中的话,代表着某一个产物即为最终整个链的产物,这和当初定义这个包装类的初衷又是相违背的。当然,这些问题都是基于未来业务扩展的角度来考虑,针对实人比较简单的业务场景,是可用的。提出太多的问题,有「过度设计」之嫌。

所以责任链到底解决了什么问题?

  1. 前置检查,减少不必要的后置代码逻辑
  2. 发送者(sender)和接收者(receiver(s))的逻辑解耦,提高代码灵活性,这一点是最重要的
  3. 通过链路顺序传递请求,也使每一个链节点职责明确,符合单一职责原则
  4. 通过改变链内的成员或调动它们的次序,允许你动态地新增或删除,也提高了代码的灵活性

责任链模式代码的基本表达

我们来看一下责任链模式的 UML 图。

image.png

从最基本的 UML 图中可以看出责任链里一般包含4种角色:

  1. Client 客户端,定义链的规则和运行方式,根据具体业务场景动态生成链(所以链并不是固定不变的,可定制组合,并选择链头和链尾)
  2. Handler 处理者接口,主要用于声明具体处理者的通用能力,一般包含抽象处理能力以及指向下一个节点的能力
  3. BaseHandler 基本处理者,这是一个可有可无的角色,可以根据业务场景将具体处理者中的一些共有逻辑放到该类当中
  4. ConcreteHandlers 具体处理者,构成了链中的处理节点,核心职能是处理请求,决定请求是在该节点消费掉还是沿着链继续传递(具体处理者之间独立且不可变)

可以看出,责任链模式核心的逻辑是处理和传递,同时具备由外部灵活定制的能力。

通过 UML 图也可以看出责任链的固定的几步实现方式:

  1. 声明 Handler 接口定义节点处理的接口
  2. 通过创建抽象处理者基类消除具体处理者之间的重复模版代码
  3. 依次创建具体处理者子类及其实现方法,通过具体处理类决定当前处理类是否要消费这个请求或者沿着链继续传递
  4. 最终体现到业务层,由 Client 对象自行组装实现的链节点,实现逻辑处理和调用对象的解耦
// 处理者接口声明了一个创建处理者链的方法。还声明了一个执行请求的方法。
interface Handler is
    method handle()
    method setNext(h: Handler)


// 简单组件的基础类。
abstract class BaseHandler implements Handler is

    field canHandle: boolean

    // 如果组件能处理请求,则处理
    method handle() is
            doCommonThings
    method setNext(h: Handler)



// 原始组件应该能够使用帮助操作的默认实现
class ConcreteHandlerA extends BaseHandler is
    // ...


// 复杂处理者可能会对默认实现进行重写
class ConcreteHandlerB extends BaseHandler is

    method handle() is
        if (canHandle)
            // 处理者B的处理方式
        else
            super.handle()

// ...同上...
class ConcreteHandlerC extends BaseHandler is
    field wikiPageURL: string

    method handle() is
        if (canHandle)
                // 处理者C的处理方式
        else
            super.handle()


// Client
class Application is
    // 每个程序都能以不同方式对链进行配置。
    method cor() is
        handlerA = new ConcreteHandlerA()
        handlerB = new ConcreteHandlerB()
        handlerC = new ConcreteHandlerC()
        // ...
        handlerA.setNext(handlerB)
        handlerB.setNext(handlerC)

适用场景

通过上面的描述我们也可以看出,其实只要涉及到逻辑顺序处理的,都可以使用责任链模式来处理。但从实际场景出发,决定是否使用该模式要考虑一下两个因素:

  1. 场景是不是够复杂,逻辑链是不是很长
  2. 是否有灵活变化的业务变化场景需求

同时还要注意使用责任链不可避免带来的三个问题:

  1. 处理者的数量问题。对链中请求处理者的遍历,如果处理者太多那么遍历必定会影响性能,特别是在一些递归调用中,所以要慎重
  2. 代码出现问题时,不容易观察运行时的特征,有碍于排查问题
  3. 需要 cover 请求即使传递到链尾端也一直没被处理,从而导致的一些异常问题

下面借助 Android 系统中和 ViewGroup 事件传递消费机制相关的例子来具体说明责任链使用的方式。
先看我们在屏幕上点击一次的行为在 Android 源码中的传递路径。

image.png

可以很清晰的看到,Android 系统的事件传递和分发也是通过链的方式来实现的。如果将 ActivityViewGroupsView 三者作为具体处理者,通过他们自身的 dispatchTouchEvent() 方法对事件进行消费和传递,那就是一个标准的责任链模式。

// 伪代码逻辑
public boolean dispatchTouchEvent(MotionEvent ev) {

    if(onInterceptTouchEvent(ev)) {
            // onInterceptTouchEvent 方法作为是否需要在本处理者中被消费的判断,如果为 true,则在本控件中消费
            this.onTouchEvent(ev);
    } else {
            // 本控件不被拦截,则传递给下一个控件的 dispatchTouchEvent 方法当中
            next.dispatchTouchEvent(ev);
    }

}

细心的同学也许发现了一个问题,就是当用户的点击事件传递到控件最顶端的 View 后,如果在该 View 中 touch 事件还没有被消费掉,那么它会依照原来的传递过来的链路重新回到调用链最初开始的地方,即从 ViewonTouch() 或者 onTouchEvent() 重新回到 ActivityonTouchEvent() 方法中。

前文在描述责任链模式可能存在的问题的时候,我们也提到过,该模式有一个特别需要注意的点是,如果请求到链的末端还没有被处理的话极有可能会让代码出现稳定性问题。而 Android 通过重新将请求交回给最初的链节点方式来解决这个问题,这样做的好处是:

  1. 请求不论走到哪一步都可控(即一定会被处理,即使可能最终是空实现)
  2. 让和 UI 相关的功能类具备一致的行为方式(使 ActivityViewGroupView 均具备分发和消费能力)

从图中也可以看出,以用户输入作为请求的起点,该请求在任何一个节点都有可能被消费掉,是一个典型的责任链设计。

再列举一些日常开发过程中用到的责任链模式的场景,细节不表:

  • 登录模块(通过责任链进行各种前置账号校验的逻辑组合调整)
  • 账务报销系统(以权限的不同来作为是否进行下一级处理的审批)
  • 邮件过滤系统(以邮件属性,诸如重要邮件、广告邮件、垃圾邮件、病毒邮件等邮件类型来进行过滤和拦截)

责任链模式与其他模式的关系

设计模式不是单指由某一个设计模式独立存在于代码工程,而是多种设计模式根据不同的业务场景进行组合、变形、适配后的一个非常「丰富」的产物,那么和责任链模式关系比较密切,或者说可以互相配合的设计模式有哪些呢?

通过责任链模式中两种角色,发送者和接收者,可以看出它和命令模式以及中介者模式是有一定相似性的,像命令模式在发送者和请求者之间也是建立了单向连接,区别在于命令模式更倾向于解决参数化对象以及操作回滚等场景,当然责任链模式可以用命令模式来实现。

而中介者模式则将发送者和接收者之间的直接连接改为中介对象进行连接,减少了对象间混乱的依赖关系。
设计模式之间的关系是配合、转化的关系,其中的细节非常多,非本篇文章核心,这里暂且不表。提到这一点也是为了让有兴趣的读者自行探索。

总结

我们使用设计模式基本都是从代码扩展性、代码稳定性和代码可读性等角度去考虑的。

对于责任链模式,它很好地解决了复杂逻辑场景下前后逻辑的耦合问题,同时对于需要灵活应对多变的业务场景,也是一种具有参考价值的解决方式。我们使用该模式时,需要特别注意对于中间链节点消费后抛出后的行为以及到达链结尾请求没有被处理的特殊场景。

写在最后

最后借用《Head First Design Patterns》一书中对设计模式如何使用的表述,做一个收尾,深以为然。

  1. 为实际需要的扩展使用模式,不要只是为了假想的需要而使用模式
  2. 简单才是王道,如果不用模式就能设计出更简单的方案,那就去干吧
  3. 模式是工具而不是规则,需要被适当地调整以符合实际的需求

参考

作者:ES2049 / 拂晓
文章可随意转载,但请保留此原文链接。
非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com
阅读 387

ES2049 Studio
阿里巴巴 - CRO 技术部 - 体验技术
3.2k 声望
2.8k 粉丝
0 条评论
你知道吗?

3.2k 声望
2.8k 粉丝
宣传栏