说起COR(责任链模式),大家最熟悉可能就是阎老在他那部大部头《设计模式》中举的‘击鼓传花’的形象例子,那里实现了一个很传统的COR模式。而从我看过的COR实现的方式上讲,从业务角度上区分,可分为两种:
- 完全推卸责任的COR
- 部分推卸责任的COR
当我们在开发一个项目或者产品时,为了职责分离以及开发的效率,我们常常采用的是分层分模块开发,一部分人负责前台,一部分人负责业务层,一部分人负责数据访问层等等。当开发完成后,我们都会移交给QA进行测试。当QA测试发现问题时,就会去找前台的人(大多数情况下,QA一般不知道这个问题是归谁负责,所以一般都从源头找起)。
- 完全推卸责任的场景:前台的人一看QA报的问题,发现不是属于前台的范畴,于是就把BUG转给了中间层的开发(业务层),说你帮忙看看,你们传过来的数据不对。中间层的开发DEBUG了下,发现从数据访问层来的数据就已经出错了,于是又把BUG转给了数据访问层的人员,数据访问层的人员查了查,发现问题是他们自己的数据问题,于是便把BUG Fix了,移交QA验证。
- 部分推卸责任的场景:而对于部分推卸责任的场景来说,就是前台,中间层,数据访问层都存在问题,每一层都需要进行代码的修改,才能够将这个BUG Fix。
对于上面的场景来说,QA相当于COR模式中的调用者(CLIENT端),我不管你们谁负责处理这个问题,我只负责发现问题然后移交给你们。而对于前台,中间层和数据访问层的开发人员来说,相当于COR模式中的处理者。处理者不管问题从哪里来,他只看下是不是属于他的问题,如果是那他就FIX然后返回,如果不是,那就传给下一个处理者处理。
当我们了解了这两种场景后,我们来看看具体的实现,首先先看下传统的COR模式。
在每个具体的IHandler实现类中,都会先CHECK,看下BUG是不是属于自己的处理范畴,如果是则处理,否则,传给下一个IHandler处理。
public void handle(Bug bug) {
if(如果是我们的BUG)
{
//那么我们处理它
doProcess(bug);
}
else
{
//否则,传给下个IHandler
nextHandler.handle(bug);
}
}
这种经典的COR模式从属于上面所讲的第一种,是属于完全推卸责任的COR。那么这种类型的COR有什么缺点呢?
缺点一:每个具体的IHandler实现类中,必须要自己记得当发现BUG不是属于自己的时候,要将控制转给下一个IHandler,即调用下列语句:
nextHandler.handle(bug);
但是,这个从类的职责上讲,这个转发并不是从属于具体IHandler类的职责。
缺点二:在每个具体的IHandler类都必须要有这么一段转发给下一个IHnadler处理的逻辑,看到代码的bad smell了吗?
缺点三:而且这个转发并不是强制的,即我在实现某个IHandler的时候,很有可能就忘记了将未处理的bug转发给下一个IHandler进行处理,如果这样的话,那这个bug很有可能就没有人来处理了。
缺点四:经典COR模式的结构实际上就是一个单向链表,每个具体的IHandler子类都持有一个IHandler的引用,用来保存下一个IHandler的引用,那么,客户端是怎么组装的呢?
//创建handler
IHandler presentationHandler=new PresentationLogicHandler();
IHandler businessHandler=new BusinessLogicHandler();
IHandler persistentHandler=new PersistentLogicHandler();
//组装handler
presentationHandler.setNextHandler(businessHandler);
businessHandler.setNextHandler(persistentHandler);
//调用handler
presentationHandler.handle(bug);
从上可以看到,client强依赖于具体的IHandler实现类了,OO告诉我们,我们应该依赖于抽象,不应该依赖于具体。在这IOC盛行的天下,有人就跳了出来说,这个好办,把所有的IHandler都配置在XML文件中,然后在client端调用IOC container,然后取出presentationHandler,直接再调用其handle(bug)方法即可。但是,可别忘了,现在大多数IOC container只是帮你封装了创建,但不会帮你组装创建好的handler,你还得自己取出创建好的handler,实现自己的组装策略。当然,这个也不会是问题。问题是,对于这里的COR来说,handler的顺序重要吗?对于我这里的业务场景来说,handler的顺序并不重要,你QA先把bug提给前台开发人员还是数据层开发人员,结果是一样。其实,对于完全推卸责任的COR模式来讲,handler的顺序都不重要,因为只会有一个IHandler会处理这个bug,不管哪个handler先执行。而对于部分推卸责任的COR模式来讲,handler的顺序有可能会影响整个结果,因为handler之间可能会有依赖,就拿我们的业务场景来说,中间层的Fix需要后台提供一个新的API才能够进行,那么后台handler必须要先于中间层handler执行。但是,这个依赖实际上还是线性的,我们不需要单链表这样的结构了,仅仅一个handler数组就够了。接着你会看到,在我们的下一个COR改进中,IHandler的setNextHandler(handler)变成了addHandler(handler).
综上所述,转发给下一个IHandler处理的逻辑在每个IHandler实现类中都是相似的,所以应该抽象到他们共同的基类中去, 而且我们应该通过接口强制的方式来提醒实现IHandler的人员,不要忘记了如果这个IHandler不能处理这个bug的话,应该要转给下一个IHandler处理。于是我们的设计变成了这样:
我们有了一个基类AbstractHandler来处理所有IHandler的共同逻辑-转发bug给下一个IHandler,如果该IHandler自己不能处理的话.
public boolean handle(Bug bug) {
for(IHandler handler:allHandlers)
{
//如果这个handler能够处理这个bug,处理结束后,返回true,告诉qa
//说这个bug已经有人处理了
if(handler.handle(bug))
return true;
}
//如果所有的IHandler都不能处理这个bug,那么返回false告诉qa,
//说没有人能够处理这个bug
return false;
}
并且改变了IHandler的handler接口,让其有了返回值,通过返回值告诉基类AbstractHandler,这个IHandler能不能处理这个bug,如果返回true,则表示该IHandler能够处理这个bug,返回false,则表示该IHandler不能处理这个bug,需要下一个IHandler来处理。与此同时,因为IHandler#handle(bug)方法有了返回值,那么我们在实现IHandler的时候,我们必须返回一个true或false.由接口强制来解决了经典COR的缺点三。
而且从IHandler和AbstractHandler的关系来看,这里使用了透明的Composite模式,每个IHandler子类必须要自己实现addHandler(handler)方法,要么留空,要么抛出UnSupportedException. 实际上我个人并不喜欢在这里使用透明的Composite模式,在这里我更倾向于使用安全的Composite模式,这样职责会更加清晰。这部分留给读者自己实现。
当COR改进到这里,可能大家都觉得已经足够了,但是我却不全然喜欢这个通过在handle()方法增加返回值来达到接口强制的方式,为什么,因为改进后的COR模式中,我们的具体Handler已经变成了这样:
public boolean handle(Bug bug) {
boolean hasBeenHandled=false;
if(如果是我们的BUG)
{
//那么我们处理它
doProcess(bug);
//并指明我们已经处理了这个bug
hasBeenHandled=true;
}
return hasBeenHandled;
}
看到了吗,hasBeenHandled这个boolean值仅仅是用于标识这个bug有没有被这个handler处理,并没有参加任何业务逻辑。实际上,稍微留意下就会发现,用于判断是不是这个handler所能处理的bug的逻辑和用于指明这个bug有没有被这个handler处理的逻辑是可以合并在一起的。
可以看到,IHandler#handle()方法的返回类型由boolean变回了void,但是增加了一个checkIfIcanHandle(bug)方法.基类AbstractHandler也需要改变。
public void handle(Bug bug) {
IHandler handler=null;
int i=0;
for(;i<allHandlers.length;i++)
{
handler=allHandlers[i];
if(handler.checkIfIcanHandle(bug))
{
handler.handle(bug);
break;
}
}
//如果所有的handler都不能处理这个bug,通知qa
if(i==allHandlers.length)
throw new IllegalArgumentException("No handler can handle this bug.");
}
而我们的IHandler实现类的职责由于checkIfIcanHandle(bug)方法的加入,变得更加的清晰。
public class PresentationLogicHandler extends AbstractHandler {
public void handle(Bug bug) {
//专注于处理bug的逻辑
}
public boolean checkIfIcanHandle(Bug bug) {
//专注于判断这个bug是否该handler能处理的逻辑
return false;
}
}
到了这里,对于COR模式本身应该没有多少需要改进的地方了(对于这篇文章中的业务场景来说),但有两个地方在设计的时候还是需要进行下考虑:
- 与Client之间的解耦,一般来说,COR会以Facade的方式发布给client使用,这样client仅仅依赖于这个Facade,而对其里面的Handler等一无所知。
- IHandler的handle()方法中使用的数据结构,特别是在实现部分推卸责任的COR时,这个数据结构如何设计,应该在灵活性和扩展性以及对象的封装性上取的一个均衡点。
下面让我们看看完全推卸责任的COR与部分推卸责任的COR的区别,在我看来,只是理念上的区别,技术上的实现是一致的。
- 首先是我们前面提到的handler顺序的问题,完全推卸责任的COR不在意handler的组装顺序,而部分推卸责任的COR的handler之间则有可能存在依赖,需要注意handler的组装顺序。
- 其次,对于部分推卸责任的COR来讲,handler之间存在共享数据,我们可以理解为上面IHandler#handle()方法中的bug对象,上一个handler对bug对象的修改会影响到下一个handler的处理,实际上也就是handler的依赖问题。
接下来让我们看看COR与其他模式的关系,COR和Observer很相似,Observer专注于处理一对多的依赖,而完全推卸责任的COR则是专注于处理一对一的关系,一个handler处理一个request,部分推卸责任的COR则也是用来处理一对多的关系。从COR最后的实现代码上看,实际上跟Observer也极其神似。而IHandler部分则实现了Command模式,而IHandler中checkIfIcanHandle()方法的加入,使其成了Command和Template的混合体。
最后,让我们来看下COR模式的使用场景.对于COR模式的使用场景,Bill Siggelkow给出了一个guideline,就是用于处理步骤易于变化的顺序工作流。首先让我们先看下一个业务场景,我们都去过淘宝买东西,开始肯定是挑选商品,然后是填写订单(填写地址,购买的数量,给卖家的留言等),再接着是付款,然后等待收货,最后是对卖家的评价。对于这个业务场景,我们怎么来建模呢?从上面的描述看,很明显,买东西这个流程的步骤是固定的,我们很容易就会得出下面的流程:
而且我们很容易看出这里面其中的变化点,比如说,付款,我们可以通过网上银行付款,也可以通过支付宝来付款等等,那么对于这样的变化,我们怎么应对?很自然的方式就是继承这个BuyGoods类,然后覆写其中的付款方法,pay4good(),像评价卖家,我们可以有好评,中评,差评以及各种各样的评价,那么我们也可以对不同的评价行为分别为其覆写其中的evaluate(),看出来了吗?这里用了很常见的Template模式。如果需求到这里为此,我觉得还是可以接受。但是,如果我要买的东西很贵,那么我挑选完商品,我就不能简单的直接下订单了,我得跟卖家好好砍砍价后才能下订单。如果沿用我们上面的模型,那我们得在下订单前加入一个步骤,协商价格:
这样一来,由于基类BuyGoods的步骤发生了变化,导致其所有的子类都需要增加negotiation()方法的实现,即使它不需要进行协商价格(这种情况下的子类实现会为该方法放置一个空实现)。再或者,我们在拿到货物后发现有损坏了,要退货,那么这个时候又要修改BuyGoods基类增加接口。当然,对于这种变化的需求,我们完全可以通过增加协商价格接口和退货接口,并让新的BuyGoods子类来实现,也可以通过Adapator模式来引入新的方法,如果这个买商品的流程只有少许这样的变化,那么我认为增加接口或者Adapator来引入新方法的方式是可以满足的。如果这样的变化很多,那么我们能引入多少接口,这个时候使用COR或许是一种更好的选择。而且,还有可能有这样的变化,就是步骤的顺序不固定,可能我在A店铺这里是协商好价格再下订单,在B店铺那里则可能是下了订单后再协商的价格。所以,当一个流程(或算法),它的步骤不确定而且步骤的顺序有可能变化时,那么COR就能派上用场。
如果采用COR,那么上面的BuyGoods的每个方法都演化成一个IHandler实例,这些IHandler实例按照顺序组合成一个IHandler chain,买商品实际上就变成了调用这个IHandler chain。
当我们采用COR后再看看前面的需求变化,第一种需求变化是步骤的增加,比如说增加协商价格/退货,这个时候只需要增加对应的IHandler实例,并注册到IHandler chain中即可,不需要修改别的IHandler实例或者client代码,完全符合开闭原则。对于第二种需求变化,那就更简单了,只需要修改注册到IHandler chain的IHandler实例顺序即可(大多数情况下,我们注册IHandler到IHandler chain都是通过配置文件的配置来实现的),这里的IHandler chain实际上就是上述的AbstractHandler。
除了上面的应用场景以外,实际上使用Command的地方也可以使用COR,举个最常见的Command例子,就是我们常常在处理WEB请求的时候,规定某一类请求对应于某一种处理,比如说,如果发过来的HTTP REQUEST的请求TYPE为List,那么就调用ListAction进行处理这个请求,如果请求TYPE为Save,那么就调用SaveAction进行处理。而在主程序启动的时候就已经规定好了这样的映射,通常是请求Type作为key,对应处理请求的action作为value储存在一个map中,当有请求过来的时候,那么就根据请求的type从该map中取出action进行处理。这个很明显是一个典型的Command模式,但是,当请求与对应的处理请求的action的对应关系并不是这么简单的时候,比如,要采用什么action来处理这个请求,需要满足的条件是可变的,易变的,即不仅仅是根据请求的type,而还需要根据请求里面的用户信息,请求的某些hearder信息来一起确定时,那么cor会比command要更加适用。