死磕设计模式-责任链模式

1,976 阅读18分钟

死磕设计模式之如何抽象责任链模式

学习设计模式有什么作用

文章开篇,抛出一个老生常谈的问题,学习设计模式有什么作用?
设计模式主要是为了应对代码的复杂性,让其满足开闭原则,提高代码的扩展性。

另外,学习的设计模式 一定要在业务代码中落实,只有理论没有真正实施,是无法真正掌握并且灵活运用设计模式的。

下面会先从这个几个方面展开说:

责任链设计模式,认识此模式是在读 Mybatis 源码时, Interceptor 拦截器主要使用的就是责任链,当时读过后就留下了很深的印象(内心 OS:还能这样玩)

文章先从基础概念说起,另外分析一波 Mybatis 源码中是如何运用的,最后按照 "习俗",设计一个真实业务场景上的应用。

什么是责任链模式

举个例子,SpringMvc 中可以定义拦截器,并且可以定义多个。当一个用户发起请求时,顺利的话请求会经过所有拦截器,最终到达业务代码逻辑,SpringMvc 拦截器设计就是使用了责任链模式。
为什么说顺利的话会经过所有拦截器?因为请求不满足拦截器自定义规则会被打回,但这并不是责任链模式的唯一处理方式,继续往下看。

在责任链模式中,多个处理器(参照上述拦截器)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条,链条上的每个处理器 各自承担各自的处理职责。

责任链模式中多个处理器形成的处理器链在进行处理请求时,有两种处理方式:

  1. 请求会被 所有的处理器都处理一遍,不存在中途终止的情况,这里参照 MyBatis 拦截器理解。

  2. 二则是处理器链执行请求中,某一处理器执行时,如果不符合自制定规则的话,停止流程,并且剩下未执行处理器就不会被执行,大家参照 SpringMvc 拦截器理解。

这里通过代码的形式对两种处理方式作出解答,方便读者更好的理解。首先看下第一种,请求会经过所有处理器执行的情况。

image.png

IHandler 负责抽象处理器行为,handle() 则是不同处理器具体需要执行的方法,HandleAHandleB 为具体需要执行的处理器类,HandlerChain 则是将处理器串成一条链执行的处理器链。

public class ChainApplication {
    public static void main(String[] args) {
        HandlerChain handlerChain = new HandlerChain();
        handlerChain.addHandler(Lists.newArrayList(new HandlerA(), new HandlerB()));
        handlerChain.handle();
        /**
         * 程序执行结果:
         * HandlerA打印:执行 HandlerA
         * HandlerB打印:执行 HandlerB
         */
    }
}

这种责任链执行方式会将所有的 处理器全部执行一遍,不会被打断。Mybatis 拦截器用的正是此类型,这种类型 重点在对请求过程中的数据或者行为进行改变。

责任链模式-迅捷PDF转换器.gif

而另外一种责任链模式实现,则是会对请求有阻断作用,阻断产生的前置条件是在处理器中自定义的,代码中的实现较简单,读者可以联想 SpringMvc 拦截器的实现流程。

image.png

根据代码看的出来,在每一个 IHandler 实现类中会返回一个布尔类型的返回值,如果返回布尔值为 false,那么责任链发起类会中断流程,剩余处理器将不会被执行。就像我们定义在 SpringMvc 中的 Token 拦截器,如果 Token 失效就不能继续访问系统,处理器将请求打回。

public class ChainApplication {
    public static void main(String[] args) {
        HandlerChain handlerChain = new HandlerChain();
        handlerChain.addHandler(Lists.newArrayList(new HandlerA(), new HandlerB()));
        boolean resultFlag = handlerChain.handle();
        if (!resultFlag) {
            System.out.println("责任链中处理器不满足条件");
        }
    }
}

读者可以自己在 IDEA 中实现两种不同的责任链模式,对比其中的不同,设想下业务中真实的应用场景,再或者可以跑 SpringBoot 项目,创建多个拦截器来佐证文中的说辞。

image.png

本章节介绍了责任链设计模式的具体语义,以及不同责任链实现类型代码举例,并以 Mybatis、SpringMvc 拦截器为参照点,介绍各自不同的代码实现以及应用场景。

责任链业务场景设计

趁热打铁,本小节对使用的真实业务场景进行举例说明。假设业务场景是这样的,我们 系统处在一个下游服务,因为业务需求,系统中所使用的 基础数据需要从上游中台同步到系统数据库。
基础数据包含了很多类型数据,虽然数据在中台会有一定验证,但是 数据只要是人为录入就极可能存在问题,遵从对上游系统不信任原则,需要对数据接收时进行一系列校验。

最初是要进行一系列验证原则才能入库的,后来因为工期问题只放了一套非空验证,趁着春节期间时间还算宽裕,把这套验证规则骨架放进去。

从我们系统的接入数据规则而言,个人觉得需要支持以下几套规则:

  1. 1必填项校验,如果数据无法满足业务所必须字段要求,数据一旦落入库中就会产生一系列问题。
  2. 2非法字符校验,因为数据如何录入,上游系统的录入规则是什么样的我们都不清楚,这一项规则也是必须的。
  3. 3长度校验,理由同上,如果系统某字段长度限制 50,但是接入来的数据 500长度,这也会造成问题。

为了让读者了解业务嵌入责任链模式的前因,这里列举了三套校验规则,当然真实中可能不止这三套。但是 一旦将责任链模式嵌入数据同步流程,就会 完全符合文初所提的开闭原则,提高代码的扩展性。
本案例设计模式中的开闭原则通过 Spring 提供支持,后续添加新的校验规则就可以不必修改原有代码。

这里要再强调下,设计模式的应用场景一定要灵活掌握,只有这样才能在合适的业务场景合理运用对象的设计模式。

既然设计模式场景说过了,最后说一下需要达成的业务需求。将一个批量数据经过处理器链的处理,返回出符合要求的数据分类。

image.png

定义顶级验证接口和一系列处理器实现类没什么难度,但是应该如何进行链式调用呢?

这一块代码需要有一定 Spring 基础才能理解,一起来看下 VerifyHandlerChain 如何将所有处理器串成一条链。

image.png

VerifyHandlerChain 处理流程如下:

    1. 实现自 InitializingBean 接口,在对应实现方法中获取 IOC 容器中类型为 VerifyHandler 的 Bean,也就是 EmptyVerifyHandler、SexyVerifyHandler。
    1. 将 VerifyHandler 类型的 Bean 添加到处理器链容器中。
    1. 定义校验方法 verify(),对入参数据展开处理器链的全部调用,如果过程中发现已无需要验证的数据,直接返回。

这里使用 SpringBoot 项目中默认测试类,来测试一下如何调用。

@SpringBootTest
class ChainApplicationTests {

    @Autowired
    private VerifyHandlerChain verifyHandlerChain;

    @Test
    void contextLoads() {
        List<Object> verify = verifyHandlerChain.verify(Lists.newArrayList("aska", "aska2"));
        System.out.println(verify);
    }
}

这样的话,如果客户或者产品提校验相关的需求时,我们只需要实现 VerifyHandler 接口新建个校验规则实现类就 OK 了,这样符合了设计模式的原则:满足开闭原则,提高代码的扩展性。

熟悉之前作者写过设计模式的文章应该知道,强调设计模式重语意,而不是具体的实现过程。所以,你看这个咱们这个校验代码,把责任链两种模式结合了使用。

上面的代码只是示例代码,实际业务中的实现要比这复杂很多,比如:

  1. 如何定义处理器的先后调用顺序。比如说某一个处理器执行时间很长并且过滤数据很少,所以希望把它放到最后面执行。
  2. 这是为当前业务的所有数据类型进行过滤,如何自定义单个数据类型过滤。比如你接入学生数据,学号有一定校验规则,这种处理器类肯定只适合单一类型。

还有很多的业务场景,所以设计模式强调的应该是一种思想,而不是固定的代码写法,需要结合业务场景灵活变通。

责任链模式的好处

一定要使用责任链模式么?不使用能不能完成业务需求?

回答是肯定可以,设计模式只是帮助减少代码的复杂性,让其满足开闭原则,提高代码的扩展性。如果不使用同样可以完成需求。

如果不使用责任链模式,上面说的真实同步场景面临两个问题:

  1. 如果把上述说的代码逻辑校验规则写到一起,毫无疑问这个类或者说这个方法函数奇大无比。减少代码复杂性一贯方法是:将大块代码逻辑拆分成函数,将大类拆分成小类,是应对代码复杂性的常用方法。如果此时说:可以把不同的校验规则拆分成不同的函数,不同的类,这样不也可以满足减少代码复杂性的要求么。这样拆分是能解决代码复杂性,但是这样就会面临第二个问题。
  2. 开闭原则:添加一个新的功能应该是,在已有代码基础上扩展代码,而非修改已有代码。大家设想一下,假设你写了三套校验规则,运行过一段时间,这时候领导让加第四套,是不是要在原有代码上改动。

综上所述,在合适的场景运用适合的设计模式,能够让代码设计复杂性降低,变得更为健壮。朝更远的说也能让自己的编码设计能力有所提高,告别被人吐槽的烂代码...

Mybatis Interceptor 底层实现

上面说了那么多,框架底层源码是怎么设计并且使用责任链模式的?之前在看 Mybatis 3.4.x 源码时了解到 Interceptor 底层实现就是责任链模式,这里和读者分享 Interceptor 具体实现。

开门见山,直接把视线聚焦到 Mybatis 源码,版本号 3.4.7-SNAPSHOT

image.png

熟悉么?是不是和我们上面用到的责任链模式差不太多,有处理器集合 interceptors,有添加处理器方法。

Mybatis Interceptor 不仅用到了责任链,还用到了动态代理,服务于 Mybatis 四大 "护教法王",在创建对象时通过动态代理和责任链相结合组装而成插件模块。

  1. ParameterHandler。
  2. ResultSetHandler。
  3. StatementHandler。
  4. Executor。

使用过 Mybatis 的读者应该知道,查询 SQL 的分页语句就是使用 Interceptor 实现,比如市场上的 PageHelper、Mybatis-Plus 分页插件再或者我们自实现的分页插件(应该没有项目组使用显示调用多条语句组成分页吧)。

拿查询语句举例,如果定义了多个查询相关的拦截器,会先经过拦截器的代码加工,所有的拦截器执行完毕后才会走真正查询数据库操作。

image.png

扯的话就扯远了,能够知道如何用、在哪用就可以了。通过 Interceptor 也能知道一点,想要读框架源码,需要一定的设计模式基础。如果对责任链、动态代理不清楚,那么就不能理解这一块的精髓。

下单场景实战

1. 下单前置校验

在电商系统下单接口中,前置校验是非常重要的环节。下面是一个可能的校验步骤列表:

  • ●检查商品信息是否存在,包括商品名称、价格、规格等信息。
  • ●检查购买数量是否合法,是否超出了最大购买数量或最小购买数量的限制。
  • ●检查商品库存是否充足,以确保库存足够满足购买者的需求。
  • ●检查购买者的优惠券、积分等是否可以使用,以确保购买者能够享受相应的优惠或积分奖励。
  • ●检查收货地址信息是否完整和准确,以确保商品能够顺利地送达给购买者。
  • ●检查下单时间是否合法,例如检查购买者是否在限定的时间范围内下单。

真实电商场景中,验证逻辑绝对不仅仅是这些,过之而无不及。

对于完成这些前置校验逻辑,大部分程序员可能的代码思路如下:

public String createOrder(CreateOrderReqDTO xxx) {
   
    // 检查商品信息是否存在,包括商品名称、价格、规格等信息
    
    // 检查购买数量是否合法,是否超出了最大购买数量或最小购买数量的限制
    
    // 检查商品库存是否充足,以确保库存足够满足购买者的需求
   
    // 检查购买者的优惠券、积分等是否可以使用,以确保购买者能够享受相应的优惠或积分奖励
   
    // 检查收货地址信息是否完整和准确,以确保商品能够顺利地送达给购买者
  
   // 检查下单时间是否合法,例如检查购买者是否在限定的时间范围内下单
    // ......
}

解决前置校验需求需要实现一堆逻辑,常常需要写上几百上千行代码。

为了避免这种代码臃肿的情况,我们可以运用责任链设计模式,对下单验证逻辑进行抽象。

2. 责任链重构

定义一个责任链处理器接口,所有子任务都实现该接口以处理具体的业务逻辑。

同时,为了方便对责任链流程中的任务进行顺序处理,我们需要继承 Spring 框架中的排序接口 Ordered。这将有助于保证责任链中的任务顺序执行。

public interface OrderCreateChainHandler<T> extends Ordered {
    
    /**
     * 执行责任链逻辑
     *
     * @param requestParam 责任链执行入参
     */
    void handler(T requestParam);
}

创建一个责任链上下文容器,用于存储与责任链相应的子任务。

public final class OrderCreateChainContext<T> implements CommandLineRunner {
    
    private final List<OrderCreateChainHandler> orderCreateChainHandlerContainer = new ArrayList();
    
    /**
     * 责任链组件执行
     *
     * @param requestParam 请求参数
     */
    public void handler(T requestParam) {
        // 此处根据 Ordered 实际值进行排序处理
        orderCreateChainHandlerContainer.stream()
                .sorted(Comparator.comparing(Ordered::getOrder)).forEach(each -> each.handler(requestParam));
    }
    
    @Override
    public void run(String... args) throws Exception {
      	// 通过 Spring 上下文容器,获取所有 CreateOrderChainContext Bean
        Map<String, OrderCreateChainHandler> chainFilterMap = ApplicationContextHolder.getBeansOfType(OrderCreateChainHandler.class);
      	// 将对应 Bean 放入责任链上下文容器中
        chainFilterMap.forEach((beanName, bean) -> orderCreateChainHandlerContainer.add(bean););
    }
}

实现 OrderCreateChainHandler 接口作为责任链处理器,每个具体的实现类负责执行特定的逻辑。

// 订单创建参数必填检验
@Component
public final class OrderCreateParamNotNullChainHandler implements OrderCreateChainHandler<OrderCreateCommand> {
    
    @Override
    public void handler(OrderCreateCommand requestParam) {
	    // 逻辑执行
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

// 订单创建参数正确性检验
@Component
public final class OrderCreateParamVerificationChainHandler implements OrderCreateChainHandler<OrderCreateCommand> {
    
    @Override
    public void handler(OrderCreateCommand requestParam) {
	    // 逻辑执行
    }
    
    @Override
    public int getOrder() {
        return 1;
    }
}

// 订单创建商品 SKU 库存验证
@Component
public final class OrderCreateProductSkuStockChainHandler implements OrderCreateChainHandler<OrderCreateCommand> {

    @Override
    public void handler(OrderCreateCommand requestParam) {
	    // 逻辑执行
    }
    
    @Override
    public int getOrder() {
        return 2;
    }
}

通过责任链模式优化,创建订单接口前置校验代码从上千行缩减为一行。

@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final OrderCreateChainContext<OrderCreateCommand> orderCreateChainContext;
  
    public String createOrder(OrderCreateCommand requestParam) {
        // 责任链模式: 执行订单创建参数验证
        orderCreateChainContext.handler(requestParam);
    }
}

经过责任链模式的重构,你是否发现业务逻辑变得更加清晰易懂了?采用这种设计模式后,增加或删除相关的业务逻辑变得非常方便,不再需要担心更改上千行代码的几行代码,导致整个业务逻辑受到影响的情况。

责任链抽象

可能细心的小伙伴会发现一个问题,当业务使用越来越多的情况下,重复定义 OrderCreateChainHandler 以及 OrderCreateChainContext 会增加系统冗余代码量

可以考虑将这两个基础类抽象出来,作为基础组件库中的通用组件,供所有系统下的业务使用,从而避免代码冗余。
如果想了解如何实现这一操作

1. 抽象基础类

定义抽象责任链处理接口,等同于 OrderCreateChainHandler。

public interface AbstractChainHandler<T> extends Ordered {
    
    /**
     * 执行责任链逻辑
     *
     * @param requestParam 责任链执行入参
     */
    void handler(T requestParam);
    
    /**
     * @return 责任链组件标识
     */
    String mark();
}
1 mark 方法是做什么的

接口增加 mark 方法,以便不同业务使用不同的标识。
假设项目中有两个业务场景:订单下单和用户创建都需要责任链模式去验证,mark 就是用来进行分组,在业务中进行调用责任链时传递不同的 mark 方法参数,通过该参数找到对应的一组责任链具体实现类集合。

2 定义抽象责任链上下文

等同于 OrderCreateChainContext。可以看到保存责任链处理类的容器从 List 改为了 Map,这样可以方便扩展更多的不同业务责任链子类。

3 如何注册到责任链上下文中

通过实现 CommandLineRunner 接口在 SpringBoot 启动完成后,执行钩子函数将所有实现责任链抽象接口的实现类进行注册到责任链上下文中。

public final class AbstractChainContext<T> implements CommandLineRunner {
    
    private final Map<String, List<AbstractChainHandler>> abstractChainHandlerContainer = Maps.newHashMap();
    
    /**
     * 责任链组件执行
     *
     * @param requestParam 请求参数
     */
    public void handler(String mark, T requestParam) {
        abstractChainHandlerContainer.get(mark).stream()
                .sorted(Comparator.comparing(Ordered::getOrder)).forEach(each -> each.handler(requestParam));
    }
    
    @Override
    public void run(String... args) throws Exception {
        // 获取 Spring IOC 容器中所有 AbstractChainHandler 接口实现
        Map<String, AbstractChainHandler> chainFilterMap = ApplicationContextHolder.getBeansOfType(AbstractChainHandler.class);
        chainFilterMap.forEach((beanName, bean) -> {
            List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(bean.mark());
            if (abstractChainHandlers == null) {
                abstractChainHandlers = new ArrayList();
            }
            abstractChainHandlers.add(bean);
            // 根据 mark 标识将责任链模式分类,放入责任链容器上下文中
            abstractChainHandlerContainer.put(bean.mark(), abstractChainHandlers);
        });
    }
}

这两个接口已经被放置在基础组件库中,如果业务需要使用责任链模式,则无需重新定义。

现在,让我们来看看业务代码需要编写哪些逻辑。

2. 抽象业务接口

让我们先进行头脑风暴,思考一下这个接口的用途是什么?

// 订单创建责任链过滤器
public interface OrderCreateChainFilter<T extends OrderCreateCommand> extends AbstractChainHandler<OrderCreateCommand> {
    
    @Override
    default String mark() {
        return OrderChainMarkEnum.ORDER_CREATE_FILTER.name();
    }
}

首先,如果没有 OrderCreateChainFilter 接口,会是什么样的场景?

  1. ●由于责任链处理子类都需要依赖顶级抽象接口,因此要想知道某个业务场景下有多少具体子类是相当困难的。
  2. ●由于责任链处理子类都需要实现 Mark 方法,实际上某一类责任链子类的 Mark 方法返回值是相同的。

通过在业务层面上抽象出一个具体业务责任链接口,就能很好地解决上述两个问题。

现在,让我们继续探讨责任链子类的编写。实际上,改动并不多,只需要将之前的  OrderCreateChainHandler 实现接口改为  OrderCreateChainFilter  即可。

// 订单创建参数必填检验
@Component
public final class OrderCreateParamNotNullChainHandler implements OrderCreateChainFilter<OrderCreateCommand> {
    
    @Override
    public void handler(OrderCreateCommand requestParam) {
    	// 逻辑执行
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

// 订单创建参数正确性检验
@Component
public final class OrderCreateParamVerificationChainHandler implements OrderCreateChainFilter<OrderCreateCommand> {
    
    @Override
    public void handler(OrderCreateCommand requestParam) {
    	// 逻辑执行
    }
    
    @Override
    public int getOrder() {
        return 1;
    }
}

// 订单创建商品 SKU 库存验证
@Component
public final class OrderCreateProductSkuStockChainHandler implements OrderCreateChainFilter<OrderCreateCommand> {

    @Override
    public void handler(OrderCreateCommand requestParam) {
    	// 逻辑执行
    }
    
    @Override
    public int getOrder() {
        return 2;
    }
}

3. 业务使用

在具体业务场景中使用时,与之前相比并没有太大的差别。除了增加了 Mark 标识外,没有进行其他变更。

@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final AbstractChainContext<OrderCreateCommand> abstractChainContext;
  
    public String createOrder(OrderCreateCommand requestParam) {
        // 责任链模式: 执行订单创建参数验证
        abstractChainContext.handler(OrderChainMarkEnum.ORDER_CREATE_FILTER.name(), requestParam);
    }
}

文末总结

本文详细介绍了责任链模式的概念,并通过电商下单场景模拟了真实使用场景。
为了复用责任链接口定义和上下文,我们通过抽象的方式将责任链门面接口加入到基础组件库中,实现快速接入责任链的目的。

虽然在本文中,我们没有使用 boolean 类型的返回值,而是通过异常来终止流程,但在后续的增强中,我们可以考虑添加布尔类型的返回值。

此外,我们还可以在 AbstractChainHandler 中增加是否异步执行的方法,以提高方法执行性能和减少接口响应时间。

架构设计总是在不断演进,本文的设计也有优化和进步的空间,让我们继续探索责任链模式的更多可能性。