业务中台构建-服务识别

SOA团队 2020-03-16

对于中台构建,实际上两个关键点,第一个是划分微服务模块粒度,第二个就是在模块划分清楚后确定服务识别和定义的粒度。在服务识别和定义前,可以先参考本文档【3.2.2.2 Http Rest接口设计】这篇文章。

在上篇谈中台构建模块划分的时候,强调了一个关键点,就是尽可能以数据的维度来进行模块拆分,数据包括了基础主数据和核心共享数据,在数据驱动下拆分模块,那么模块底层对应的数据库如何拆分基本也就清楚了。一定要知道,微服务架构下,我们底层数据库也是拆分了的。

底层数据库没有拆分,但是仍然用SpringCloud框架开发,可以拆分为多个JAR包,在这种模式下只能认为是一个微服务模块,而不是独立,因为其不存在独立自治能力。我们看到很多上层开发采用SpringCloud框架,但是数据库仍然采用一个数据库的情况,再次说明,对于这种架构设计,不是标准意义上的微服务架构,没有做到彻底解耦。

面向资源,面向对象和领域驱动设计

对于Http Rest接口说的最多的就是面向资源的设计,对于资源有标准的Http Put,Post,Delte,Get等操作。因此你需要定义好相应的资源。资源可以放在计算机上并体现为比特流的事物,可以是结构化的数据或对象集合,也可以是图片或文件流,这些都是可以处理和操作的资源。

面向资源和领域驱动本身不是一种新的软件工程设计方法,真正的方法只有传统的面向结构设计和面向对象设计两种。因此对于面向资源可以按面向传统结构化设计,也可以按面向对象设计。当然最好的方式仍然是面向对象进行设计。

资源即实体,实体即对象,这个对象代表的是业务对象,有明确的业务含义,类似供应商,采购订单,产品,合同等。同时这些对象本身存在关联和递进的层次结构,比如供应商有对应的联系人,有对应的银行账号。产品可能有对应的维修记录等。

而这些业务对象正是我们在领域驱动设计时候经常会识别的领域对象,这个领域对象涉及到多个子类,是否归结到一个大的领域对象最关键的还是是否共属一个生命周期。面向资源设计,完全可以采用面向领域设计方法,首先定义领域对象,将领域对象建模为对应的资源,然后再考虑看这个资源应该暴露哪些能力接口出来。

所以在微服务架构下,首先要了解清楚面向资源进行Rest接口能力设计,资源的识别和定义可以参考领域设计的思路进行,识别和定义领域对象,然后再转为资源定义。

接口服务的粗粒度是关键

如果我们不按领域对象方式来定义资源,那么我们最容易犯的错误就是将所有的数据库表对象都全部定义为一个个独立的资源,将这些资源的CRUD操作,全部暴露为Get,Put,Post和Delete接口方法。那么这样暴露出来的Http Rest接口方法就全部是细粒度的接口。

这种方法很省事,一个模块有100张表,你只需要暴露100个接口,每个接口都含标准的上述操作就完事了。但是这种接口服务识别和定义没有任何意义,也不符合我们粗粒度的要求。

那么问题的关键点在哪里?其关键就是原来应该是粗粒度的体现业务价值的接口服务全部都变成了细粒度的DAO访问类细粒度接口服务。失去了接口的意义,同时又将本身应该完全内聚在微服务模块内部的业务逻辑全部暴露到外层去解决。这个有点类似我们在进行领域驱动设计的时候,经常谈到的贫血的领域服务层,即我们的Http Rest接口应该是粗粒度的,应该是满血的领域服务层的能力暴露,而不是底层数据库CRUD操作的暴露。

通过识别的资源来识别和定义接口

只要确定了资源,那么我们就很容易来确定资源应该提供哪些接口。

在我们进行接口设计的时候,如果一个资源完全不需要和外部微服务模块或外部应用打交道,那么这个资源完全不用开放任何接口。这个是我一直强调的原则,即在微服务模块内部最好是走传统API接口交付方法进行调用,而不是走Http Rest接口服务,这一方面是提升性能,一方面是减少各类难以应对的分布式事务问题。

在资源定义清楚后,往往资源都是一个复合对象,比如采购订单资源,涉及到采购订单头或采购订单明显信息,之间还存在关联,但是这是一个完整的资源对象。对于采购订单对象,根据业务场景,存在外部导入新的采购订单,存在外部对已有的采购订单进行变更后导入,存在外部需要查询采购订单集合,同时查看某一个特定key值的采购订单的详细明细数据。这可能是我们经常会遇到的接口需求场景,从这些场景可以看到,我们的设计完全可以基于采购订单资源展开。

创建新的采购订单:POST /Orders

修改一张ID为1的已有订单:PATCH /Orders/1

删除ID为1的已有订单:DELETE /Orders/1

查询所有采购订单:GET /Orders

查询ID为1的采购订单:GET /Orders/1

查询1月到5月的订单:GET /Orders?StartData='201801'&&EndDate='201805'

可以看到,第一种方法就是上面的,可以直接在资源后面增加不同的参数条件进行模糊查询。其次,我们可以将查询条件定义为一个查询实体类,同时将整个查询实体类的信息通过一个完整的实体对象传递过去进行查询,查询完成后再返回相应的结果。比如我们定义一个OrderQueryExt实体类。

基于特定条件的模糊查询:POST /Orders/OrderQueryExt

那是否会存在只查询一张采购订单的采购明细列表信息?如果存在这种情况,应该按照资源和资源层次关系进行设计,由资源逐层展开查询。

查询ID为1的订单的所有采购明细:GET /Orders/1/OrderDetails

查询ID为1的订单的流程审批记录信息:GET /Orders/1/ProcessDetails

查询ID为1的订单的所有附件信息:GET /Orders/1/Attaches

对于PUT和PATCH而言,如果涉及的情况一般是对实体的部分数据进行更新,同时还需要支持SaveAndUpdate操作,那么我们一般都采用PATCH方式,而不是PUT方式。即实际资源接口设计的时候,单纯的PUT场景往往现在已经很少发生。

定义业务规则和逻辑处理类接口

这是我们遇到的第二大类,前面基于资源进行接口设计思路已经很明确。但是对于业务规则处理类往往比较难,比如我们经常遇到的提交报账单的时候需要进行预算校验和控制,这个就是典型的业务规则处理类,报账单提交需要,合同提交往往也需要。

那么这里的资源究竟是什么?

对于预算信息应该不是这里的资源,因为预算信息的录入和维护,才会涉及到预算信息开放接口。而这里的业务场景是对已有的预算信息进行规则计算和校验。

基于这类场景,我们看到比较好的设计方法是定义一个独立的规则类,将规则类映射为一个资源,比如这例子里面我们可以定义一个BudgetControl的规则类,这个类可以定义为一个资源对象。任何一个规则处理都涉及到有具体的输入和输出。

比如预算校验:

输入:具体的组织信息,预算科目信息,当前申请预算信息,年度或月度信息

输出:校验结果信息

你会看到对于预算校验,预算扣减,预算冻结,实际上他们的输入和输出都是相同的,那么我们可以划归到同一个规则处理类里面进行处理。那么规则类的定义,需要增加一个规则处理类型即可。

那么不论是预算校验,还是预算冻结,可以看到实际的接口调用都是:

POST /BudgetControls

当然也可以将预算检查,预算冻结等定义为预算控制类的子对象,比如预算检查Valid,那接口调用为:

POST /BudgetControls/Valid

所以对于业务规则的处理可以看到,最重要的是业务规则类的定义,业务规则类定义清楚了,业务规则类转为资源,形成对资源的Http操作接口。

对于业务规则类,一定是粗粒度服务接口,规则的处理逻辑都应该完全控制在模块内部而不是被暴露到外面去。因此对于规则类的定义也是,仅仅提供仅有的输入和输出,能够满足规则处理和计算要求即可。

返回上页