设计模式之--职责链模式

233 阅读9分钟

1. 定义

职责链模式的定义: 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,知道有一个对象处理它为止。

一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象,这些对象称为链中的节点

2. 现实中的职责链模式

  • 1.搭公交车:早高峰人员多,经常上车后找不到售票员,只好把硬币往前面递,硬币在N个人手上传递,才能最终到达售票员的手里

  • 2.考试时,遇到不会的题目,把题目写在小纸条上往后传递,坐在后面的同学如果也不会答,就继续把纸条递给后面的人(注:考试递纸条,这种做法应禁止,诚信第一)

从这两个例子中,很容易找到职责链模式的最大优点:请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系。如果不使用职责链模式,那么在公交车上,就得先搞清楚谁是售票员,才能把硬币递给他。同样,在考试中,也许就要先了解同学中哪些可以解答这道题

3. 实际开发中的职责链模式

假设负责一个售卖手机的电商网站,经过分别交纳500元定金和200元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段

公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付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) {
        if (pay === true) {
            console.log('500元定金预购,得到100优惠券')
        } else {
            if (stock > 0) {
                console.log('普通购买,无优惠券')
            } else {
                console.log('手机库存不足')
            }
        }
    } else if (orderType === 2) {
        if (pay === true) {
            console.log('200元定金预购,得到100优惠券')
        } 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)

上面的代码虽然实现了正确的结果,但order函数不仅巨大到难以阅读,而且需要经常进行修改,后面代码不好维护

4. 用职责链模式重构代码

先把500元订单、200元订单以及普通购买分成3个函数

接下来把orderType、pay、stock这三个字段当作参数传递给500元订单函数,如果该函数不符合处理条件,则把这个请求传递给后面的200元订单函数,如果200元订单函数依然不能处理该请求,则继续传递请求给普通购买函数

var order500 = function(orderType, pay, stock){
    if (orderType === 1 && pay === true) {
        console.log('500元定金预购,得到100优惠券')
    } else {
        order200(orderType, pay, stock)
    }
}

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('手机库存不足')
    } 
}

执行结果和前面那个巨大的order函数完全一样,但是代码的结构已经清晰了很多,把一个大函数拆分了3个小函数,去掉了许多嵌套的条件分支语句

虽然已经把大函数拆分成了互不影响的3个小函数,但请求在链条传递中的顺序非常僵硬,传递请求的代码被耦合在了业务函数之中:

var order500 = function(orderType, pay, stock){
    if (orderType === 1 && pay === true) {
        console.log('500元定金预购,得到100优惠券')
    } else {
        order200(orderType, pay, stock)
        // order200和order500耦合在一起
    }
}

违反开放-封闭原则,如果有天要增加300元预定或者去掉200元预定,意味着就必须改动这些业务函数内部。

5.灵活可拆分的职责链节点

让链中的各个节点可以灵活拆分和重组

首先需要改写一下分别表示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 === 1 && 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,表示在链中的下一个节点

此外,Chain的prototype中还有两个函数,它们的作用如下所示

// Chain.prototype.setNextSuccessor 指定在链中的下一个节点
// Chain.prototype.passRequest 传递请求给某个节点

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
}

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)

通过改进,我们可以自由灵活地增加、移除和修改链中的节点顺序,假如某天网站运营人员又想出了支持300元定金购买,就在链中增加一个节点即可

var order300 = function(){
    // 具体实现略
}

var chainOrder300 = new Chain(order300); 
chainOrder500.setNextSuccessor(chainOrder300)
chainOrder300.setNextSuccessor(chainOrder200)

6.异步的职责链

给 Chain类再增加一个原型方法 Chain.prototype.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()

7. 职责链模式的优缺点

职责链模式的最大优点就是解耦了请求发送者和N个接收者之间的复杂关系,由于不知道链中的哪个节点可以处理你发出的请求,所以只需要把请求传递给第一个节点即可。

  • 用了职责链模式之后,每种订单都有各自的处理函数而互不影响
  • 用了职责链模式之后,链中的节点对象可以灵活地拆分重组
  • 可以手动指定起始节点,请求并不是非得从链中的第一个节点开始传递,可以减少请求在链中的传递次数,更快地找到合适的请求接收者。普通的条件分支语句是做不到的

如果运用得当,职责链模式可以很好的帮助我们组织代码,但这种模式也并非没有弊端,首先我们不能保证某个请求一定会被链中的节点处理。在这种情况下,可以在链尾增加一个保底的接受者节点来处理即将离开链尾的请求

另外,职责链模式使得程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分节点并没有起到实质性的作用,它们的作用仅仅是让请求传递下去,从性能方面考虑,我们要避免过长的职责链带来的性能损耗

8. AOP实现职责链

在上面的职责链实现中,利用了一个Chain类来把普通函数包装成职责链的节点。其实利用JavaScript的函数式特性,有一种更加方便的方法来创建职责链

Function.prototype.after = function(fn) {
    var self = this;
    return function(){
        var ret = self.apply(this, arguments);
        if (ret === 'nextSuccessor') {
            return fnn.apply(this, arguments)
        }
        return ret
    }
}

var order = order500.after(order300).after(orderNormal)

order(1,true,500)

用AOP来实现职责链既简单又巧妙,但这种把函数叠在一起的方式,同时也叠加了函数的作用域,如果链条太长的话,也会对性能有较大的影响

9.小结

职责链模式可以很好地帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。职责链中的节点数量和顺序是可以自由变化的,可以在运行时决定链中包含哪些节点

无论是作用域链、原型链,还是DOM节点中的事件冒泡,都能从中找到职责链模式的影子。职责链模式还可以和组合模式结合在一起,用来连接部件和父部件,或是提高组合对象的效率