职责链模式
先看一个典型的场景:早上上班坐公交,挤不上前门,只能从后门上。这时候就会拜托别人递公交卡。公交卡一个个向前传递——最后遇到了能够得着刷卡机的那个人。
再看一个OA场景:我要写信给公司老板,信的传递通常是:
项目组管理->部门->事业部->总部->公司大佬
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链表结构,并沿着这条链传递该请求,直到有一个对象处理它为止。
写信这种事,如果没有合适的表达渠道,可能给所有人都写信,对方不能处理又把信退回给你,然后你投给下个人,用图表述是这样的:
职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象,我们把这些对象称为链中的节点,如图:
职责链模式与链表数据结构有千丝万缕的关系。笔者写过一篇文章: 《javascript数据结构和算法》读书笔记(3):链表 阅读它或许会更好地帮助理解本文。
职责链模式的优势在于,弱化了请求和服务对象之间的联系。一个大型应用中,只需要指导谁是职责链表的第一个接受者,即可与之建立通信。
这时实际工作中的一个问题:一个数学试题详情,实现了我们所能想得到的一些功能。因此可能会有多处调用场景: >> 是老师还是学生?
试用代码设计这个组件。
const order=(user)=>{ if(user.userType=='student'){ console.log('渲染学生视图') }else if(user.userType=='teacher'){ if(user.subject=='math'){ if(user.subType=='author'){ console.log('渲染创作视图') }else if(user.subType=='reader'){ console.log('渲染读者视图') }else if(userType=='approver'){ console.log('渲染审核者视图') } }else{ console.log('渲染读者视图') } }}
这绝对算不上一个好看的代码。对后期维护造成了巨大的开销。
设想判断信息的传递过程,可以把每个判断条件作为一个独立的小函数,当遇到不能处理的请求,则在返回值中传递一个next的信息。那么我们可以对三个判断条件各写一个方法:
const userType=(user)=>{ if(user.userType=='student'){ console.log('渲染学生视图') }else{ // 处理不了就next return 'next'; }}
const subject=(user)=>{
if(user.subject!=='math'){ console.log('渲染读者视图'); }else{ return 'next'; }}
const subType=(user)=>{ if(user.subType=='author'){ console.log('渲染创作视图') }else if(user.subType=='reader'){ console.log('渲染读者视图') }else if(user.subType=='approver'){ console.log('渲染审核者视图') }}
接下来就是包装这三个小方法的函数,:
class Chain{ constructor(fn){ this.fn=fn; this.next=null; } // 负责设置指针 setNext(next){ return this.next=next; } // 处理,传递请求 passReq(){ const ret=this.fn.apply(this,arguments); if(ret=='next'){ return this.next&&this.next.passReq.apply(this.next,arguments) } return ret; }}
最后调用:
// 生成链let _userType=new Chain(userType);let _subject=new Chain(subject);let _subType=new Chain(subType);
// 设置顺序链条_userType.setNext(_subject);_subject.setNext(_subType);
_userType.passReq({ userType:'teacher', subject:'math', subType:'approver'})
在这种模式下,如果要做需求修改,就不必去动那些庞大的ifelse语句,只需要往链表中添加一个新的结点,改变其指针就行了。
职责链模式不只是负责优化条件语句,完全可以用来描述一个事务的处理过程,比如说,处理不了就甩锅——用trycatch包起来就可以了。
const fn=new Chain(()=>{ try{ // todo.. }catch(e){ return 'next' }})
命令模式和组合模式中,我们用了promise、async/await来处理异步请求。如果不用ES6+的语句。有什么办法处理异步的情况呢?
这时候返回next已经没用了。可以在原型链上设置一个next方法。
class Chain{ // ...
next(){ return this.next&&this.next.passReq.apply(this.next,arguments); }}
异步调用这个next方法
const fn1=new Chain(function(){ console.log('step1');});
const fn2=new Chain(function(){ console.log('step2'); setTimeout(()=>{ this.next(); })});
const fn3=new Chain(function(){ console.log('step3')})
fn1.setNext(fn2).setNext(fn3);fn.passReq()
如前所述,职责链优势在于解耦复杂的联系。只需要向链头发送请求,就可以得到合适走过整个处理流程(但不一定得到得到期望的结果,所以需要在链尾做容错处理)。其实,在试卷例子中,你只要知道用户不是学生,就可以向链表的第二个结点发送请求。这在ifelse中是做不到的。
当然,链条过长,也会带来一些性能损耗。
在JavaScript开发中,职责链模式是最容易被忽视的模式之一。实际上只要运用得当,职责链模式可以很好地帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。
职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。无论是作用域链、原型链,还是DOM节点中的事件冒泡,我们都能从中找到职责链模式的影子。职责链模式还可以和组合模式结合在一起,用来连接部件和父部件,或是提高组合对象的效率。