JavaScript职责链模式
1 什么是职责链模式
职责链模式是一种行为型设计模式,它允许将请求沿着处理者链进行传递,直到其中一个处理者能够处理该请求为止,从而避免了请求的发送者和接收者之间的耦合关系
在职责链模式下,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象,我们把这些对象称为链中的节点。
该模式通常用于多个对象可以处理同一请求的情况,但不知道哪个对象才能最终处理请求,并且需要避免将请求发送到所有对象。
2 举个例子
假设在一个售卖手机的电商网站中,出现了两轮预定的活动,分别是交500元定金和200元定金,并且在交定金之后,订单已经生成,到了正式购买的阶段后,公司针对支付过定金的顾客有一定的优惠政策,已经支付过500元定金的顾客,会收到100元优惠券,支付过200元定金的顾客,可以收到50元优惠券,对于没有参加过预定活动的顾客,没有优惠券,而且在库存有限的情况下不一定可以买到。
在订单页面加载时,我们会收到以下几个参数:
orderType
,表示订单类型(定金用户 or 普通购买用户),值为1时为500元定金用户,2时为200元定金用户,3时为普通购买用户pay
,表示用户是否已经支付过定金,值为true
或者false
,如果用户下过500元定金的订单,但是一直没有支付定金,也只能进入普通购买模式stock
,表示当前用于普通购买的手机库存数量,已经支付过定金的用户不计算
下面我们用代码表示这个流程:
var order = function (orderType, pay, stock) {
// 500 元定金购买模式
if (orderType === 1) {
// 已支付定金
if (pay === true) {
console.log("500 元定金预购, 得到 100 优惠券");
} else {
// 未支付定金,降级到普通购买模式
if (stock > 0) {
// 用于普通购买的手机还有库存
console.log("普通购买, 无优惠券");
} else {
console.log("手机库存不足");
}
}
// 200 元定金购买模式
} else if (orderType === 2) {
// 如果已经支付过定金
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 用职责链模式重构代码
现在我们采用职责链模式重构这段代码,先把500元订单、200元订单以及普通购买分成3个函数。
接下来把orderType
、pay
、stock
这3个字段当作参数传递给500元订单函数,如果该函数不符合处理条件,则把这个请求传递给后面的200元订单函数,如果200元订单函数依然不能处理该请求,则继续传递请求给普通购买函数,代码如下:
// 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元定金预购, 得到50优惠券
order500(3, false, 500); // 普通购买, 无优惠券
order500(3, false, 0); // 手机库存不足
可以看到,执行结果和前面那个巨大的order
函数完全一样,但是代码的结构已经清晰了很多,我们把去掉了许多嵌套的条件分支语句。但可以看到,请求在链条传递中的顺序非常僵硬,传递请求的代码被耦合在了业务函数之中。
这依然是违反开放—封闭原则的,如果有天我们要增加300元预订或者去掉200元预订,意味着就必须改动这些业务函数内部。
4 灵活可拆分的职责链节点
首先需要改写一下分别表示3种购买模式的节点函数,我们约定,如果某个节点不能处理请求,则返回一个特定的字符串'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("手机库存不足");
}
};
接下来需要把函数包装进职责链节点,我们定义一个构造函数Chain
,在new Chain
的时候传递的参数即为需要被包装的函数,同时它还拥有一个实例属性this.successor
,表示在链中的下一个节点。
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;
};
现在我们把3个订单函数分别包装成职责链的节点:
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); // 手机库存不足
5 异步的职责链
在上面的代码中,我们让每个节点函数同步返回一个特定的值"nextSuccessor"
,来表示是否把请求传递给下一个节点。而在实际开发中,我们经常会遇到一些异步的问题,比如我们要在节点函数中发起一个 ajax
异步请求,异步请求返回的结果才能决定是否继续在职责链中passRequest
。
这时要给Chain
类再增加一个方法Chain.prototype.next
,表示手动传递请求给职责链中的下一个节点:
Chain.prototype.next = function () {
return (
this.successor &&
this.successor.passRequest.apply(this.successor, arguments)
);
};
举个例子:
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;
};
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();