责任链模式
一个事件需要经过多个对象处理是一个挺常见的场景,譬如采购审批流程,请假流程,软件开发中的异常处理流程,web请求处理流程等各种各样的流程,可以考虑使用责任链模式来实现。
以请假流程为例,一般公司普通员工的请假流程简化如下:
普通员工请假简化流程图
普通员工发起一个请假申请,当请假天数小于3天时只需要得到主管批准即可;当请假天数大于3天时,主管批准后还需要提交给经理审批,经理审批通过,若请假天数大于7天还需要进一步提交给总经理审批。
使用 if-else 来实现这个请假流程的简化代码如下:
public class LeaveApproval {
public boolean process(String request, int number) {
boolean result = handleByDirector(request); // 主管处理
if (result == false) { // 主管不批准
return false;
} else if (number < 3) { // 主管批准且天数小于 3
return true;
}
result = handleByManager(request); // 准管批准且天数大于等于 3,提交给经理处理
if (result == false) { // 经理不批准
return false;
} else if (number < 7) { // 经理批准且天数小于 7
return true;
}
result = handleByTopManager(request); // 经理批准且天数大于等于 7,提交给总经理处理
if (result == false) { // 总经理不批准
return false;
}
return true; // 总经理最后批准
}
public boolean handleByDirector(String request) {
// 主管处理该请假申请
}
public boolean handleByManager(String request) {
// 经理处理该请假申请
}
public boolean handleByTopManager(String request) {
// 总经理处理该请假申请
}
}
问题看起来很简单,三下五除二就搞定,但是该方案存在几个问题:
LeaveApproval 类比较庞大,各个上级的审批方法都集中在该类中,违反了 “单一职责原则”,测试和维护难度大
当需要修改该请假流程,譬如增加当天数大于30天时还需提交给董事长处理,必须修改该类源代码(并重新进行严格地测试),违反了 “开闭原则”
该流程缺乏灵活性,流程确定后不可再修改(除非修改源代码),客户端无法定制流程
使用责任链模式可以解决上述问题。
定义
责任链模式(Chain of Responsibility Pattern):避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。
角色
Handler(抽象处理者):它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象,作为其对下家的引用。通过该引用,处理者可以连成一条链。
ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。
类图如下所示:
责任链模式.类图
纯的责任链模式:
一个具体处理者对象只能在两个行为中选择一个:要么承担全部责任,要么将责任推给下家,不允许出现某一个具体处理者对象在承担了一部分或全部责任后
又将责任向下传递的情况
一个请求必须被某一个处理者对象所接收,不能出现某个请求未被任何一个处理者对象处理的情况
不纯的责任链模式:
允许某个请求被一个具体处理者部分处理后再向下传递
或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求
而且一个请求可以最终不被任何处理者对象所接收
示例
使用责任链模式(不纯)重构请假流程
请假信息类,包含请假人姓名和请假天数
@Data
@AllArgsConstructor
public class LeaveRequest {
private String name; // 请假人姓名
private int numOfDays; // 请假天数
}
抽象处理者类 Handler,维护一个 nextHandler 属性,该属性为当前处理者的下一个处理者的引用;声明了抽象方法 process
@Data
public abstract class Handler {
protected String name; // 处理者姓名
protected Handler nextHandler; // 下一个处理者
public Handler(String name) {
this.name = name;
}
public abstract boolean process(LeaveRequest leaveRequest); // 处理请假
}
三个具体处理类,分别实现了抽象处理类的 process 方法
// 主管处理者
public class Director extends Handler {
public Director(String name) {
super(name);
}
@Override
public boolean process(LeaveRequest leaveRequest) {
boolean result = (new Random().nextInt(10)) > 3; // 随机数大于3则为批准,否则不批准
String log = "主管<%s> 审批 <%s> 的请假申请,请假天数: <%d> ,审批结果:<%s> ";
System.out.println(String.format(log, this.name, leaveRequest.getName(), leaveRequest.getNumOfDays(), result == true ? "批准" : "不批准"));
if (result == false) { // 不批准
return false;
} else if (leaveRequest.getNumOfDays() < 3) { // 批准且天数小于3,返回true
return true;
}
return nextHandler.process(leaveRequest); // 批准且天数大于等于3,提交给下一个处理者处理
}
}
// 经理
public class Manager extends Handler {
public Manager(String name) {
super(name);
}
@Override
public boolean process(LeaveRequest leaveRequest) {
boolean result = (new Random().nextInt(10)) > 3; // 随机数大于3则为批准,否则不批准
String log = "经理<%s> 审批 <%s> 的请假申请,请假天数: <%d> ,审批结果:<%s> ";
System.out.println(String.format(log, this.name, leaveRequest.getName(), leaveRequest.getNumOfDays(), result == true ? "批准" : "不批准"));
if (result == false) { // 不批准
return false;
} else if (leaveRequest.getNumOfDays() < 7) { // 批准且天数小于7
return true;
}
return nextHandler.process(leaveRequest); // 批准且天数大于等于7,提交给下一个处理者处理
}
}
// 总经理
public class TopManager extends Handler {
public TopManager(String name) {
super(name);
}
@Override
public boolean process(LeaveRequest leaveRequest) {
boolean result = (new Random().nextInt(10)) > 3; // 随机数大于3则为批准,否则不批准
String log = "总经理<%s> 审批 <%s> 的请假申请,请假天数: <%d> ,审批结果:<%s> ";
System.out.println(String.format(log, this.name, leaveRequest.getName(), leaveRequest.getNumOfDays(), result == true ? "批准" : "不批准"));
if (result == false) { // 总经理不批准
return false;
}
return true; // 总经理最后批准
}
}
客户端测试:
public class Client {
public static void main(String[] args) {
Handler zhangsan = new Director("张三");
Handler lisi = new Manager("李四");
Handler wangwu = new TopManager("王五");
// 创建责任链
zhangsan.setNextHandler(lisi);
lisi.setNextHandler(wangwu);
// 发起请假申请
boolean result1 = zhangsan.process(new LeaveRequest("小旋锋", 1));
System.out.println("最终结果:" + result1 + "\n");
boolean result2 = zhangsan.process(new LeaveRequest("小旋锋", 4));
System.out.println("最终结果:" + result2 + "\n");
boolean result3 = zhangsan.process(new LeaveRequest("小旋锋", 8));
System.out.println("最终结果:" + result3 + "\n");
}
}
总结
职责链模式的主要优点
1、对象仅需知道该请求会被处理即可,且链中的对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度
2、请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接
3、在给对象分派职责时,职责链可以给我们更多的灵活性,可以在运行时对该链进行动态的增删改,改变处理一个请求的职责
4、新增一个新的具体请求处理者时无须修改原有代码,只需要在客户端重新建链即可,符合 “开闭原则”
职责链模式的主要缺点
1、一个请求可能因职责链没有被正确配置而得不到处理
2、对于比较长的职责链,请求的处理可能涉及到多个处理对象,系统性能将受到一定影响,且不方便调试
3、可能因为职责链创建不当,造成循环调用,导致系统陷入死循环
适用场景
有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的
在不明确指定接收者的情况下,向多个对象中的一个提交一个请求
可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序
Tomcat 过滤器中的责任链模式
Filter接口是所有具体过滤器必须实现的接口,该接口有一个核心的方法doFilter,方法对Request和Response进行处理,注意第三个参数类型为FilterChain,传入该参数的目的是为了责任能够传递到下一个过滤器。
具体Filter如下所示:
public class MyFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) {
//处理request和response
//...
chain.doFilter(request,response);
}
}
首先根据需求对request和response进行处理,最后需要调用过滤器链中的doFilter方法以便责任能够传递到下一个过滤器。chain.doFilter(request,response),注意这个doFilter是FilterChain中的doFilter与Filter中的doFilter没有任何关系,名字虽然相同,但是参数是不一样的。FilterChain中的doFilter只有Request和Response两个参数,其实更好的命名方式应该是chain.doNextFilter(resquest,response)
那么,FilterChain是什么样的呢?我们上面已经看到了FilterChain的用法,也知道了FilterChain在调用doFilter(request,response)的时候需要有能力将任务传递到下一个Filter,因此我们可以推断,FilterChain中需要保存所有的Filter。在Tomcat的实现中,正是在FilterChain中用一个数组保存了所有的Filter,这不禁让我想起了java.util.ArrayList的实现。
在Tomcat中FilterChain也是一个接口,如下所示:
public interface Filter {
public void init(FilterConfig filterConfig) throws ServletException;
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
public void destroy();
}
public interface FilterChain {
void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}
而ApplicationFilterChain实现了FilterChain,其核心代码如下所示:
final class ApplicationFilterChain implements FilterChain {
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
private int pos = 0;//维持过滤器链中的当前位置
private int n = 0;//过滤器链中的过滤器数量
public void doFilter(ServletRequest request, ServletResponse response) {
internalDoFilter(request,response);
}
private void internalDoFilter(ServletRequest request,
ServletResponse response) {
if(pos<n) {
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
filter.doFilter(request, response, this);
return;
}
}
void addFilter(ApplicationFilterConfig filterConfig) {
//省略了扩容部分。。。
filters[n++] = filterConfig;
}
}
可以看出在FilterChain的实现中确实是用数组来保存所有的过滤器,但是并不是直接保存的Filter类型,而是保存的FilterConfig类型,可以把FilterConfig当做Filter的包装类,我们可以通过filterConfig.getFilter()拿到Filter实例。在internalDoFilter方法中我们看到了FilterChain是如何实现责任的传递的,通过pos和n这两个变量来判断有没有传递到过滤器链的最后面,如果没有到最后,则取出当前过滤器并调用filter.doFilter的方法,在前面MyFilter的实现当中,它的doFilter方法在最后又调用了chain.doFilter(),从而保证了过滤器链能够将责任传递到下一个过滤器中。pos和n这两个变量的作用又让我联想到了ArrayList中的迭代器的实现。
客户端代码如下所示:
public class Client{
public static void main(String[] args) {
Request request = new Request();
Response response = new Response();
Filter filter1 = new MyFilter1();
Filter filter2 = new MyFilter2();
Filter filter3 = new MyFilter3();
FilterChain chain = new ApplicationFilterChain();
ApplicationFilterConfig filterConfig1 = new ApplicationFilterConfig(filter1);
ApplicationFilterConfig filterConfig2 = new ApplicationFilterConfig(filter2);
ApplicationFilterConfig filterConfig3 = new ApplicationFilterConfig(filter3);
chain.addFilter(filterConfig1);
chain.addFilter(filterConfig2);
chain.addFilter(filterConfig3);
chain.doFilter(request, response);
}
}
管道中的阀门
上面我们提到了FilterChain中用数组保存了所有的过滤器,而在管道Pipeline中则采用的是链表的方式将一个个的阀门Valve链接起来的,这让我想起了java.util.LinkedList的实现,Pipeline相当于LinkedList,Valve相当于LinkedList中的内部类Node,不过Valve并不是以内部类的形式存在。
Valve接口如下所示:
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void invoke(Request request, Response response);
}
public class MyValve implements Valve {
protected Valve next = null;
@Override
public Valve getNext() {
return next;
}
@Override
public void setNext(Valve valve) {
this.next = valve;
}
@Override
public void invoke(Request request, Response response) {
//处理request,response
//...
getNext().invoke(request, response);
return;
}
}
public interface Pipeline{
public Valve getBasic();
public void setBasic(Valve valve);
public void addValve(Valve valve);
public Valve getFirst();
}
Tomcat中Pipeline的实现类为StandardPipeline,核心代码如下所示:
public class StandardPipeline implements Pipeline {
protected Valve basic = null;
protected Valve first = null;
@Override
public Valve getBasic() {
return (this.basic);
}
@Override
public void setBasic(Valve valve) {
Valve oldBasic = this.basic;
if (oldBasic == valve)
return;
if (valve == null)
return;
Valve current = first;
while (current != null) {
if (current.getNext() == oldBasic) {
current.setNext(valve);
break;
}
current = current.getNext();
}
this.basic = valve;
}
@Override
public void addValve(Valve valve) {
// Add this Valve to the set associated with this Pipeline
if (first == null) {
first = valve;
valve.setNext(basic);
} else {
Valve current = first;
while (current != null) {
if (current.getNext() == basic) {
current.setNext(valve);
valve.setNext(basic);
break;
}
current = current.getNext();
}
}
}
@Override
public Valve getFirst() {
if (first != null) {
return first;
}
return basic;
}
}
可以看出StandardPipeline是一个具有头尾指针的单向链表。first相当于头指针,basic相当于尾指针。addValve方法的作用是把管道中的各个阀门链接起来。
在Tomcat中是通过容器组件对管道进行调用的,wrapper就是一种容器,因此可以通过如下方式进行调用:wrapper.getPipeline().getFirst().invoke(request, response);
从上面的调用方式可以看出,通过getPipeline()拿到管道对象,再调用管道对象的getFirst()拿到第一个阀门对象,再调用该阀门对象的invoke(request,response)方法,在该invoke方法的最后,如上面MyValve所示,会调用getNext().invoke(request, response)从而将责任传递到下一个阀门。