cover_image

可编排下单服务引擎设计

四海 玩物少年们
2022年04月29日 03:58


本期内容

  • 背景

  • 实现方案

    • 纵向解决业务隔离和流程编排

    • 横向解决逻辑复用和业务扩展


由笔者分享在多场景、多业务维度的订单下单业务中,透过可编排的业务框架提升代码可扩展性和可维护性。

背景

订单系统是电商非常重要的模块之一,在整个售卖的链路中,对接了商品、库存、支付、营销等上下游不同的系统,往往都会存在链路长、逻辑复杂的特点,还存在多场景、多类型、多业务维度等业务特性。如何解决订单的可扩展性和可维护性是需要重点关注和解决的问题。

以玩物得志为例,客户端入口有APP、小程序和多公众号等,按订单业务划分有一口价和拍卖订单,订单类型又有零元购、分销单、闲置、金币、秒杀、直播等。都是下单、完成订单创建,但是流程上相互之间又有差异。

当场景和其他一些维度组合时,每一种组合都可能会有不同的处理逻辑、也可能会存在共性的业务逻辑,这种情况下代码中各种if-else肯定是不敢想象的。怎么处理这种"多场景+多维度"的复杂下单流程,又要保证整个系统的可扩展性和可维护性,本文将和大家一起探讨一种实现方式。

实现方案

要解决"多场景+多维度"的复杂下单流程,我们从纵向和横向两个维度进行设计。纵向主要从业务隔离和流程编排的角度出发解决问题、而横向主要从逻辑复用和业务扩展的角度解决问题。

纵向解决业务隔离和流程编排

策略模式的应用

通常我们处理一个多维度的业务逻辑,都会采用策略模式来解决,其核心其实可以概括为一个词"分而治之"。策略模式是解决过多 if-else(或者 switch-case) 代码块的方法之一,提高代码的可维护性、可扩展性和可读性。一般是抽象一个基础逻辑接口、每一个类型都实现该接口,业务处理时根据不同的类型调用对应的业务实现,以达到逻辑相互独立互不干扰、代码隔离的目的。

这不仅仅是从架构的可扩展性和可维护性的角度出发,对于架构稳定性,隔离是一种减少影响面的常用手段。

我们从策略定义、实现、创建和策略使用四个步骤分析下单业务隔离的实现:

1、策略接口的定义

public interface OrderAddManager {

    /**
     * 添加订单
     *
     * @param addOrderDTO 要购买的订单信息
     * @return 返回的是泛型:支付id或新建的订单
     */

    <T> addOrder(AddOrderDTO addOrderDTO);
}

2、策略接口的实现

每种订单类型都实现了上述接口(基于接口而非实现编程),这样我们可以灵活的替换不同的下单方式。下边示例代码展示部分下单方式的实现,AbstractOrderAddManager实现addOrder的通用下单流程。其他下单类继承基础抽象类,可以复用addOrder逻辑,也可以使用定制的下单逻辑覆盖它。

//下单抽象类,实现addOrder
public abstract class AbstractOrderAddManager<Timplements OrderAddManager {
  @Override
    public <T> addOrder(AddOrderDTO addOrderDTO) {/*抽象公共逻辑*/}
}
//分销商品下单实现
public class DistributedResellOrderAddManagerImpl extends AbstractOrderAddManager<String{
  @Override
    public <T> addOrder(AddOrderDTO addOrderDTO) {/*分销业务逻辑*/}
}
//一口价下单实现
public class NegotiatedItemOrderAddManagerImpl extends AbstractOrderAddManager<String{
  @Override
    public <T> addOrder(AddOrderDTO addOrderDTO) {/*一口价业务逻辑*/}
}
//零元购下单实现
public class ZeroBuyOrderAddManagerImpl extends AbstractOrderAddManager<String{
  @Override
    public <T> addOrder(AddOrderDTO addOrderDTO) {/*零元购业务逻辑*/}
}

3、策略的创建

策略模式包含一组同类的策略,在使用时我们通常通过类型来判断创建哪种策略来进行使用。我们可以使用工厂模式来创建策略,以屏蔽策略的创建细节。如下代码所示,addManagerMap是类型到策略映射的map,getOrderType确定好订单类型后,返回map里对应的下单策略实现:

@Service
public class OrderAddFactory {

    /**
     * 策略实现的map
     */

    private static Map<Integer, OrderAddManager> addManagerMap = new HashMap<>();
  
   @PostConstruct
    public void init() {
        if (addManagerMap.isEmpty()) {
            addManagerMap.put(OrderTypeEnum.ORDER_C2C.getCode(), c2cOrderAddManager);
            addManagerMap.put(ORDER_BLIND_BOX.getCode(), blindBoxOrderAddManager);
            addManagerMap.put(ORDER_ZERO_BUY.getCode(), zeroBuyOrderAddManager);
          ......
            addManagerMap.put(ORDER_DISTRIBUTED_RESELL.getCode(),distResellOrderAddManager);
            addManagerMap.put(ORDER_ONE_BUY_NEGOTIATED.getCode(), negotiatedEndOrderAddManager);
          
        }
    }
  //获取订单类型对应的策略类
    public OrderAddManager getOrderAddManager(AddOrderDTO addOrderDTO) {
        // 获取订单类型
        Integer orderType = getOrderType(addOrderDTO);
        if (null == orderType) {
            return null;
        }
        // 设置订单类型
        addOrderDTO.setType(orderType);
        // 根据订单类型,获取对应的【添加订单的manager】
        return addManagerMap.get(orderType);
    }

4、策略的使用

通常我们事先并不知道会使用哪个策略去下单,在程序运行时根据商品类型、业务场景等入参等来决定到底使用哪种策略。例如,普通商品会走一口价下单,拍卖商品走拍卖订单下单等,我们会根据判断决定使用哪种下单方式。使用策略模式的代码实现如下:

@Service
public class OrderAddManagerClient {

    @Autowired
    private OrderAddFactory orderAddFactory;
     /**
     * 添加订单
     *
     * @param addOrderDTO 要购买的订单信息
     * @return 返回的是支付id
     */

    public <T> addOrder(AddOrderDTO addOrderDTO) {
        OrderAddManager orderAddManager = orderAddFactory.getOrderAddManager(addOrderDTO);
        if (null == orderAddManager) {
            throw new OrderException("该订单类型没有匹配的流程");
        }
        return orderAddManager.addOrder(addOrderDTO);
    }
}

5、小结

综上代码中,下单接口类只负责业务策略的定义,每个策略的具体实现单独放在实现类中,OrderAddFactory 只负责获取具体实现类,而接口实现类则负责业务逻辑的编排。策略类图如下,这些实现用到了面向接口而非实现编程,满足了职责单一、开闭原则,从而达到了功能上的高内聚低耦合、提高了可维护性、扩展性以及代码的可读性。

单一职责原则:一个类只有1个发生变化的原因开闭原则:对扩展开放,对修改关闭

图片


下单流程的封装

通过策略模式开发多个实现类来执行下单的情况,满足了多维度组合的业务场景。我们思考执行这些实现类在流程上是否有再次抽象和封装的地方、以减少研发工作量和尽量的实现通用流程。我们经过观察和抽象,发现每种下单的流程中,都会有三个流程:校验、业务逻辑执行、数据持久化;于是再次抽象,可以将流程分为数据准备(prepare)——>校验(check)——>业务逻辑执行(action)——>数据持久化(save)——>后续处理(after)这五个阶段;然后通过一个模板方法将五个阶段串联在一起、形成一个有顺序的执行逻辑。这样一来整个下单流程的执行逻辑就更加清晰和简单了、可维护性上也得到一定的提升。步骤执行如下代码所示。

public abstract class AbstractOrderAddManager<Timplements OrderAddManager {
    @Override
    public <T> addOrder(AddOrderDTO addOrderDTO) {
      AddOrderBaseDTO addOrderBaseDTO = convert(addOrderDTO);  //context
      prepare(addOrderBaseDTO);
      check(addOrderBaseDTO);
      action(addOrderBaseDTO);
      save(addOrderBaseDTO);
      after(addOrderBaseDTO);
    }
}

1、数据准备

上面提到了数据准备(prepare),我们都知道下单流程的调用其实都少不了一些业务数据,创建订单需要先获取涉及的产品信息,然后获取该产品的优惠信息、店铺类型、地址、用户账号等级会员权益等信息。对于数据准备怎么解耦呢,既要从复杂的业务流程中独立出来、同时又需要把不同结果解析简单化,使整个获取逻辑更具有可扩展性和可维护性。

其实做法也比较简单,只需要抽象一个数据同步接口asynBase,把多种业务数据获取的任务拆开、形成多个单一任务的实现类。下单流程在执行prepare时只需要通过扩展点方法加载这些任务列表,再调用invoke方法就能执行多个数据获取任务就可以了。将数据准备prepare进行封装之后,发现要加入一个新的数据获取任务就十分简单,只需要写一个新的AsynBase实现类加入扩展列表就行。


public interface AsynBase<TRS{

    Supplier<R> invoke(T t);  //具体执行的动作
    analysisResult(R r)//对结果进行解析
    boolean sync();   //是否同步执行
    String getName();
    int order();
    String group();
}

数据准备方法,通过模板方法getQueryAddOrderBaseAsynList获取数据准备任务列表,调用执行器获取resultMap。

//1、Prepare
private void prepare(AddOrderBaseDTO addOrderBaseDTO){
  List<QueryAddOrderBaseAsyn> queryAddOrderBaseAsynsList = getQueryAddOrderBaseAsynList();
  WorkerInvoke workerInvoke = new WorkerInvoke(queryAddOrderBaseAsynsList, getTaskExecutor());
  Map<String, Object> returnMap = workerInvoke.invoke(addOrderBaseDTO.getParam);
  addOrderBaseDTO.addPrepareResult(returnMap);
}

如果多个数据获取任务串行执行性能肯定比较差,此时很简单的可以想到使用并行执行,是的、我们使用多线程并行执行多个获取任务能显著提高执行效率。但是也应该意识到,有些任务逻辑可能是有前后依赖的,还有些业务流程中要求某些任务的执行必须有前后顺序,支持同步和异步任务,此处就可以用sync+order+Future实现了。

public class WorkerInvoke {

    private static final String FUTURE_TIME_OUT = "future_time_out";
    private static Integer timeOut = TimeConstant.INT_MS_UNIT_SECONDS_2;

    private List<? extends AsynBase> baseList;
    private Executor executor;

    public WorkerInvoke(List<? extends AsynBase> baseList, Executor executor) {
        this.baseList = baseList;
        this.executor = executor;
    }

    public Map<String, Object> invoke(Object context) {
        Map<AsynBase, CompletableFuture> futureMap = new LinkedHashMap<>();
        baseList.stream().sorted((a1, a2) -> {
            if (Objects.equals(a1, a2)) {
                return 0;
            }
            return a1.order() > a2.order() ? 1 : -1;
        }).forEach(t -> {
            Supplier invoke = t.invoke(context);
            if (!t.sync()) {
                futureMap.put(t, CompletableFuture.supplyAsync(invoke, executor));
            } else {
                futureMap.put(t, CompletableFuture.completedFuture(invoke.get()));
            }
        });
        if (MapUtils.isEmpty(futureMap)) {
            return null;
        }
        Map<String, Object> reMap = new HashMap<>();
        for (Map.Entry<AsynBase, CompletableFuture> futureEntry : futureMap.entrySet()) {
            Object completableResult = FutureHelper.futureGet(futureEntry.getKey().getName(), 
              () ->futureEntry.getValue().get(timeOut , TimeUnit.MILLISECONDS));        reMap.put(futureEntry.getKey().group(),futureEntry.getKey().analysisResult(completableResult));
        }
        return reMap;
    }
}

图片


2、上下文

从上面addOrder方法里的代码可以发现,整个过程的几个方法都是使用上下文AddOrderBaseDTO对象串联的。AddOrderBaseDTO对象中有3种类型数据,(1)入参带过来的信息、(2)数据准备阶段异步请求到的数据、(3)中间处理得到的结果。一般要将数据在多个方法中进行传递有两种方案:一种是包装使用ThreadLocal,每个方法都可以对当前ThreadLocal进行赋值和取值;另一种是使用一个上下文Context对象做为每个方法的入参传递。两种方案都有一些优缺点,使用ThreadLocal其实是一种"隐式调用",虽然可以在"随处"进行调用,但是对使用方其实不明显的、在中间件中会大量使用、在开发业务代码中是需要尽量避免;而使用Context做为参数在方法中进行传递,可以有效的减少"不可知"的问题。下面代码是AddOrderBaseDTO的定义:

@Data
public class AddOrderBaseDTO {

    /* 商品sku信息 */
    private AddSkuItemWrapDTO addSkuItemWrapDTO;
    /* 买家用户信息 */
    private User buyerUser;
    /* 用户优惠券信息 */
    private UserCoupon userCouponDO;
    private UserAddressDTO userAddressDTO;
    private ShipExpenseDTO shipExpenseDTO;
    ......
 }

3、校验 校验(check),我们都知道任何一个接口的调用其实都少不了一些校验规则,这里我们抽象出了一个OrderAddCheck接口,将校验逻辑和校验规则简单化,使整个校验逻辑更具有可扩展性和可维护性。

public interface OrderAddCheck {
    /* 对添加订单的基础信息进行验证 */
    void checkBase(AddOrderBaseDTO addOrderBaseDTO);
}

图片


要加入一个新的校验逻辑十分简单,只需要写一个新的OrderAddCheck实现类。通过模板方法getOrderAddCheckList加载到校验流程里,对其他代码基本没有改动。规则的校验顺序通过加入列表的先后顺序控制。

//通过加入checkList的先后来决定执行顺序
@Override
protected List<OrderAddCheck> getOrderAddCheckList() {
  List<OrderAddCheck> orderAddCheckList = new ArrayList<>();
  orderAddCheckList.add(addressOrderAddCheck);
  orderAddCheckList.add(buyerUserOrderAddCheck);
  orderAddCheckList.add(riskOrderAddCheck);
  ......
  return orderAddCheckList;
}

执行校验流程

private void check(AddOrderBaseDTO addOrderBaseDTO){
  List<OrderAddCheck> orderAddCheckList = getOrderAddCheckList();
  if (CollectionUtils.isNotEmpty(orderAddCheckList)) {
    for (OrderAddCheck orderAddCheck : orderAddCheckList) {
      orderAddCheck.checkBase(addOrderBaseDTO);
    }
  }
}

横向解决逻辑复用和业务扩展

实现多种订单类型的代码分离治理、以及标准下单流程模板化后,其实在真正编码的时候会发现不同订单类型不同维度的下单处理过程,有时多个处理逻辑中的一部分流程一样的或者是相似的,比如下单时不管是拍卖订单还是一口价方式,其中核销优惠券的处理逻辑、调用支付的逻辑等都是一样的;甚至有些时候多个类型间的处理逻辑大部分是相同的而差异是小部分。

对于上面这种情况、其实就是要实现在纵向解决业务隔离和流程编排的基础上,需要支持小部分逻辑或代码段的复用、或者大部分流程的复用,减少重复建设和开发。

基于代码继承方式的复用

当发现新增一个下单业务处理流程,和当前已存在的一个大部分逻辑是相同的,先实现一个默认的下单处理器,把所有的标准处理流程和可扩展点进行封装实现、其他处理器进行继承、覆写、替换就好。

AbstractOrderAddManager的addOrder方法定义了标准的下单流程,我们展开action方法看到服务、券、运费、扣点、分账和扣资源等子逻辑。其他类继承它后,就具备下单基本流程的能力。我们对调用支付、资源扣减动作等一样逻辑的提供了统一的执行代码,减少重复建设。

public abstract class AbstractOrderAddManager<Timplements OrderAddManager {
    @Override
    public <T> addOrder(AddOrderDTO addOrderDTO) {
      prepare();
      check();
      //3、Action
      //以下代码做了删减和处理,方便表述
      //3.1 支持的服务 
      List<BizSupportService> bizSupportServiceList = getBizSupportServiceList();
      //3.2 计算优惠
      List<DiscountsService> discountsServiceList = getDiscountsServiceList(); 
      //3.3 运费
      ShipExpenseService expenseService = getShipExpenseService();
      //3.4 计算扣点
      pointFee pointFee = pointFeeService.getPointFee(addOrderBaseDTO);
    //3.5 价格处理
      Price priceDTO = calculatePrice(addOrderBaseDTO, discountsDTOList);
      //3.6 分账计算
      OrderAccount accountDTO = calculateAccount(addOrderBaseDTO, itemOrderDTO);   
      //3.7 地址
      Address address = getAddressService().getAddress(addOrderBaseDTO);
      //3.8 子类扩展处理
      ItemOrderDTO itemOrderDTOSub = subHandle(itemOrderDTO, addOrderBaseDTO);
      //3.9 调用支付
      SendPayDTO sendPayDTO = sendPay(itemOrderDTOSub, addOrderBaseDTO);
    //3.10 扣除资源
      List<ResourceDecService> resourceDecServiceList = getResourceDecServiceList();
      // 执行扣减
      if (CollectionUtils.isNotEmpty(resourceDecServiceList)) {
            for (ResourceDecService resourceDecService : resourceDecServiceList) {
                resourceDecService.decResource(itemOrderDTOSub, addOrderBaseDTO);
            }
        }
      ......
      save();
      after();
    }
  
   /*获取包含服务*/
    protected List<BizSupportService> getBizSupportServiceList(){
        return null;//默认无
    }
  protected abstract List<DiscountsService> getDiscountsServiceList();//获取优惠
  protected abstract ShipExpenseService getShipExpenseService();//获取运费
  //可以让子类来覆盖该类,决定是否走支付流程
  protected SendPayDTO sendPay(ItemOrderDTO itemOrderDTO, AddOrderBaseDTO addOrderBaseDTO) {
        return orderPayService.sendPay(itemOrderDTO, addOrderBaseDTO);
    }
}


图片


业务扩展

上面代码中有收货地址、券、运费等抽象方法,这些都是业务扩展点,继承类要定义这些方法的扩展服务返回、涵盖差异逻辑、满足部分定制的下单逻辑,达到横向业务扩展的目的。

一口价下单流程继承抽象模板类,对扩展点提供合适的扩展服务,下单流程会加载并执行这些扩展服务,实现差异化的下单流程。比如实物商品是有收货地址,对于虚拟商品没有收货地址,子类只要在getOrderAddressManager扩展方法返回合适的manager就能满足业务需求。

@Service("negotiatedItemOrderAddManager")
public class NegotiatedItemOrderAddManagerImpl extends AbstractOrderAddManager<String{

    @Override
    protected ShipExpenseManager getOrderShipExpenseManager() {
        return commonShipExpenseManager;
    }
    @Override
    protected OrderPriceManager getOrderPriceManager() {
        return salePriceOrderPriceManager;
    }
    @Override
    protected OrderAddressManager getOrderAddressManager() {
        return orderAddressManager;
    }
    ......
    
    @Override
    protected List<OrderServerManager> getOrderServerManagerList() {
        List<OrderServerManager> list = new ArrayList<>();
        list.add(platformIdentOrderServerManager);
        list.add(sourcePlatformIdentOrderServerManager);
        list.add(tpOrderServerManager);
        return list;
    }
}

本文技术支持:基础平台交易组,希望通过本篇文章的探讨,对多场景、多业务维度的复杂业务场景实现,能更加容易上手。写出更好、更优雅的代码!


修改于2022年04月29日
继续滑动看下一个
玩物少年们
向上滑动看下一个