JavaScript 职责链模式
前言
职责链模式,它为请求的发送者和请求者之间实现了解耦,他将具体的请求者连成一条链,并沿着这条链依次处理请求,直到有一个对象处理它为止。
这么说可能不好理解,其实就是将复杂的条件判断进行了解耦处理,具体我们看下面的例子。
商城购买
我们现在有一个优惠活动,如果用户交了500元定金,他在购买的时候可以收到100元优惠卷,如果是交了200元定金则收到50元优惠卷,如果没有定金,那就没有优惠卷,库存有限的情况下不一定保证能买到。
假设我们有这三个参数:
- orderType:表示订单类型(定金用户或者普通购买用户),code 的值为 1 的时候是 500 元 定金用户,为 2 的时候是 200 元定金用户,为 3 的时候是普通购买用户。
- pay:表示用户是否已经支付定金,值为 true 或者 false, 虽然用户已经下过 500 元定金的 订单,但如果他一直没有支付定金,现在只能降级进入普通购买模式。
- stock:表示当前用于普通购买的手机库存数量,已经支付过 500 元或者 200 元定金的用 户不受此限制。
那么我们的代码如下:
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 优惠券
这个函数实现了我们想要的功能,但是它里面充斥着大量的条件判断语句,当我们还需要新增更多的优惠时,它的代码量将会不断增大,维护难度也直线上升。
使用职责链模式重构
我们将每个优惠封装成一个函数,函数接收这三个参数进行处理,如果这个优惠不是它处理的,它调用其他优惠处理函数。
// 500 元订单
var order500 = function(orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500 元定金预购, 得到 100 优惠券');
} else {
order200(orderType, pay, stock); // 将请求传递给 200 元订单
}
};
// 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 元定金预购, 得到 500 优惠券
order500(3, false, 500); // 输出:普通购买, 无优惠券
order500(3, false, 0); // 输出:手机库存不足
可以看到,现在我们的代码就清晰了很多,它通过上一个优惠函数调用下一个优惠函数,实现了链条,但是这种实现非常僵硬,它使得优惠函数之间的关系是强耦合状态,违反了开放-封闭原则,如果我们要调整优惠的顺序,或者新增优惠,还是需要改动函数。
灵活的可拆分职责链
处理这种问题的办法就是规范化,我们规定优惠函数在不是自己处理的情况下,返回一个约定的值,然后外部通过这个值来判断是否传递给下一个优惠函数。
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 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;
};
使用setNextSuccessor
来链接优惠函数,通过统一调用passRequest
来运行优惠函数。
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); // 输出:手机库存不足
现在我们如果要新增优惠函数,就不需要改动原来的优惠函数了。
异步职责链
如果存在异步的情况,后一个优惠函数的处理需要等待上一个函数结束才行,而处理异步有两种方式,一种是Promise,一种是回调函数,我们以回调函数为例。
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();
这种写法其实并不完善,大家参考思路即可,因为上面的写法导致return的结果没有参考意义了,我们还需要考虑到这点,使用promise和async await可能会更加合适。
职责链模式的优缺点
现在我们再回过头来看开头,职责链模式确实是将发送者和N个请求者之间实现了解耦,由于不知道到底是谁可以处理请求,所以需要从第一个请求者开始。
通过职责链模式,我们可以很方便的组合链条,甚至有时候可以单独拿出某一个链条节点来单独使用。
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);
chainOrder200.passRequest(2, true, 500); // 输出:200 元定金预购,得到 50 优惠券
但是这种模式也不是没有弊端,如果所有的请求者都不处理,那就导致我们根本不知道这个请求处理了没有,解决的办法就是给末尾增加一个特殊处理,如果到它了,就抛出一个异常,这样来实现一个通知作用。
另外由于职责链模式是一个个运行的,如果链条中存在大量的请求者,从性能的考虑,大部分的请求者都没有实质性的作用,只是传递,为此我们还需要避免职责链过长带来的性能损耗。
AOP实现职责链
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 = order500yuan.after(order200yuan).after(orderNormal);
order(1, true, 500); // 输出:500 元定金预购,得到 100 优惠券
order(2, true, 500); // 输出:200 元定金预购,得到 50 优惠券
order(1, false, 500); // 输出:普通购买,无优惠券
当我们运行order
的时候,实际上运行的是最后一个after
抛出的匿名函数,这个匿名函数会运行参数fn,等待fn的结果,而fn又是上一个after
返回的匿名函数,它又会运行它的参数fn,以此类推,直到运行到最顶层的order500yuan
函数,等他结果出来后,再依次在匿名函数中进行处理,如果不是nextSuccessor
,它们会通过return,一层层的抛出结果。
使用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());
使用这种方式也能很方便的获取到文件上传对象,如果是迭代器的话,还得单独封装一个运行迭代器的函数。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据