职责链模式(Chain of Responsibility Pattern)为请求创建了一个接受者对象的链,使得多个接受者对象都有机会处理该请求,从而将请求的发送者与接受者之间解耦。请求沿着该职责链传递,直到遇到能处理它的对象为止,其过程类似于递归调用。在设计模式的定义中,职责链模式属于行为型模式。
这里简单做个扩展,设计模式根据使用目的的不同,被分为三大类:创建型模式、结构型模式以及行为型模式。创建型模式主要用于创建对象,是对对象实例化过程的一种抽象;结构型模式处理类和对象的组合,从而得到相对更复杂的类;行为型模式主要用于描述类或对象的交互以及职责分配。其实不管是哪种设计模式,其本质都是借助于面向对象的编程思想,通过封装、继承、多态将程序的耦合性降低。
应用场景
职责链在现实生活中有较多体现。假设你是一名公司的普通员工,你需要提出休假申请,休假的长短需要不同权限、职级的人进行审批。如果你就请个半天小假,可能部门小组长就给你审批通过了;如果你需要请个3、5天假,可能就需要小组长将你的申请上报给主管进行审批;如果你确实心情太糟糕,需要一个月以上的时间来放空自己,可能你的主管也无权审批了,遂上报至总经理等更高职级的人来决定你是否能获得休假。当然了,除非你十分优秀,如果你想申请个1-2年的假期,那审批请求可能就直接转到HR
了。
职责链模式就是如此,首先有多个对象共同对一个任务进行处理,对象之间用类似于链式结构保持,每个对象都知道自己的下一个对象,如果该对象能处理任务,就处理并结束返回,否则添加一些操作后将任务传递给自己的下一个对象。整个过程,提出请求的那一方无需关心最终是谁处理了任务。
从程序的角度而言,职责链只要求提出请求的客户端负责组装链式结构,将对应的业务逻辑、函数包装到每个职责链对象节点,而后等待返回值即可。
如果你是一名前端工程师,不同于熟悉的发布订阅模式,职责链模式会有种很陌生,但又似曾相识的错觉。
实际上,koa
,express
等中间件都可以看做是职责链模式的体现。在中间件中,我们需要:
- 调用
use
函数进行注册 - 当某个中间件接收到处理的数据时,注册的中间件在执行过程中依次被调用,每个中间件的输入都是上一个中间件的输出
- 每个中间件都可以停止当前数据的处理,只需要不调用
next
或返回错误即可
个人也觉得,用职责链模式去解释DOM
的冒泡、捕获的运行过程也能说得通,当你点击某个UI元素,其事件沿着DOM tree
(本质上也是一种链)向父/子元素进行传递,如果该元素绑定了事件回调,则完成了事件的处理。但从设计思想出发,基于DOM
的冒泡、捕获可能更多考虑的是灵活性、事件代理以及DOM
操作的内存,这样一想可能还是享元模式更合适(享元模式通过共享大量的细粒度的对象,减少对象的数量,从而减少对象的内存,提高应用程序的性能)。
其次,在web
服务中添加身份验证、授权
等逻辑时,如果随着需求的不断更迭,我们的验证方法越来越复杂,则可以考虑利用AOP + 职责链
(AOP
的简单理解可以参考我的这篇博客)模式进行方法的重构,定义一条链路,将验证步骤的逻辑抽象出来,注册到链路中,然后在具体定义每一个职责链节点处理完成后的返回状态,来确定是继续处理还是提前终止。
代码示例
这里截取曾探老师的《JavaScript设计模式与开发实践》中的例子进行解释,更多细节也可以参看该书籍,相信你看完会有更深的体会。
假设我们负责一个售卖手机的电商网站,经过分别交纳500元定金和200元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。
公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过500元定金的用户会收到100元的商城优惠券,200元定金的用户可以收到50元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。
在写代码前,先定义3个字段:
orderType
:订单类型,1表示500定金支付,2表示200定金支付,3表示普通支付pay
:是否支付过定金,true表示已支付,如果用户的订单类型为1(500定金支付)但未支付定金,则将改订单类型降级为3(普通支付)stock
:手机库存数量,仅对普通支付用户有效。
据此,洋洋洒洒写出下面流程代码:
const order = function (orderType, pay, stock) {
if (orderType === 1) { // 500 定金支付
if (pay) {
console.log("500元预购,100元优惠券已到账...")
} else {
if (stock > 0) {
console.log("普通购买,无优惠券...")
} else {
console.log("购买失败,手机库存不足...")
}
}
} else if (orderType === 2) { // 200 定金支付
if (pay) {
console.log("200元预购,50元优惠券已到账...")
} else {
if (stock > 0) {
console.log("普通购买,无优惠券...")
} else {
console.log("购买失败,手机库存不足...")
}
}
} else { // 普通购买
if (stock > 0) {
console.log("普通购买,无优惠券...")
} else {
console.log("购买失败,手机库存不足...")
}
}
}
order(1, true, 200); // 500元预购,100元优惠券已到账...
order(2, false, 100); // 普通购买,无优惠券...
order(3, true, 0); // 购买失败,手机库存不足...
相信没人会欣赏上面的代码吧,写时一时爽,改时火葬场
。下面用职责链模式简单重构:这边涉及到3种不同的购买方式,则我们应该设置3个不同的函数进行处理,然后把orderType, pay, stock
3个字段当做参数传递给上述设置的函数,如果该函数不能处理,则将请求转发给下一个函数,直到普通购买函数为止。
const order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay) {
console.log("500元预购,100元优惠券已到账...")
} else {
order200(orderType, pay, stock)
}
}
const order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay) {
console.log("200元预购,50元优惠券已到账...")
} else {
orderNormal(orderType, pay, stock)
}
}
const orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log("普通购买,无优惠券...")
} else {
console.log("购买失败,手机库存不足...")
}
}
order500(1, true, 200); // 500元预购,100元优惠券已到账...
order500(2, false, 100); // 普通购买,无优惠券...
order500(3, true, 0); // 购买失败,手机库存不足...
相对if-else
代码,该版本的代码显得简洁漂亮。但事实上,上面的代码仍有缺陷。因为逻辑链条的传递很僵硬,不够灵活,倘若在500定金预购和200定金预购间多了一种预购模式,则同时影响了2处的代码,即传递请求的代码被耦合进了业务函数之中,专业一点的说法就是,此次代码设计违反了开放-封闭原则,我们还需要改进。
实际上,为了得到更灵活、可拆分的职责链节点,我们不应加入具体业务逻辑到每个函数中,而是通过一个确定的返回值来表示请求是被传递,还是被处理。这里我们请求被传递则返回字符串为nextSuccessor
,否则就直接返回处理后的结果ret
。
我们定义一个Chain
类,用于封装职责链节点:
class Chain {
constructor(fn) {
this.fn = fn
this.successor = null
}
setNextSuccessor(successor) {
this.successor = successor
}
passRequest() {
const ret = this.fn.apply(this, arguments)
if (ret === "nextSuccessor") {
// 被传递 类似于递归调用
return
this.successor &&
this.successor.passRequest.apply(this.successor, arguments)
}
// 被处理
return ret
}
}
将业务代码简单修改,例如order500
(其他函数如是):
const order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay) {
console.log("500元预购,100元优惠券已到账...")
} else {
return "nextSuccessor"
}
}
接着对3个函数进行包装:
const chainOrder500 = new Chain(order500)
const chainOrder200 = new Chain(order200)
const chainOrderNormal = new Chain(orderNormal)
然后指定职责链的顺序:
chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)
所有请求都传递给第一个入口节点chainOrder500
:
chainOrder500.passRequest(1, true, 200); // 500元预购,100元优惠券已到账...
chainOrder500.passRequest(2, true, 200); // 200元预购,50元优惠券已到账...
chainOrder500.passRequest(3, true, 200); // 普通购买,无优惠券...
chainOrder500.passRequest(1, false, 0); // 购买失败,手机库存不足...
通过上述改进,我们就可以自由地增加、移除或修改链中的节点顺序,而不影响到具体的业务代码orderXXX
。
优缺点
最后是一个小总结,责任链模式是一种对象行为型模式,它具备很多优点:
- 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
- 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
- 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
- 简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
- 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
当然它也有缺点,主要表现在:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
参考
《JavaScript设计模式与开发实践》 ——曾探