JavaScript模式——职责链模式:解耦请求发送者与接收者的艺术
本文介绍了软件开发中的职责链模式,通过将多个处理对象连成链来解耦请求发送者和接收者。文章从公交车投币等生活实例入手,以电商订单处理为例展示传统实现方式的弊端,并通过拆解函数、创建Chain类等方式重构代码,实现灵活可扩展的职责链。同时探讨了异步职责链和AOP实现方式,分析了该模式解耦灵活等优点和性能损耗等缺点。最后指出职责链模式在作用域链、事件冒泡等场景的广泛应用,强调其提高系统灵活性的价值。全文
在软件开发中,我们经常会遇到这样的场景:一个请求需要经过多个对象处理,但发送者并不关心具体由谁处理,只关心请求最终能被处理。职责链模式(Chain of Responsibility Pattern)正是为此而生——它使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
本文将从现实生活中的例子入手,通过订单处理的重构案例,深入讲解职责链模式的核心思想,并展示其灵活的实现方式,包括异步职责链和AOP实现。
一、现实中的职责链模式
在现实生活中,职责链模式的例子比比皆是:
-
公交车投币:早高峰公交车上人多拥挤,上车后找不到售票员,只好把两块钱硬币往前递。除非站在你前面的第一个人就是售票员,否则你的硬币通常要在N个人手上传递,才能最终到达售票员手里。
-
考试传纸条:中学期末考试,遇到不会答的题目,就把题目编号写在小纸条上往后传递。坐在后面的同学如果也不会答,他就会把这张小纸条继续递给他后面的人。
从这两个例子中,我们可以发现职责链模式的最大优点:请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系。
二、实际开发中的职责链模式
2.1 问题场景:手机订单处理
假设我们负责一个售卖手机的电商网站,经过分别交纳500元定金和200元定金的两轮预定后,现在进入正式购买阶段。优惠政策如下:
- 支付500元定金的用户:获得100元优惠券
- 支付200元定金的用户:获得50元优惠券
- 未支付定金的用户:普通购买,无优惠券,且受库存限制
页面加载时,后端会传递以下几个字段:
orderType:1=500元定金用户,2=200元定金用户,3=普通购买用户pay:是否已支付定金stock:普通购买的手机库存数量
最初的代码实现如下:
var order = function (orderType, pay, stock) {
if (orderType === 1) { // 500元定金购买模式
if (pay === true) {
console.log('500元定金预购,得到100优惠券');
} else {
if (stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
}
} else if (orderType === 2) { // 200元定金购买模式
if (pay === true) {
console.log('200元定金预购,得到50优惠券');
} else {
if (stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
}
} else if (orderType === 3) {
if (stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
}
};
order(1, true, 500); // 输出:500元定金预购,得到100优惠券
这段代码存在明显缺点:
- 函数巨大,难以阅读
- 违反开放-封闭原则,每次新增或修改订单类型都需要改动函数内部
- 状态切换不明显,维护困难
三、用职责链模式重构代码
3.1 初步重构:拆分成独立函数
我们将500元订单、200元订单、普通购买分别拆分成三个函数,通过函数调用传递请求:
// 500元订单
var order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,得到100优惠券');
} else {
order200(orderType, pay, stock);
}
};
// 200元订单
var order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,得到50优惠券');
} else {
orderNormal(orderType, pay, stock);
}
};
// 普通购买订单
var orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
};
order500(1, true, 500); // 输出:500元定金预购,得到100优惠券
order500(1, false, 500); // 输出:普通购买,无优惠券
order500(2, true, 500); // 输出:200元定金预购,得到50优惠券
order500(3, false, 500); // 输出:普通购买,无优惠券
order500(3, false, 0); // 输出:手机库存不足
虽然代码结构清晰了,但请求传递顺序被硬编码在函数内部,仍然违反开放-封闭原则。
3.2 灵活可拆分的职责链节点
我们定义一个Chain类,用于包装函数,并支持链式设置后继节点:
var Chain = function (fn) {
this.fn = fn;
this.successor = null;
};
Chain.prototype.setNextSuccessor = function (successor) {
return this.successor = successor;
};
Chain.prototype.passRequest = function () {
var ret = this.fn.apply(this, arguments);
if (ret === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}
return ret;
};
然后改写订单函数,使其返回'nextSuccessor'表示无法处理:
var order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,得到100优惠券');
} else {
return 'nextSuccessor';
}
};
var order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,得到50优惠券');
} else {
return 'nextSuccessor';
}
};
var orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
};
最后包装成节点并设置链条顺序:
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);
chainOrder500.passRequest(1, true, 500); // 输出:500元定金预购,得到100优惠券
chainOrder500.passRequest(2, true, 500); // 输出:200元定金预购,得到50优惠券
chainOrder500.passRequest(3, true, 500); // 输出:普通购买,无优惠券
chainOrder500.passRequest(1, false, 0); // 输出:手机库存不足
现在我们可以自由灵活地增加、移除和修改链中的节点顺序。例如,增加300元定金购买:
var order300 = function () {
// 具体实现略
};
var chainOrder300 = new Chain(order300);
chainOrder500.setNextSuccessor(chainOrder300);
chainOrder300.setNextSuccessor(chainOrder200);
四、异步的职责链
在实际开发中,我们经常会遇到异步操作,比如在节点中发起ajax请求,根据返回结果决定是否继续传递请求。此时,我们需要让节点可以手动触发下一个节点。
为Chain增加next方法:
Chain.prototype.next = function () {
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
};
异步职责链示例:
var fn1 = new Chain(function () {
console.log(1);
return 'nextSuccessor';
});
var fn2 = new Chain(function () {
console.log(2);
var self = this;
setTimeout(function () {
self.next();
}, 1000);
});
var fn3 = new Chain(function () {
console.log(3);
});
fn1.setNextSuccessor(fn2).setNextSuccessor(fn3);
fn1.passRequest(); // 输出:1 → 2(1秒后) → 3
这种异步职责链配合命令模式,可以方便地创建异步ajax队列库。
五、职责链模式的优缺点
5.1 优点
- 解耦:请求发送者和接收者之间的复杂关系被解耦,发送者只需将请求传递给第一个节点即可。
- 灵活重组:链中的节点对象可以灵活拆分、重组,增加或删除节点很容易,且符合开放-封闭原则。
- 可指定起始节点:请求不一定从链的第一个节点开始,可以根据需求手动指定,减少传递次数。
5.2 缺点
- 无法保证处理:不能保证某个请求一定会被链中的节点处理,需要在链尾增加保底节点。
- 性能损耗:大部分节点可能仅起到传递作用,过长的链会影响性能。
六、用AOP实现职责链
利用JavaScript的函数式特性,我们可以通过Function.prototype.after更加简洁地实现职责链:
Function.prototype.after = function (fn) {
var self = this;
return function () {
var ret = self.apply(this, arguments);
if (ret === 'nextSuccessor') {
return fn.apply(this, arguments);
}
return ret;
};
};
var order = order500.after(order200).after(orderNormal);
order(1, true, 500); // 输出:500元定金预购,得到100优惠券
order(2, true, 500); // 输出:200元定金预购,得到50优惠券
order(1, false, 500); // 输出:普通购买,无优惠券
用AOP实现职责链既简单又巧妙,但链条过长时,函数作用域的叠加也会影响性能。
七、用职责链模式获取文件上传对象
回顾迭代器模式中获取文件上传对象的例子,我们可以用职责链模式更简单地实现:
var getActiveUploadObj = function () {
try {
return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE上传控件
} catch (e) {
return 'nextSuccessor';
}
};
var getFlashUploadObj = function () {
if (supportFlash()) {
var str = '<object type="application/x-shockwave-flash"></object>';
return $(str).appendTo($('body'));
}
return 'nextSuccessor';
};
var getFormUpladObj = function () {
return $('<form><input name="file" type="file"/></form>').appendTo($('body'));
};
var getUploadObj = getActiveUploadObj.after(getFlashUploadObj).after(getFormUpladObj);
console.log(getUploadObj());
八、小结
职责链模式是容易被忽视但非常实用的模式。它通过将请求沿着链传递,直到有对象处理,实现了请求发送者与接收者的解耦。在JavaScript中,我们可以通过面向对象的方式或AOP的方式实现职责链。
无论是作用域链、原型链,还是DOM节点中的事件冒泡,都能找到职责链模式的影子。学会使用职责链模式,将帮助我们更好地组织代码,提高系统的灵活性和可维护性。
更多推荐

所有评论(0)