设计模式之美—-职责链模式 本文将对责任链模式(职责链模式)分为四个部分进行讲解
让大家系统全面的学会使用责任链模式, 并应用到开发中去
原理篇
大部分设计模式的核心是应对工程中的复杂性, 使得代码满足开闭原则, 提高代码扩展性
职责链的英文名为Chain Of Responsibility Design Pattern
, 在GoF的设计模式中的定义如下
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
“将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。”
实际上就是将请求
通过类似链表的结构, 通过结点处理并线性的传递下去,直到请求完成或者没有下一个结点
这里的结点是指单个处理请求的对象, 接受上一个结点处理后的结果并在处理后, 传递给下一个结点或者请求完成结束处理, 因为每个结点都承担着细粒度的职责, 所以称为责任链模式
实现篇
设计模式的实现往往多种多样, 可能因为语言特性(枚举单例)也可能因为算法(迭代或者递归),使得代码结构各异, 但核心思想是不会变得, 之后的设计模式, 我都会以Java为例做出实现, 因为Java算是比较典型且纯粹的面向对象编程, 其他语言的同学可以借助chatgpt转语言来理解, 本质是一样的
职责链模式的实现分为三个结构, 分别是抽象处理器类(Handler), 处理器类(XXXHandler), 以及处理链(HandlerChain)
抽象处理器类(Handler)
public abstract class Handler { protected Handler successor = null ; public abstract void handle () ; public void setSuccessor (Handler successor) { this .successor = successor; } }
处理器类(XXXHandler)
public class HandlerA extends Handler { @Override public boolean handle () { boolean handled = false ; if (!handled && successor != null ) { successor.handle(); } } } public class HandlerB extends Handler { @Override public boolean handle () { boolean handled = false ; if (!handled && successor != null ) { successor.handle(); } } }
处理链(HandlerChain)
public class HandlerChain { private Handler head = null ; private Handler tail = null ; public void addHandler (Handler handler) { handler.setSuccessor(null ); if (null == head) { head = handler; tail = handler; return ; } tail.setSuccessor(handler); tail = handler; } }
public class Main { public static void main (String[] args) { HyuanandlerChain chain = new HandlerChain (); chain.addHandler(new HandlerA ()); chain.addHandler(new HandlerB ()); chain.handle(); } }
经典GoF的实现不够优雅, 处理器类中的handle()函数不仅包含业务逻辑, 还有对下一个处理器的调用, 对于不熟悉代码结构的程序员,在添加处理器调用时可能忘记调用下一个处理器, 可能会导致bug, 所以我们利用模板方法模式进行重构(让我想起Spring源码中AbstractBeanFactory中的doGetBean()方法), 将下一个处理器调用的逻辑抽象到模板中
public abstract class Handler { protected Handler successor = null ; public void setSuccessor (Handler successor) { this .successor = successor; } public final void handle () { boolean handled = doHandle(); if (successor != null && !handled) { successor.handle(); } } protected abstract boolean doHandle () ; } public class HandlerA extends Handler { @Override protected boolean doHandle () { boolean handled = false ; return handled; } } public class HandlerB extends Handler { @Override protected boolean doHandle () { boolean handled = false ; return handled; } }
我们发现责任链模式本质就是将请求 线性传递
到每个处理逻辑上进行处理直到某个结点处理成功或者职责链到头
既然是线性结构,我们同样可以使用数组来实现责任链模式, 这种方式更加简单
public interface IHandler { boolean handle () ; } public class HandlerA implements IHandler { @Override public boolean handle () { boolean handled = false ; return handled; } } public class HandlerB implements IHandler { @Override public boolean handle () { boolean handled = false ; return handled; } } public class HandlerChain { private List<IHandler> handlers = new ArrayList <>(); public void addHandler (IHandler handler) { this .handlers.add(handler); } public void handle () { for (IHandler handler : handlers) { boolean handled = handler.handle(); if (handled) { break ; } } } } public class Application { public static void main (String[] args) { HandlerChain chain = new HandlerChain (); chain.addHandler(new HandlerA ()); chain.addHandler(new HandlerB ()); chain.handle(); } }
在 GoF 给出的定义中,如果处理器链上的某个处理器能够处理这个请求,那就不会继续往下传递请求。实际上,职责链模式还有一种变体,那就是请求会被所有的处理器都处理一遍,不存在中途终止的情况。
public abstract class Handler { protected Handler successor = null ; public void setSuccessor (Handler successor) { this .successor = successor; } public final void handle () { doHandle(); if (successor != null ) { successor.handle(); } } protected abstract void doHandle () ; } public final void handle () { doHandle(); if (successor != null ) { successor.handle(); } }
我们参照<<设计模式之美>>中, 灵活扩展算法的敏感词过滤框架来讲
对于支持 UGC(User Generated Content,用户生成内容)的应用(比如论坛)来说,用户生成的内容(比如,在论坛中发表的帖子)可能会包含一些敏感词(比如涉黄、广告、反动等词汇)。针对这个应用场景,我们就可以利用职责链模式来过滤这些敏感词。
对于包含敏感词的内容,我们有两种处理方式,一种是直接禁止发布,另一种是给敏感词打马赛克(比如,用 *** 替换敏感词)之后再发布。第一种处理方式符合 GoF 给出的职责链模式的定义,第二种处理方式是职责链模式的变体。
第一种
public interface SensitiveWordFilter { boolean doFilter (Content content) ; } public class SexyWordFilter implements SensitiveWordFilter { @Override public boolean doFilter (Content content) { boolean legal = true ; return legal; } } public class SensitiveWordFilterChain { private List<SensitiveWordFilter> filters = new ArrayList <>(); public void addFilter (SensitiveWordFilter filter) { this .filters.add(filter); } public boolean filter (Content content) { for (SensitiveWordFilter filter : filters) { if (!filter.doFilter(content)) { return false ; } } return true ; } } public class ApplicationDemo { public static void main (String[] args) { SensitiveWordFilterChain filterChain = new SensitiveWordFilterChain (); filterChain.addFilter(new AdsWordFilter ()); filterChain.addFilter(new SexyWordFilter ()); filterChain.addFilter(new PoliticalWordFilter ()); boolean legal = filterChain.filter(new Content ()); if (!legal) { } else { } } }
第二种
public interface SensitiveWordFilter { void doFilter (Content content) ; } public class SexyWordFilter implements SensitiveWordFilter { @Override public void doFilter (Content content) { } } public class SensitiveWordFilterChain { private List<SensitiveWordFilter> filters = new ArrayList <>(); public void addFilter (SensitiveWordFilter filter) { this .filters.add(filter); } public void filter (Content content) { for (SensitiveWordFilter filter : filters) { filter.doFilter(content) } } }
你可能会说,我像下面这样也可以实现敏感词过滤功能,而且代码更加简单,为什么非要使用职责链模式呢?这是不是过度设计呢?
public class SensitiveWordFilter { public boolean filter (Content content) { if (!filterSexyWord(content)) { return false ; } if (!filterAdsWord(content)) { return false ; } if (!filterPoliticalWord(content)) { return false ; } return true ; } private boolean filterSexyWord (Content content) { } private boolean filterAdsWord (Content content) { } private boolean filterPoliticalWord (Content content) { } }
应用设计模式主要是为了应对代码的复杂性,让其满足开闭原则,提高代码的扩展性。这里应用职责链模式也不例外
首先, 我们来看, 职责链模式如何应对代码的复杂性
将大的代码逻辑拆分成函数, 将大类拆分成小类, 是应对代码复杂性的常用方法, 职责链模式中,我们把各个敏感词过滤函数继续拆分成独立的类, 进一步简化了SensitiveWordFilter类, 让SenesitiveWordFilter类的代码不会过多, 过复杂.
其次, 我们来看, 职责链模式如何让代码满足开闭原则, 提高代码的扩展性
当我们考虑扩展新的过滤算法的时候, 比如, 我们还需要过滤特殊符号, 按照非职责链模式的代码实现,我们需要修改SensitiveWordFilter的代码, 违反开闭原则, 不过, 这样的修改比较集中,还可以接受的, 而职责链模式的实现方法更加优雅, 只需要新添加一个Filter类, 并且通过addFilter()函数将他添加到FilterChain中即可, 其他代码完全不需要修改
你可能会说,即便使用职责链模式来实现,当添加新的过滤算法的时候,还是要修改客户端代码(ApplicationDemo),这样做也没有完全符合开闭原则。
public class NewWordFilter implements SensitiveWordFilter { public boolean doFilter (Content content) { boolean legal = true ; return legal; } } public class ApplicationDemo { public static void main (String[] args) { SensitiveWordFilterChain filterChain = new SensitiveWordFilterChain (); filterChain.addFilter(new AdsWordFilter ()); filterChain.addFilter(new SexyWordFilter ()); filterChain.addFilter(new PoliticalWordFilter ()); filterChain.addFilter(new NewWordFilter ()); boolean legal = filterChain.filter(new Content ()); if (!legal) { } else { } } }
细化一下的话,我们可以把上面的代码分成两类:框架代码和客户端代码。其中,ApplicationDemo 属于客户端代码,也就是使用框架的代码。除 ApplicationDemo 之外的代码属于敏感词过滤框架代码。
假设敏感词过滤框架并不是我们开发维护的,而是我们引入的一个第三方框架,我们要扩展一个新的过滤算法,不可能直接去修改框架的源码。这个时候,利用职责链模式就能达到开篇所说的,在不修改框架源码的情况下,基于职责链模式提供的扩展点,来扩展新的功能。换句话说,我们在框架这个代码范围内实现了开闭原则。
除此之外, 利用职责链模式相对于不用职责链模式的实现方法, 还有一个好处, 就是配置过滤算法更加灵活了, 只需要使用某几个过滤算法
应用篇 职责链模式常用在框架的开发中,为框架提供扩展点,让框架的使用者在不修改框架源码的情况下,基于扩展点添加新的功能。实际上,更具体点来说,职责链模式最常用来开发框架的过滤器 和拦截器
Servlet Filter Servlet Filter
是 Java Servlet 规范
中定义的组件,翻译成中文就是过滤器,它可以实现对 HTTP 请求的过滤功能,比如鉴权、限流、记录日志、验证参数等等。
在实际项目中,我们该如何使用 Servlet Filter
呢?我写了一个简单的示例代码,如下所示。添加一个过滤器,我们只需要定义一个实现 javax.servlet.Filter
接口的过滤器类,并且将它配置在 web.xml
配置文件中。Web 容器启动的时候,会读取 web.xml
中的配置,创建过滤器对象。当有请求到来的时候,会先经过过滤器,然后才由 Servlet
来处理。
public class LogFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("拦截客户端发送来的请求." ); chain.doFilter(request, response); System.out.println("拦截发送给客户端的响应." ); } @Override public void destroy () { } } <filter> <filter-name>logFilter</filter-name> <filter-class>com.xzg.cd.LogFilter</filter-class> </filter> <filter-mapping> <filter-name>logFilter</filter-name> <url-pattern>
我们发现,添加过滤器非常方便,不需要修改任何代码,定义一个实现 javax.servlet.Filter 的类,再改改配置就搞定了,完全符合开闭原则。
Servlet Filter
利用的正是职责链模式来保证如此好的扩展性
Servlet
只是一个规范,并不包含具体的实现,所以,Servlet
中的 FilterChain
只是一个接口定义。具体的实现类由遵从 Servlet
规范的 Web 容器来提供,比如,ApplicationFilterChain
类就是 Tomcat 提供的 FilterChain
的实现类
public final class ApplicationFilterChain implements FilterChain { private int pos = 0 ; private int n; private ApplicationFilterConfig[] filters; private Servlet servlet; @Override public void doFilter (ServletRequest request, ServletResponse response) { if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; Filter filter = filterConfig.getFilter(); filter.doFilter(request, response, this ); } else { servlet.service(request, response); } } public void addFilter (ApplicationFilterConfig filterConfig) { for (ApplicationFilterConfig filter:filters) if (filter==filterConfig) return ; if (n == filters.length) { ApplicationFilterConfig[] newFilters = new ApplicationFilterConfig [n + INCREMENT]; System.arraycopy(filters, 0 , newFilters, 0 , n); filters = newFilters; } filters[n++] = filterConfig; } }
ApplicationFilterChain
中的 doFilter()
函数的代码实现比较有技巧,实际上是一个递归调用。
这样实现主要是为了在一个 doFilter()
方法中,支持双向拦截,既能拦截客户端发送来的请求,也能拦截发送给客户端的响应,你可以结合着 LogFilter
那个例子,以及对比待会要讲到的 Spring Interceptor
.
Spring Interceptor Servlet Filter
,有一个功能上跟它非常类似的东西,Spring Interceptor
,,都用来实现对 HTTP 请求进行拦截处理。
它们不同之处在于, Servlet Filter
是 Servlet
规范的一部分,实现依赖于 Web 容器。Spring Interceptor
是Spring MVC
框架的一部分,由 Spring MVC
框架来提供实现。客户端发送的请求,会先经过 Servlet Filter
,然后再经过 Spring Interceptor
,最后到达具体的业务代码中。( Filter
在 Request
抵达Servlet
之前进行过滤 以及Servlet.service()
执行完返回reponse
之后过滤; 而Interceptor
是在抵达 DispatcherServlet
之后根据请求路径找到对应的映射器候选者后前置处理
候选者, 在候选者处理请求并视图解析器处理完成后后置处理
)
在项目中,我们该如何使用 Spring Interceptor
呢?
声明拦截器类, 实现HandlerInterceptor
接口, 实现preHandler()
, postHandler()
, ,afterCompletion()
方法
通过配置文件中添加<mvc:interceptor>
, 配置<mvc:mapping path="拦截路径"/>
, <bean class="拦截器实现类全限定名" />
(也可以使用注解配置类)
LogInterceptor
实现的功能跟刚才的 LogFilter
完全相同,只是实现方式上稍有区别。LogFilter
对请求和响应的拦截是在 doFilter()
一个函数中实现的,而 LogInterceptor
对请求的拦截在 preHandle()
中实现,对响应的拦截在 postHandle()
中实现。
public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("拦截客户端发送来的请求." ); return true ; } @Override public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("拦截发送给客户端的响应." ); } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("这里总是被执行." ); } } <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/*" /> <bean class="com.xzg.cd.LogInterceptor" /> </mvc:interceptor> </mvc:interceptors>
Spring interceptor
也是基于职责链模式实现的。其中,HandlerExecutionChain
类是职责链模式中的处理器链。它的实现相较于 Tomcat 中的 ApplicationFilterChain
来说,逻辑更加清晰,不需要使用递归来实现,主要是因为它将请求和响应的拦截工作,拆分到了两个函数中实现。
HandlerExecutionChain
的源码如下所示,同样对代码也进行了一些简化,只保留了关键代码。
public class HandlerExecutionChain { private final Object handler; private HandlerInterceptor[] interceptors; public void addInterceptor (HandlerInterceptor interceptor) { initInterceptorList().add(interceptor); } 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 ; } } } return true ; } void applyPostHandle (HttpServletRequest request, HttpServletResponse response, 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, 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); } } } } }
在 Spring 框架中,DispatcherServlet
的 doDispatch()
方法来分发请求,它在真正的业务逻辑执行前后,执行 HandlerExecutionChain
中的 applyPreHandle()
和 applyPostHandle()
函数,用来实现拦截的功能。
练习篇 尝试优化一下代码(案例来源)
在本案例中我们模拟在618大促期间的业务系统上线审批流程场景
像是这些一线电商类的互联网公司,阿里、京东、拼多多等,在618期间都会做一些运营活动场景以及提供的扩容备战,就像过年期间百度的红包一样。但是所有开发的这些系统都需要陆续的上线,因为临近618有时候也有一些紧急的调整的需要上线,但为了保障线上系统的稳定性是尽可能的减少上线的,也会相应的增强审批力度。就像一级响应、二级响应一样。
而这审批的过程在随着特定时间点会增加不同级别的负责人加入,每个人就像责任链模式中的每一个核心点。对于研发小伙伴并不需要关心具体的审批流程处理细节,只需要知道这个上线更严格,级别也更高,但对于研发人员来说同样是点击相同的提审按钮,等待审核。
接下来我们就模拟这样一个业务诉求场景,使用责任链的设计模式来实现此功能。
场景简述 模拟审核服务 public class AuthService { private static Map<String, Date> authMap = new ConcurrentHashMap <String, Date>(); public static Date queryAuthInfo (String uId, String orderId) { return authMap.get(uId.concat(orderId)); } public static void auth (String uId, String orderId) { authMap.put(uId.concat(orderId), new Date ()); } }
这里面提供了两个接口一个是查询审核结果(queryAuthInfo
)、另外一个是处理审核(auth
)。
这部分是把由谁审核的和审核的单子ID作为唯一key值记录到内存Map结构中。
待优化代码 public class AuthController { private SimpleDateFormat f = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); public AuthInfo doAuth (String uId, String orderId, Date authDate) throws ParseException { Date date = AuthService.queryAuthInfo("1000013" , orderId); if (null == date) return new AuthInfo ("0001" , "单号:" , orderId, " 状态:待三级审批负责人 " , "王工" ); if (authDate.after(f.parse("2020-06-01 00:00:00" )) && authDate.before(f.parse("2020-06-25 23:59:59" ))) { date = AuthService.queryAuthInfo("1000012" , orderId); if (null == date) return new AuthInfo ("0001" , "单号:" , orderId, " 状态:待二级审批负责人 " , "张经理" ); } if (authDate.after(f.parse("2020-06-11 00:00:00" )) && authDate.before(f.parse("2020-06-20 23:59:59" ))) { date = AuthService.queryAuthInfo("1000011" , orderId); if (null == date) return new AuthInfo ("0001" , "单号:" , orderId, " 状态:待一级审批负责人 " , "段总" ); } return new AuthInfo ("0001" , "单号:" , orderId, " 状态:审批完成" ); } }
这里从上到下分别判断了在指定时间范围内由不同的人员进行审批,就像618上线的时候需要三个负责人都审批才能让系统进行上线。
像是这样的功能看起来很简单的,但是实际的业务中会有很多部门,但如果这样实现就很难进行扩展,并且在改动扩展调整也非常麻烦。
参考:
设计模式之美 (geekbang.org)
[重学 Java 设计模式:实战责任链模式「模拟618电商大促期间,项目上线流程多级负责人审批场景」 | 小傅哥 bugstack 虫洞栈](https://bugstack.cn/md/develop/design-pattern/2020-06-18-重学 Java 设计模式《实战责任链模式》.html)