责任链模式在转转精准估价中的应用

一、业务简介

转转线上 C2B 回收业务,主要是针对 3C 数码产品进行回收。

而用户在下单前,通过填写机器信息获取预估报价的过程,即精准估价。

精准估价作为回收流程的第一步,对业务来说是较为重要的。

常规估价流程

二、早期遇到的问题

  • 粗糙:初期粗糙的产品模式衍生了粗糙的代码,对于后续的扩展和维护造成困难。
//判断是新用户发放加价红包
if(newUser) {
	sendCoupon();
}
//数码走单独的报价逻辑
if(shuma) {
	shumaPrice();
}
//手机走单独的报价逻辑
if(shouji) {
	shoujiPrice();
}
//处理异常数据
if(price == null) {
	return 兜底数值;
}
  • 变化快:核心节点,产品的迭代频繁,因为较低的维护性改起来较痛苦。
  • 逻辑复杂:场景多、系统多、报价方多、计算逻辑复杂等。导致整体复杂度较高。
  • 性能问题:当报价的能力被使用到更多业务场景后,对于系统性能也有不小的挑战。

因此需要对模块进行重新设计

三、责任链

什么是责任链:

责任链模式(Chain of Responsibility Pattern):责任链模式是一种对象的行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。

为什么选择使用责任链重写业务代码?

先看看责任链模式的特点

责任链的优点:

  • 单一职责:⼀个类和⽅法只做⼀件事 。降低了耦合度,调用方不需要知道请求由哪些 handler 处理,而 handler 也不需要知道互相之间的传递关系,由系统组织和分配。
  • 开闭原则:一个软件实体(如类、模块和函数)应该对扩展开放,对修改关闭。带来良好的扩展性,使得增加处理者的实现很简单,可以在链路中任意位置插入新的 handler。

责任链的缺点:

  • 链路长:请求从链头发出,直到被中断或者结束,可能会影响系统性能。
  • 排查较慢:一般是不太能一目了然的看到是哪个节点出现了问题,需要多添加一些核心日志。

再看看较为常见的估价流程

特点:

  • 链式结构
  • 每个模块职责单一

通过比较业务的特点和责任链的特点,可以看出是较为契合的。所以选择以责任链模式为基础,开始重写业务逻辑。

四、实战应用

首先定义抽象处理者,可以是接口或者是抽象类,或者二者结合。定义好需要每个处理者实现的功能。

public interface ValuationResponsibilityChainHandle<ChainResponsibilityContext, ChainResponsibilityRequest,ChainResponsibilityResponse> {

    /**
     * 处理
     * @param context
     * @param request
     * @param response
     */
    void handle(ChainResponsibilityContext context, ChainResponsibilityRequest request, ChainResponsibilityResponse response);

 	/**
     * 设置下一个责任处理器
     * @param nextHandle
     */
    void setNextHandler(ValuationResponsibilityChainHandle<ChainResponsibilityContext,ChainResponsibilityRequest,ChainResponsibilityResponse> nextHandle);

    /**
     * 获取下一个责任处理器
     * @return
     */
    ValuationResponsibilityChainHandle<ChainResponsibilityContext,ChainResponsibilityRequest,ChainResponsibilityResponse>  getNextHandler();
}

处理者负责实现抽象处理者。用黑名单校验模块的举例。

public class BlacklistHandler extends AbstractValuationChainValuationResponsibilityHandle<ValuationContext, ValuationRequest, BmDealPriceDto> {

    @Autowired
    private ValuationApolloService valuationApolloService;

    @Override
    public void handle(ValuationContext context, ValuationRequest request, BmDealPriceDto bmDealPriceDto) {
        log.info("精准估价-黑名单校验, context={}, bmDealPriceDto={}", context, bmDealPriceDto);
        if(Objects.isNull(request.getUid())) {
            return;
        }
        if(!valuationApolloService.getBmValuationBlackList().contains(request.getUid())) {
            return;
        }
        context.setSkipNode(true);
		 //找下个处理者
        this.invokeNext(context, chainResponsibilityRequest,chainResponsibilityResponse);
    }

	//设置下一个处理者
    @Override
    public void setNextHandler(ValuationResponsibilityChainHandle<ChainResponsibilityContext, ChainResponsibilityRequest, ChainResponsibilityResponse> nextHandler) {
        this.nextHandler = nextHandler;
    }

}

初始化黑名单模块,一个简单的责任链就可以跑起来了。

public static void main(String[] args) {
        BlacklistHandler blacklistHandler = new BlacklistHandler();
        blacklistHandler.handle();
    }

但是,如何动态组装责任链?

上述的方式每一个处理者和下个处理者的关系是固定的,没有办法根据入参去做动态的组装,需要进行一些调整。

责任链+建造者

我们定义一个内部的 Builder类,负责通过场景类型给到我们对应的整组责任链。

调用方请求时,根据场景获取链路类型。

private int getChainType(String params1, String params2) {
        //根据参数组装出场景key
        String mapKey = params1 + params2;
 		 //从配置中获取到key对应的链路类型
        return BuildChainConstant.CHAIN_MAP.getOrDefault(key, -1);
    }

根据获取到的链路类型获取指定的责任链。

   //根据类型找链路
	public List<AbstractValuationChainValuationResponsibilityHandle> buildChain(Integer type) {
        List<AbstractValuationChainValuationResponsibilityHandle> chain = Lists.newArrayList();
        switch (type) {
            case BuildChainConstant.SCENE_A:
                buildSceneA(chain);
                break;
            default:
                break;
        }
        return chain;
    }

	//某一链路中含有的所有handler
    public void buildSceneA(List<AbstractValuationChainValuationResponsibilityHandle> chain) {
        chain.add(blacklistHandler);
        chain.add(buildCategoryBrandModelInfoHandler);
        chain.add(dealWithFloatCouponHandler);
        chain.add(saveVirtualQcReportHandler);
        chain.add(getBmPriceHandler);
        chain.add(calculateCouponPriceHandler);
        chain.add(saveUserEvalRecordHandler);
        chain.add(otherFunctionHandler);
        chain.add(environmentalRecycleHandler);
    }

拿到责任链后,进行遍历执行。

public void handle(ChainResponsibilityContext context, ChainResponsibilityRequest request,ChainResponsibilityResponse response, List<AbstractValuationChainValuationResponsibilityHandle> handleList) {
        if(!CollectionUtils.isEmpty(handleList)) {
            int count = handleList.size();
            for (int i = 0; i < count; i++) {
               	 handleList.get(i).handle(context, request, response);
            }
        }
    }

组合在一起,获取报价的接口就写好了。

public void getPrice(ValuationRequest valuationRequest) {
		//根据业务场景数据确定链路类型
        int chainType = getChainType(valuationRequest.getCateId(), valuationRequest.getEvaPlanType());
        //拿到对应链路
        List<AbstractValuationChainValuationResponsibilityHandle> handleList=valuationChainBuilder.buildChain(chainType);
		//执行链路
        this.handle(context, valuationRequest, bmDealPriceDto, handleList);
    }

如何控制流程中断?

设想一个场景:链路中 A 负责报价 ,B 负责有报价的处理 ,C 负责无报价的处理。 而有没有报价我们是没有办法预先知道的,只有在程序执行时才能拿到结果。如果我此时的链路是 A → B → C, 常见的方式是 BC 在执行时先判断一下是否有价格再去执行。或者我们也可以通过再去定义一个方法,来让处理者根据当前的上下文来判断是否需要执行,使得职责更清晰。

抽象处理者定义 skipNode 判断是否执行的方法。

public interface ValuationResponsibilityChainHandle<ChainResponsibilityContext, ChainResponsibilityRequest,ChainResponsibilityResponse> {

    /**
     * 处理
     * @param context
     * @param request
     * @param response
     */
    void handle(ChainResponsibilityContext context, ChainResponsibilityRequest request, ChainResponsibilityResponse response);

    /**
     * 是否执行该节点逻辑
     */
    boolean skipNode(ChainResponsibilityContext context, ChainResponsibilityRequest request,ChainResponsibilityResponse response);
}

处理者实现该方法。

public class EnvironmentalRecycleHandler extends AbstractValuationChainValuationResponsibilityHandle<ValuationContext, ValuationRequest, BmDealPriceDto> {

    private static final Integer ERROR_CODE = 12345;


    @Override
    public void handle(ValuationContext context, ValuationRequest request, BmDealPriceDto bmDealPriceDto) {
        //DO-SOMETHING
    }

    //实现方法,根据上下文来判断当前状态下是否执行
    @Override
    public boolean skipNode(ValuationContext valuationContext, ValuationRequest valuationRequest, BmDealPriceDto bmDealPriceDto) {
        return Boolean.TRUE.equals(valuationContext.getSkipNode())
               && !Objects.equals(valuationContext.getCode(), ERROR_CODE);
    }
}

遍历时校验是否可以执行。

protected void handle(ChainResponsibilityContext context, ChainResponsibilityRequest request,ChainResponsibilityResponse response, List<AbstractValuationChainValuationResponsibilityHandle> handleList) {
        if(!CollectionUtils.isEmpty(handleList)) {
            int count = handleList.size();
            for (int i = 0; i < count; i++) {
 				//校验是否可以执行
                if(handleList.get(i).skipNode(context, request, response)){
                    continue;
                }
                handleList.get(i).handle(context, request, response);
            }
        }
    }

重写完毕后,我们通过责任链的优势,使代码复杂性降低,变得更为健壮,整体逻辑链路清晰。可以和过去让人头疼的代码说拜拜。

重写后的类关系图.png

其它典型应用--双向链

责任链在各个组件中应用很多,基本都大同小异。但是 Netty 的责任链相对来说有趣一些,我们用它来举例简单介绍在netty中是如何使用责任链。

ChannelHandler是 Netty 提供的默认处理者。核心的两个实现是ChannelInboundHandlerChannelOutboundHandler

  • ChannelInboundHandler:入站处理器,处理读数据的逻辑。
  • ChannelOutBoundHandler:出站处理器,处理写数据的逻辑。

Netty双向链

假如当前监听到一个读事件时,通过上下文 ChannelHandlerContext 获取到当前的 ChannelHandler 开始进行处理,找到第一个入站处理器,处理完成找下一个入站处理器。处理完毕后,最后一个入站处理器不会去找下个连接的出站处理器,而是直接跳转到 tail 节点。由 tail 节点从后往前去寻找第一个出站处理器。所以完整的处理流程会是:head→ inbound1 → inbound2 → inbound3 → tail → outbound3 → outbound2 → outbound1。

双向链较为特殊,适用场景偏少,适用于输入输出的场景。Netty 的实现方式也不是唯一的方式,只是适合它自己的用法。在此也是希望能拓宽大家的思路,写自己喜欢的代码。

还有一些典型的场景,列举在这。感兴趣可以抽空看看。

  • Spring Security : 处理 request 请求。
  • Spring AOP:当一个切入点(目标方法)有多个通知要织入时,这些需要织入的通知就形成了一个责任链。
  • Mybatis Plugins:如何对 Plugins 进行装配。
  • Dubbo Filter:调用时的一些过滤处理。

关于性能

在上文问题部分,提到了性能相关的问题,本身和责任链没有关系,这里作为题外话来聊聊。

首先明确的一点是,设计模式并不能帮助我们提高性能。

对于估价来说,核心能力都在下游,估价系统、营销系统、质检系统等。业务是对这些底层能力进行编排组合,获取产品期望的结果。所以一旦下游出现问题,上游比较被动。而对估价来讲,一个环节不好使,那就都不好使。只能告诉用户无法报价,没有什么操作空间。那如何做呢?

  • :如果下游接口耗时不稳定,一定要积极推动(给点压力)去优化。
  • :链路整体偏长、考虑是否能通过调整交互的方式,把某些节点提前。
  • :减轻代码流程中不必要的耗时操作,例如循环调用。
  • :进行压测,清楚自身系统能应对的流量峰值。
  • :做好监控,除了性能监控以外,会去统计异常数据,可以定时的查看精准估价模块的运行情况。
  • :为了防止一些恶意的行为,一定时间估价次数超过一定量的用户,我们会要求进行滑动验证,如若用户依旧持续估价,则屏蔽用户。

三、总结

本文从实际业务出发,讲解了如何用责任链模式编写业务逻辑。

日常工作中,业务总是在快速的向前探索,充满着变化和不确定性。

作为一名研发人员,开发的同时,尽量选择一个好的编码方式去支撑业务快速向前。


关于作者

多斯,转转 C2B 业务研发工程师

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。

关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~

猜你喜欢

转载自juejin.im/post/7124889640430993439