0、引言

在前面四篇文章中,我们把物流中台的基础能力层构建了起来,接下来,我们就可以在这些基础能力之上构建我们的产品服务,从而支撑各条业务线。基础能力层主要关注的是稳定可用的原子接口,因此在设计的时候重点关注了很多高并发高可用的技术。产品服务层主要是为了支撑不断创新的业务,因此在设计的时候需要关注业务流程的可扩展性。下面我们会先从整体上设计出一套抽象模型,然后针对模型的各个模块深入介绍。

1、整体设计

写过业务代码的同学一定知道,我们的系统往往是由service层、manager层和远程服务层组成。这三层中,service层定义了对外的接口,manager层处理各种业务逻辑,主要是数据校验、数据组装、调用其他应用的(dubbo服务,远程服务层是对dubbo服务的包装,比较简单。当业务不断膨胀的时候,我们可能会遇到下面的情况:

1、service层的接口越来越多,相似的业务定义多套接口

2、manager代码越来越臃肿,各种if else嵌套,每次改一点都需要测试回归整块业务

3、远程服务,特别是查询服务,在一条业务流程中的多个地方被重复调用,从而导致整体响应时间下降

对于第一个问题,我们需要仔细分析业务,然后抽象出通用的接口,比如创建物流单,业务上既可以创建快递订单,也可以创建仓储订单,我们需要定义一个通用的下单接口,而不是针对快递和仓储分别设计两套订单接口。然后,我们在入参和出参上进行可扩展的设计,比如使用继承、组合的方式,在保证入参模型基本不变的情况下可以支撑不同的业务,这样一来,新业务接入的时候,就不需要更改接口,只需要扩展入参对象就行。

对于第二个问题,我们把manager的业务逻辑抽象为三个步骤:校验、数据转换、执行。快递有快递的校验、数据转换、执行,仓储有仓储的校验、数据转换、执行,service调用manager的时通过策略模式路由。这样一来,各个业务都是不同的代码流程,不存在修改一点会影响全局,测试只需要关注被修改的流程,新增的业务线只需要添加一条业务流程然后配置一下路由就可以了,也不会影响老业务。

针对第三个问题,我们可以规定把所有的数据查询都放在manager的数据转换步骤,然后通过流程上下文去传递数据,这样就不会把查询服务扩散到整个流程了。总结下上面的三个问题的解决方案,我们可以抽象出下面的业务流程模型:

2、接口层设计

产品服务层的接口服务遵循request-response风格,每个方法的入参都是一个request,出参都是response。request和response通过继承和组合进行扩展。

2.1、通过继承扩展

request往往可以通过继承进行不同业务的扩展。以物流发货来说,发货方式有快递发货和仓储发货之分,这两种发货方式的入参不太一样,这时候我们可以把公共的参数抽出来,然后不同的参数下沉。我们会定义一个LogisticsServiceDTO对象,快递和仓储分别继承它,得到ExpressLogisticsServiceDTO和WarehouseLogisticsServiceDTO,然后各自定义参数,最后我们就可以得到如下图所示的一个request对象:

以后如果还有其他新的发货方式出现,我们只需要定义新的LogisticsServiceDTO就可以了,接口的基本结构几乎不需要改变。

2.2、通过组合扩展

response往往可以通过组合来扩展不同的业务,比如用户物流单详情页,需要展示物流单信息、用户信息和地址信息,我们可以定义一个OrderDetailResponse,这个对象组合了LogisticsOrderDTO、UserDTO、AddressDTO等对象。通过组合进行扩展的方式比较常见,在此就不展开讲了。

2.3、如何定义方法

根据我们的经验,定义方法往往需要结合业务的生命周期,拿物流接口LogisticsService来举例,我们可以定义createLogistics、consignLogistics、logisticsCallback这三个方法。

a)、createLogistics

createLogistics是创建物流订单方法,通过定义可扩展的request对象,可以支持快递、仓储、配送等等物流订单的创建

b)、consignLogistics

consignLogistics是发货方法,通过定义可扩展的request对象,可以支持快递、仓储、配送等等物流订单的发货

c)、logisticsCallback

logisticsCallback是物流回传方法,通过定义可扩展的request对象,可以支持仓接单、仓分拣、仓出库、配送揽收、配送派送、配送签收等等物流回传过程

三个方法基本上涵盖了绝大部分业务场景,如果出现新的业务场景,首先需要评估现有的方法是否支持业务流程,如果不支持,再新增方法,比如有些物流场景存在多段发货(典型的就是农村淘宝),这时候一个consignLogistics肯定是不够的,这时候就需要扩展方法。在扩展方法的时候,一开始可以就为特殊的业务开特殊的接口,等后面业务越来越多后,将这些特殊接口统一整理一次整理成一个通用的接口,这个过程就是一个沉淀业务模式的过程,是无法通过提前设计设计出来的,必须经过业务的不断沉淀才能跑出来。

3、接口实现层

设计说完了接口层的设计,接下来就要说说接口的实现层怎么设计。按照整体设计的思路,我们把业务切分成一条一条的流程,然后把每条流程切分成校验、数据转换、执行三个节点,然后通过入参来决策走哪条流程。因此,一个接口的实现层其实就是一个流程决策+流程节点执行的过程。

流程决策其实就是在一张映射表里获得需要执行的节点列表,这里获取的方法可以是普通的哈希映射,也可以通过复杂的规则引擎获取。拿到节点列表(一般是节点类名)后,就可以通过spring容器获取节点对象。所有的流程节点,都会继承同一个抽象节点对象,然后实现同一个方法:execute方法,因此,通过spring容器获取节点对象后,就可以通过反射执行节点。节点与节点之间往往需要传递数据,因此节点的execute方法需要传入一个context对象,A节点在执行后将数据传入context对象,B节点执行的时候通过这个context对象获取A的数据,这就打通了节点之间的数据通道。

将上面的抽象模型带入具体的业务,我们可以得到下面的架构:

以创建物流单为例,所有的业务代码都按照业务分散在不同的流程节点里,入口处通过策略路由选择流程,同一个流程规定了三个节点,三个节点之间通过流程上下文传递数据,三个节点通力合作完成不同业务的物流单的创建过程。

如果从更宏观的视角来看,产品服务层和基础能力层组合起来就是下面的架构:

产品服务层对外提供service,每个service都定义了高度聚合的业务方法,每个业务方法通过策略路由和流程编排,把业务代码进行隔离,业务流程中的每个节点,调用基础能力层的原子接口进行一系列业务操作。

4、总结

产品服务平台总的思路其实就是一条非常古老的原则:对扩展开放、对修改关闭。我们通过继承和组合,让接口的出入参变得可扩展,通过策略模式和流程节点的组合,让新老业务代码隔离,进而提升了扩展性。当然,凡是有利必有弊,产品服务平台在提升了可扩展性的同时,增加了一定的代码编写量,同时对分布式事务没有太好的办法。