模式定义
责任链模式(Chain of Responsibility) 是行为型设计模式之一,其将链中每一个节点看作是一个对象,每个节点处理的请求均不同,从而避免了请求的发送者和接收者之间的耦合关系,且内部自动维护一个下一节点对象。当一个请求从链式的首端发出时,会沿着链的路径依次传递给每一个节点对象,直至有对象处理这个请求为止。
主要作用
责任链模式 解耦了请求与处理,客户只需将请求发送到链上即可,无需关心请求的具体内容和处理细节,请求会自动进行传递直至有节点对象进行处理。
UML类图
角色
Handler(抽象处理者):它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象,作为其对下家的引用。通过该引用,处理者可以连成一条链。
ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。
模式优点
对象仅需知道该请求会被处理即可,且链中的对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度。
请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接。
链路结构灵活,可以通过改变链路结构动态地新增或删减责任。
新增一个新的具体请求处理者时无须修改原有代码,只需要在客户端重新建链即可,符合 “开闭原则”。
模式缺点
一个请求可能因职责链没有被正确配置而得不到处理。
对于比较长的职责链,请求的处理可能涉及到多个处理对象,系统性能将受到一定影响,且不方便调试。
如果节点对象存在循环引用时,会造成死循环,导致系统崩溃。
适用场景
有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的。
在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序。
使用步骤
定义抽象处理者
1 | package main.java.com.study.designPatterns.chain.common; |
定义具体处理者
1 | package main.java.com.study.designPatterns.chain.common; |
客户端调用
1 | package main.java.com.study.designPatterns.chain.common; |
模式示例
背景
很多公司都有这样的福利,就是项目组或者是部门可以向公司申请一些聚餐费用,用于组织项目组成员或者是部门成员进行聚餐活动,以增进人员之间的情感,更有利于工作中的相互合作。
申请聚餐费用的大致流程一般是:由申请人先填写申请单,然后交给领导审查,如果申请批准下来了,领导会通知申请人审批通过,然后申请人去财务核领费用,如果没有核准,领导会通知申请人审批未通过,此事也就此作罢了。
不同级别的领导,对于审批的额度是不一样的,比如:项目经理只能审批500元以内的申请;部门经理能审批1000元以内的申请;而总经理可以审核任意额度的申请。
也就是说,当某人提出聚餐费用申请的请求后,该请求会由项目经理、部门经理、总经理之中的某一位领导来进行相应的处理,但是提出申请的人并不知道最终会由谁来处理他的请求,一般申请人是把自己的申请提交给项目经理,或许最后是由总经理来处理他的请求,但是申请人并不知道应该由总经理来处理他的申请请求。
分析
当某人提出聚餐费用申请的请求后,该请求会在项目经理-部门经理-总经理这样一条领导处理链上进行传递,发出请求的人并不知道谁会来处理他的请求,每个领导会根据自己的职责范围,来判断是处理请求还是把请求交给更高级的领导,只要有领导处理了,传递就结束了。
需要把每位领导的处理独立出来,实现成单独的职责处理对象,然后为它们提供一个公共的、抽象的父职责对象,这样就可以在客户端来动态的组合职责链,实现不同的功能要求了。
具体代码
定义抽象处理者
1 | package main.java.com.study.designPatterns.chain.demoTwo; |
定义具体处理者
项目经理:
1 | package main.java.com.study.designPatterns.chain.demoTwo; |
部门经理:
1 | package main.java.com.study.designPatterns.chain.demoTwo; |
总经理:
1 | package main.java.com.study.designPatterns.chain.demoTwo; |
客户端调用
1 | package main.java.com.study.designPatterns.chain.demoTwo; |
拓展-处理多种请求
上面的示例是同一个职责链处理一种请求的情况,现在有这样的需求,还是费用申请的功能,这次是申请预支差旅费,假设还是同一流程,也就是组合同一个职责链,从项目经理-传递给部门经理-传递给总经理,虽然流程相同,但是每个处理类需要处理两种请求,它们的具体业务逻辑是不一样的。
简单的处理方式
简单处理方式就是为每种业务单独定义一个方法,然后客户端根据不同的需要调用不同的方法。
直接改造上面的代码,这里故意把两个方法做的有些不一样,一个是返回String类型的值,一个是返回boolean类型的值;一个返回到客户端再输出信息,一个是直接在职责处理里面就输出信息。
- 首先是改造职责对象的接口,添加上新的业务方法:
1 | package main.java.com.study.designPatterns.chain.demoTwo; |
- 职责的接口发生了改变,对应的处理类也要改变,这几个处理类是类似的,原有的功能不变,然后在新的实现方法里面,同样判断一下是否属于自己处理的范围,如果属于自己处理的范围那就处理,否则就传递到下一个处理。
项目经理:
1 | package main.java.com.study.designPatterns.chain.demoTwo; |
部门经理:
1 | package main.java.com.study.designPatterns.chain.demoTwo; |
总经理:
1 | package main.java.com.study.designPatterns.chain.demoTwo; |
- 客户端调用
1 | package main.java.com.study.designPatterns.chain.demoTwo; |
通用请求处理方式
简单处理方式实现起来很容易,但是有一个明显的问题就是只要增加一个业务,就需要修改职责的接口,这是很不灵活的,Java开发中很强调面向接口编程,因此接口应该相对保持稳定,接口一改,需要修改的地方就太多了,频繁修改接口绝对不是个好主意。所以改造如下:
首先定义一套通用的调用框架,用一个通用的请求对象来封装请求传递的参数;
然后定义一个通用的调用方法,这个方法不去区分具体业务,所有的业务都是这一个方法,在通用的请求对象里面会有一个业务的标记进行业务区分;
到了职责对象里面,愿意处理就跟原来一样的处理方式,如果不愿意处理,就传递到下一个处理对象就好了。
对于返回值也可以来个通用的,最简单的就是使用Object类型。
具体代码
- 通用的请求对象的定义
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
- 此时的通用职责处理对象,在这里要实现一个通用的调用框架
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
- 现在来加上第一个业务,就是“聚餐费用申请”的处理,为了描述具体的业务数据,需要扩展通用的请求对象,把业务数据封装进去,另外定义一个请求对象
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
- 接下来该实现职责对象的处理了,首先要覆盖父类的通用业务处理方法,然后在里面处理自己想要实现的业务,不想处理的就让父类去处理,父类会默认的传递给下一个处理对象。
项目经理:
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
部门经理:
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
总经理:
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
- 对于客户端,唯一的麻烦是需要知道每个业务对应的具体的请求对象,因为要封装业务数据进去.
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
- 接下来看看如何在不改动现有的框架的前提下,扩展新的业务,这样才能说明这种设计的灵活性。
假如就是要实现上面示例过的另外一个功能“预支差旅费申请”。要想扩展新的业务,第一步就是新建一个封装业务数据的对象
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
- 对于具体进行职责处理的类,比较好的方式就是扩展出子类来,然后在子类里面实现新加入的业务,当然也可以直接在原来的对象上改。这里采用扩展出子类的方式。
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
- 此时的测试类
1 | package main.java.com.study.designPatterns.chain.demoTwo.upgrade; |
总结
这种设计方式的好处,相当的通用和灵活,有了新业务,只需要添加实现新功能的对象就可以了,但是带来的缺陷就是可能会造成对象层次过多,或者出现较多的细粒度的对象,极端情况下,每次就扩展一个方法,会出现大量只处理一个功能的细粒度对象。