一、什么是责任链模式

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

这种模式的主要优点是降低了请求发送者和接收者之间的耦合度,使得系统更加灵活和可扩展。

常见的场景有:审批流程,参数验证等

审批流程.jpeg 图1 审批流程

图2 参数校验逻辑

解决这类需求需要实现一堆逻辑,常常需要写上几百上千行代码,类会变得庞大且臃肿。并且,上面的代码违反了"开闭原则"。为了避免这种坏代码味道,我们需要运用责任链设计模式

二、代码实现

下面用一个创建优惠券的案例来说明责任链模式怎么结合spring进行处理

2.1 定义责任链抽象接口

public interface MerchantAdminAbstractChainHandler<T> extends Ordered {

    /**
     * 执行责任链逻辑
     * @param requestParam 责任链执行入参
     */
    void handler(T requestParam);
    
    /**
     * 责任链组件标识
     */
    String mark();
}

2.2 责任链链路

2.2.1 CouponTemplateCreateParamNotNullChainFilter

service/handler/filter/CouponTemplateCreateParamNotNullChainFilter.java

/**
 * 验证优惠券创建接口参数是否正确责任链|验证必填参数是否为空或空的字符串
 */
@Component
public class CouponTemplateCreateParamNotNullChainFilter implements MerchantAdminAbstractChainHandler<CouponTemplateSaveReqDTO> {

    @Override
    public void handler(CouponTemplateSaveReqDTO requestParam) {
        if (StrUtil.isEmpty(requestParam.getName())) {
            throw new ClientException("优惠券名称不能为空");
        }

        if (ObjectUtil.isEmpty(requestParam.getSource())) {
            throw new ClientException("优惠券来源不能为空");
        }

        if (ObjectUtil.isEmpty(requestParam.getTarget())) {
            throw new ClientException("优惠对象不能为空");
        }

        if (ObjectUtil.isEmpty(requestParam.getType())) {
            throw new ClientException("优惠类型不能为空");
        }

        if (ObjectUtil.isEmpty(requestParam.getValidStartTime())) {
            throw new ClientException("有效期开始时间不能为空");
        }

        if (ObjectUtil.isEmpty(requestParam.getValidEndTime())) {
            throw new ClientException("有效期结束时间不能为空");
        }

        if (ObjectUtil.isEmpty(requestParam.getStock())) {
            throw new ClientException("库存不能为空");
        }

        if (StrUtil.isEmpty(requestParam.getReceiveRule())) {
            throw new ClientException("领取规则不能为空");
        }

        if (StrUtil.isEmpty(requestParam.getConsumeRule())) {
            throw new ClientException("消耗规则不能为空");
        }
    }

    @Override
    public String mark() {
        return MERCHANT_ADMIN_CREATE_COUPON_TEMPLATE_KEY.name();
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

2.2.2 CouponTemplateCreateParamBaseVerifyChainFilter

service/handler/filter/CouponTemplateCreateParamBaseVerifyChainFilter.java

/**
 * 验证优惠券创建接口参数是否正确责任链|验证参数基本数据关系是否正确
 */
@Component
public class CouponTemplateCreateParamBaseVerifyChainFilter implements MerchantAdminAbstractChainHandler<CouponTemplateSaveReqDTO> {

    private final int maxStock = 20000000;

    @Override
    public void handler(CouponTemplateSaveReqDTO requestParam) {
        boolean targetAnyMatch = Arrays.stream(DiscountTargetEnum.values())
                .anyMatch(enumConstant -> enumConstant.getType() == requestParam.getTarget());
        if (!targetAnyMatch) {
            // 此处已经基本能判断数据请求属于恶意攻击,可以上报风控中心进行封禁账号
            throw new ClientException("优惠对象值不存在");
        }
        if (ObjectUtil.equal(requestParam.getTarget(), DiscountTargetEnum.ALL_STORE_GENERAL)
                && StrUtil.isNotEmpty(requestParam.getGoods())) {
            throw new ClientException("优惠券全店通用不可设置指定商品");
        }
        if (ObjectUtil.equal(requestParam.getTarget(), DiscountTargetEnum.PRODUCT_SPECIFIC)
                && StrUtil.isEmpty(requestParam.getGoods())) {
            throw new ClientException("优惠券商品专属未设置指定商品");
        }

        boolean typeAnyMatch = Arrays.stream(DiscountTypeEnum.values())
                .anyMatch(enumConstant -> enumConstant.getType() == requestParam.getType());
        if (!typeAnyMatch) {
            // 此处已经基本能判断数据请求属于恶意攻击,可以上报风控中心进行封禁账号
            throw new ClientException("优惠类型不存在");
        }

        Date now = new Date();
        if (requestParam.getValidStartTime().before(now)) {
            // 为了方便大家测试,不用关注这个时间,这里取消异常抛出
            // throw new ClientException("有效期开始时间不能早于当前时间");
        }

        if (requestParam.getStock() <= 0 || requestParam.getStock() > maxStock) {
            // 此处已经基本能判断数据请求属于恶意攻击,可以上报风控中心进行封禁账号
            throw new ClientException("库存数量设置异常");
        }

        if (!JSON.isValid(requestParam.getReceiveRule())) {
            // 此处已经基本能判断数据请求属于恶意攻击,可以上报风控中心进行封禁账号
            throw new ClientException("领取规则格式错误");
        }
        if (!JSON.isValid(requestParam.getConsumeRule())) {
            // 此处已经基本能判断数据请求属于恶意攻击,可以上报风控中心进行封禁账号
            throw new ClientException("消耗规则格式错误");
        }
    }

    @Override
    public String mark() {
        return MERCHANT_ADMIN_CREATE_COUPON_TEMPLATE_KEY.name();
    }

    @Override
    public int getOrder() {
        return 10;
    }
}

2.2.3 CouponTemplateCreateParamVerifyChainFilter

service/handler/filter/ CouponTemplateCreateParamVerifyChainFilter.java

/**
 * 验证优惠券创建接口参数是否正确责任链|验证参数数据是否正确
 */
@Component
public class CouponTemplateCreateParamVerifyChainFilter implements MerchantAdminAbstractChainHandler<CouponTemplateSaveReqDTO> {

    @Override
    public void handler(CouponTemplateSaveReqDTO requestParam) {
        if (ObjectUtil.equal(requestParam.getTarget(), DiscountTargetEnum.PRODUCT_SPECIFIC)) {
            // 调用商品中台验证商品是否存在,如果不存在抛出异常
            // ......
        }
    }

    @Override
    public String mark() {
        return MERCHANT_ADMIN_CREATE_COUPON_TEMPLATE_KEY.name();
    }

    @Override
    public int getOrder() {
        return 20;
    }
}

2.3 创建责任链模式上下文容器

service/basics/chain/MerchantAdminChainContext

@Component
//实现ApplicationContextAware方法主要是获取应用上下文applicationContext
public final class MerchantAdminChainContext<T> implements ApplicationContextAware, CommandLineRunner {

    /**
     * 应用上下文,我们这里通过 Spring IOC 获取 Bean 实例
     */
    private ApplicationContext applicationContext;
    
    /**
     * 保存商家后管责任链实现类
     * 
     * 比如有一个优惠券模板创建责任链,实例如下:
     * Key:MERCHANT_ADMIN_CREATE_COUPON_TEMPLATE_KEY
     * Val:
     * - 验证优惠券信息基本参数是否必填 —— 执行器 {@link CouponTemplateCreateParamNotNullChainFilter}
     * - 验证优惠券信息基本参数是否按照格式传递 —— 执行器 {@link CouponTemplateCreateParamBaseVerifyChainFilter}
     * - 验证优惠券信息基本参数是否正确,比如商品数据是否存在等 —— 执行器 {@link CouponTemplateCreateParamVerifyChainFilter}
     * - ......
     */
    private final Map<String, List<MerchantAdminAbstractChainHandler>> abstractChainHandlerContainer = new HashMap<>();

    /**
     * 责任链组件执行
     *
     * @param mark         责任链组件标识
     * @param requestParam 请求参数
     */
    public void handler(String mark, T requestParam) {
        // 根据 mark 标识从责任链容器中获取一组责任链实现 Bean 集合
        List<MerchantAdminAbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(mark);
        if (CollectionUtils.isEmpty(abstractChainHandlers)) {
            throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
        }
        abstractChainHandlers.forEach(each -> each.handler(requestParam));
    }

    //实现了CommandLineRunner接口,springBoot启动后自动执行run方法
    @Override
    public void run(String... args) throws Exception {
        // 从 Spring IOC 容器中获取指定接口 Spring Bean 集合,getBeansOfType方法获取一个map
        Map<String, MerchantAdminAbstractChainHandler> chainFilterMap = applicationContext.getBeansOfType(MerchantAdminAbstractChainHandler.class);
        chainFilterMap.forEach((beanName, bean) -> {
            // 判断 Mark 是否已经存在抽象责任链容器中,如果已经存在直接向集合新增;如果不存在,创建 Mark 和对应的集合
            List<MerchantAdminAbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.getOrDefault(bean.mark(), new ArrayList<>());
            abstractChainHandlers.add(bean);
            abstractChainHandlerContainer.put(bean.mark(), abstractChainHandlers);
        });
        abstractChainHandlerContainer.forEach((mark, unsortedChainHandlers) -> {
            // 对每个 Mark 对应的责任链实现类集合进行排序,优先级小的在前
            unsortedChainHandlers.sort(Comparator.comparing(Ordered::getOrder));
        });
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

2.4 使用

@Service
@RequiredArgsConstructor
public class CouponTemplateServiceImpl extends ServiceImpl<CouponTemplateMapper, CouponTemplateDO> implements CouponTemplateService {

    //注入上下文,直接调用即可
    private final MerchantAdminChainContext merchantAdminChainContext;
  
    @Override
    public void createCouponTemplate(CouponTemplateSaveReqDTO requestParam) {
        // 通过责任链验证请求参数是否正确
        merchantAdminChainContext.handler(ChainBizMarkEnum.MERCHANT_ADMIN_CREATE_COUPON_TEMPLATE_KEY.name(), requestParam);
        // xxxxxx
    }
}

相关文章

SpringBoot实现模板+策略模式