设计模式之责任链模式
本文通过图书馆管理系统中,用户名校验、密码校验、需要增加问题,每次都要增加if判断语句,将其改用责任链模式进行链式调用,为了让代码更加的优雅,我们使用之前学过的建造者模式就代码进行改造。接着我们会介绍责任链模式在我们常用的框架中的运用,最后是责任链模式的优缺点和应用场景。
读者可以拉取完整代码到本地进行学习,实现代码均测试通过后上传到码云。
一、引出问题
小王给老王打造了一套图书馆管理系统,随着访问量的不断增加,老王要求增加访问的用户名校验。
小王说这有何难,说着就在用户访问图书馆之前加了一层判断语句,判断用户名是否合法。过了一段时间后,又给每个用户颁发了一个密码,就需要在用户名校验通过以后校验密码。
小王就准备在用户名的判断语句后,增加密码的校验语句。老王赶忙拦住了要继续更改代码的小王。如果以后再增加角色校验、权限校验、你准备写多少个判断语句。
而且你把软件设计原则中的——开闭原则丢到哪里去了。
你可以考虑使用一种模式,将所有的校验方法都独立出来一个类,每一个类只负责处理各自的校验逻辑,当前的校验类通过以后传递给下一个校验类进行处理,这样每次增加新的逻辑判断都只需要增加校验类就行了。
就像是一条流水线,每个类负责处理线上的一个环节。
二、责任链模式的概念和使用
实际上,老王提出来的正是行为型设计模式中的——**责任链模式。
责任链模式正如它的名字一样,将每个职责的步骤串联起来执行,并且一个步骤执行完成之后才能够执行下一个步骤。
从名字可以看出通常责任链模式使用链表来完成。 因此当执行任务的请求发起时,从责任链上第一步开始往下传递,直到最后一个步骤完成。
在责任链模式当中, 客户端只用执行一次流程开始的请求便不再需要参与到流程执行当中,责任链上的流程便能够自己一直往下执行,
客户端同样也并不关心执行流程细节,从而实现与流程之间的解耦。
责任链模式需要有两个角色:
抽象处理器(Handler):处理器抽象接口,定义了处理请求的方法和执行下一步处理的处理器。
具体处理器(ConcreteHandler):执行请求的具体实现,先根据请求执行处理逻辑,完成之后将请求交给下一个处理器执行。
基于责任链模式实现图书馆的用户名校验和密码校验。
抽象处理器:
/** * @author tcy * @Date 22-08-2022 */ public abstract class Handler { private Handler next; public Handler getNext() { return next; } public void setNext(Handler next) { this.next = next; } public abstract void handle(Object request); }
用户名校验处理器:
/** * @author tcy * @Date 23-08-2022 */ public class ConcreteHandlerUsername extends Handler{ @Override public void handle(Object request) { //相应的业务逻辑... System.out.println("用户名校验通过. 参数: " + request); //调用链路中下一个节点的处理方法 if (getNext() != null) { getNext().handle(request); } } }
密码校验器:
/** * @author tcy * @Date 23-08-2022 */ public class ConcreteHandlerPassword extends Handler{ @Override public void handle(Object request) { //相应的业务逻辑... System.out.println("密码校验通过. 参数: " + request); //调用链路中下一个节点的处理方法 if (getNext() != null){ getNext().handle(request); } } }
客户端调用:
public class Client { //普通模式---------- public static void main(String[] args) { Handler concreteHandler1 = new ConcreteHandlerUsername(); Handler concreteHandler2 = new ConcreteHandlerPassword(); concreteHandler1.setNext(concreteHandler2); concreteHandler1.handle("用户名tcy"); } }
用户名校验通过. 参数: 用户名tcy 密码校验通过. 参数: 用户名tcy
这样我们就实现了责任链模式,但是这种方式我们注意到,调用方调用的时候手动将两个处理器set到一起,如果这条链路很长的时候,这样的代码实在是太不优雅了。
将我们曾经学过的设计模式扒出来,看使用哪种模式能让它看起来更优雅一点。
三、责任链模式+建造者模式
我们看建造型设计模式的文章,看建造者模式中的典型应用中的Lombok。
参考Lombok的 @Builder例子,是不是和我们这个有着些许相似呢?
我们在Handle的类中创建一个Builder内部类。
/** * 建造者模式 */ public static class Builder{ private Handler head; private Handler tail; public Builder addHanlder(Handler handler){ //head==null表示第一次添加到队列 if (null == head){ head = this.tail = handler; return this; } //原tail节点指向新添加进来的节点 this.tail.setNext(handler); //新添加进来的节点设置为tail节点 this.tail = handler; return this; } public Handler build(){ return this.head; } }
该内部类更像是一个链表结构,定义一个头和尾对象,add方法是向链接的头尾中赋值,build返回头元素方便开始链式调用。我们对调用方代码进行改造。
//建造者模式--------- public static void main(String[] args) { Handler.Builder builder = new Handler.Builder(); builder.addHanlder(new ConcreteHandlerUsername()) .addHanlder(new ConcreteHandlerPassword()); builder.build().handle("用户名tcy"); }
这样的实现方式比原方式优雅多了。责任链模式本身是很简单的,如果将责任链模式和建造者模式组合起来使用就没那么容易理解了。
在实际使用中往往不是一个单一的设计模式,更多的是多种组合模式组成的“四不像”,实际上这并不是一件轻松的事。
四、责任链模式在源码运用
为了加深理解我们继续深入责任链模式在Spring中的运用。
Spring Web 中的 HandlerInterceptor
,里面有preHandle()
、postHandle()
、afterCompletion()
三个方法,实现这三个方法可以分别在调用"Controller"方法之前,调用"Controller"方法之后渲染"ModelAndView"之前,以及渲染"ModelAndView"之后执行。
public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { } default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { } }
HandlerInterceptor
就是角色中的抽象处理者,HandlerExecutionChain相当于上述中的Client,用于调用责任链上的各个环节。
public class HandlerExecutionChain { ... @Nullable private HandlerInterceptor[] interceptors; private int interceptorIndex = -1; boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { HandlerInterceptor[] interceptors = getInterceptors(); if (!ObjectUtils.isEmpty(interceptors)) { for (int i = 0; i < interceptors.length; i++) { HandlerInterceptor interceptor = interceptors[i]; if (!interceptor.preHandle(request, response, this.handler)) { triggerAfterCompletion(request, response, null); return false; } this.interceptorIndex = i; } } return true; } void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception { HandlerInterceptor[] interceptors = getInterceptors(); if (!ObjectUtils.isEmpty(interceptors)) { for (int i = interceptors.length - 1; i >= 0; i--) { HandlerInterceptor interceptor = interceptors[i]; interceptor.postHandle(request, response, this.handler, mv); } } } void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) throws Exception { HandlerInterceptor[] interceptors = getInterceptors(); if (!ObjectUtils.isEmpty(interceptors)) { for (int i = this.interceptorIndex; i >= 0; i--) { HandlerInterceptor interceptor = interceptors[i]; try { interceptor.afterCompletion(request, response, this.handler, ex); } catch (Throwable ex2) { logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); } } } } }
私有的数组 private HandlerInterceptor[] interceptors 用于存储责任链的每个环节,,然后通过interceptorIndex
作为指针去遍历责任链数组按顺序调用处理者。
结合我们上面给出的例子,在Spring中的应用是比较容易理解的。
在Servlet的一系列拦截器也是采用的责任链模式,有兴趣的读者可以深入研究一下。
五、总结
当必须按顺序执行多个处理者时,可以考虑使用责任链模式。如果处理者的顺序及其必须在运行时改变时,可以考虑使用责任链模式。责任链的模式是缺点也很明显,增加了系统的复杂性。
但是要切忌避免过度设计,在实际应用中,校验用户名和密码的业务逻辑并没有那么的复杂,可能只是一个判断语句,使用设计模式只会增加系统的复杂性,而在Shiro、SpringSecurity、SpringMVC的拦截器中使用责任链模式是一个好的选择。
如果在你的项目业务中需要定义一系列拦截器,那么使用责任链模式就是一个比较不错的选择。
我已经连续更新了数十篇设计模式博客,推荐你结合学习。