对领域模型和上下文边界分析来划分微服务的再思考


在我前面关于中台和微服务架构规划方面的文章中,实际已经谈到基于企业架构规划并结合CRUD分析的思路来进行微服务模块的识别和拆分。很多人会认为这是一个偏传统的结构化的分析建模思想,而不是面向对象分析的思路。

而今天当重新再回顾领域建模,子领域拆分和上下文边界划分的时候,再次看到实际前面谈到的企业架构和CRUD方法是有效的。在领域模型分析中,经常会用到的就是通过子领域的划分,通过上下文界限的划分来拆分微服务。

因此今天还是重新对这个方法进行回顾和思考。

领域模型和上下文边界

1.png

在领域模型和领域驱动设计下首先要理解领域的概念,简单来说领域可以理解我们传统软件需求分析中的业务场景对应的业务域,每一个领域又可以分为问题域和解决方案域。

领域驱动设计本身就是要完成从问题域到解决方案域的映射和抽象。而这个映射和抽象可以理解为领域分析的一个关键内容。
2.png

领域本身分为了领域和子领域,而这些都属于问题域。而上下文边界划分则可以理解为解决方案域的一个关键内容。在领域驱动设计中首先要完成的就是上下文边界的划分,这本身符合我们传统软件分析设计方法中的子系统或组件划分思路。

限界上下文定义了领域模型的边界,应该在团队组织、应用中特定部分的使用、代码库和数据库模式等物理表达等方面显式地设定边界。限界上下文的目的就是理清子域,然后区分这些子域那些是核心域、支撑子域和通用子域。一个领域模型涵盖了核心域、子域和限界上下文,其中核心域、子域也可以表达为一个子领域模型,这样一层层嵌套下去。

微服务和界限上下文

在前面已经谈到,可以将界限上下文对应到具体的微服务,微服务拆分的最小粒度就是界限上下文,但是多个界限上下文可以合并到一个微服务。

那么界限上下文划分完成后,每个上下文边界里面应该包括哪些核心内容?简单来说至少应该包括三个方面的内容,即:
  • 功能:哪些业务功能要划分到该边界里面
  • 对象:领域对象划分,每个对象的Owner是哪个
  • 接口:对外暴露的接口能力


当这些都基本定下来后,可以看到单个界限上下文是可以分配到独立的开发团队来完成的。即每个界限上下文从代码库,编译构建,数据库,开发,测试,部署,包括后续的运维都可以做到完全的独立。这个和单体到微服务拆分思想是一致的。

通过事件风暴来划分上下文

3.png

事件风暴是一项团队活动,领域专家与项目团队通过头脑风暴的形式,罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对每一个事件,标注出导致该事件的命令,再为每一个事件标注出命令发起方的角色。

命令可以是用户发起,也可以是第三方系统调用或者定时器触发等,最后对事件进行分类,整理出实体、聚合、聚合根以及限界上下文。

在整个事件风暴方法中,有三个关键要素如下:
  • 事件 -> 某个动作的结果
  • 命令 -> 某个动作
  • 实体 -> 命令的触发者


而整个分析流程可以简单总结为:

首先是基于业务分析来识别关于的事件,同时识别和梳理出事件对应的命令,在事件和命令中都会附带有对应的领域对象和实体,因此进一步基于对象实体进行命令和事件的聚合。在完成聚合后再进一步梳理和划分上下文界限。

网上有篇文章举了一个完整的例子大家可以参考:https://www.jianshu.com/p/96801c93a47d

其中关键的三个步骤如下:

识别事件和命令

事件你可以理解为一个事物所编写出来的最终状态,例如订单已创建,支付已完成,商品已发货等即是关键的事件。而命令可以理解为具体的业务功能或操作,比如创建订单,检索商品,扣减库存等。

可以看到事件和命令的识别和分析中,都可以识别到具体的领域对象。
4.png

基于领域对象进行聚合

如何进行聚合?简单来说仍然是通过识别和梳理出来的共性领域对象进行聚合。将对同一领域对象的所有命令和事件都聚合在一期。
5.png

进行上下文边界的划分

基于聚合完成的情况进行上下文边界的划分,这里实际上不同的领域对象如果属于同一个大类的业务场景,仍然是可以划分到一个上下文里面的。

也就是不是简单的按聚合完成的领域对象划分上下文边界,很多和核心领域对象相关的附属对象也需要划分到同一个上下文的。

比如电商里面的订单是一个大量的领域对象,可以划分为独立的上下文,但是对应购物车也是我们识别的对象,购物车本身同样属于产品订购场景,订单的扩展附属对象,因此需要将购物车也划分到订单上下文里面。
6.png

对事件风暴方法的思考

7.png

前面介绍了事件风暴方法,我简单说下我的结论,即对于全新的业务领域,或者架构人员原来没有接触过的领域,采用这种方法来进行上下文和边界划分完全行不通。特别是全新业务领域,本身整个业务流程,业务场景,业务模型又很复杂,关联依赖又很多的情况下更是很难采用。

具体我想分析以下几个点:

对非熟悉领域,事件风暴方法第一步就去识别事件的方法是是不科学的。事件本身是实体的状态,要识别事件首先就是要识别领域对象实体,而要识别实体本身又要要进行业务流程分析,通过业务流程分解到业务活动,通过业务活动识别业务操作命令,再来分析具体的实体对象。

其次,该方法识别的事件,命令很可能不完整,无法覆盖完整的业务场景和流程。基于上面方法会导致一个错觉,即事件识别出来后简单的从事件来识别命令,这很容易到命令事件一对一的关系。举例来说订单已取消是一个事件,但是对应的命令不是简单的取消订单。取消订单有很多场景出现,比如用户自己取消订单,管理员取消订单,超时未支付系统自动取消订单。这些都会触发订单已取消的事件。

第三按领域对象进聚合比较容易,但是聚合完成后不是每个领域对象就是一个上下文,即哪些领域对象应该整合到一起,划分到一个大的上下文里面。实际上在当前事件风暴公布的方法里面对这块并没有明确的描述。

比如用户和会员是两个领域对象实体,那么用户和会员这两个聚合点是否应该划分到一个上下文边界里面,还是说分为两个上下文边界?具体的判断方法和准则是如何的?实际上对于事件风暴分析方法是无法给出明确思路的。而在传统方法里面我们往往是通过CRUD方法进行详细的耦合性和依赖分析,最终才能够确认是拆分还是合并。

围绕核心领域展开

对于领域模型中的领域可以分为核心域,支撑域和通用域,而对于支撑域和通用域都可以理解为对核心域业务流程执行的关键支撑。

也就是说你将核心域关于业务梳理清楚,那么支撑域自然也就出来了。

比如对于资产管理系统,核心域就是资产,所有业务全部围绕资产的全生命周期管理展开。而回到一个电商平台,核心域即订单,要给电商就是用户在网上下单,最终完成接收确认的闭环流程。

基于这个思路回到电商平台,首要的分析思路仍然应该是找到核心域,再进行核心域进行全生命周期阶段展开,这个是识别大阶段和微服务划分的关键点。往往比单纯的事件风暴后,再聚合更加有效。比如电商核心业务场景和订单生命周期如下。
8.png

当你对核心领域对象的生命周期阶段梳理清楚后,实际上已经可以明确的识别出关键的上下文边界和关键的领域对象。比如上图可以看到商品,订单,供应商,用户,库存都是关键的领域对象,相关的业务操作也围绕这些业务对象展开。

也正是这个原因,我们强调要进行领域建模或微服务拆分,首先还是要熟悉你当前面对的业务领域,将关键的业务流程和业务对象全部搞清楚,这是进行详细的领域建模或微服务拆分的基础。

一个好的最佳实践可以告诉你相同业务场景下面对相似的问题如何解决?但是一个好的方法论一定是告诉你当你面对一个全新的业务场景或未知领域的时候,如何去独立分析和解决问题。

其次,还要强调一点。

一个方法对于简单业务领域或场景有效,并不代表对复杂领域同样有效。同时对于简单领域有效的方法论你会发现,即使不用方法论的方法你同样可以很好的解决问题。

而对于事件风暴的方法,实际上没有很好的解决上面的问题,即复杂的业务场景无法解决。简单的业务场景不用事件风暴也同样可以解决。

事件驱动到命令驱动

在事件风暴里面可以看到,核心是首先识别关键事件,该方法的一个指导思想就是不论你前端的业务流程或活动如何变,事件本身相对式最稳定的。但是却忽略了对于未知领域实际很难一开始就马上识别出关键的领域事件。

但是对于事件风暴方法仍然可以有很多借鉴的地方,比如命令事件和实体三要素,比如基于实体进行的聚合操作等。

对于一个全新的业务领域,最好的方法仍然是按部就班的基于业务流程和场景分析来梳理关键的业务活动,或者说基于面向对象的用例分析设计思路来进行。从事件驱动转到命令驱动的方式来进行分析和建模。

命令驱动本质仍然是业务和用例驱动。

即在进行业务流程和场景分析后,到了最底层即前面谈到的命令,命令更加类似于传统用例分析方法里面的用例建模,也类似于Scrum敏捷方法论里面的用户故事。

但是这个粒度可能比命令粒度大,那么可以进一步拆分。
  • 用例到用例点的展开
  • 从用户故事到具体业务操作的展开


最终我们形成的是所有业务操作或命令的合集,比如提交订单,取消订单,评价商品,查看物流状态,确认收到商品等。

业务操作点最好是如上的标准动宾结构:动词(业务操作)+ 宾语(实体对象)

从这个可以明确的看到一个业务命令实际包括了业务功能操作和业务对象实体两个方面的内容,而在前面进行上下文和微服务划分的时候就强调,最终拆分的微服务必须要进行边界确认的就是两个关键点。

其一是业务功能是划分边界,其二是数据库表的Owner边界。一个经过拆分后的命令实际上同时涉及到两个方面的内容,但是并不代表业务功能和实体都属于同一个微服务或上下文。

我们来举例说明下:

比如原来的用例是用户提交订单,这个是要给独立的用例,经过分析还需要进一步拆分到具体的业务操作或关键用例点,那么就对该用户进一步拆分。

提交订单-》(01新增订单, 02冻结库存)

对于冻结库存是一个细化后的命令,但是该业务功能操作在订单中心完成,但是实际操作的实体对象则属于库存中心。即业务功能操作和业务对象本身是分离的。

经过分析后可以得到命令和中心间的引用依赖图,如下:
9.png

在我构建这个图的时候,完整的方法论思路还不足够体系,但是该图至少解决了两个关键的问题点,其一是命令包括的两个部分业务功能操作和实体对象分离,更加容易进行微服务划分,比如前面谈到的冻结库存,业务功能在订单微服务,但是库存对象在库存微服务。其二是基于业务功能场景的业务操作和底层的数据或业务服务能力分离,即更加容易去按SOA服务分层的方式去思考如果构建对外共性可复用能力。

当上面这个图思考清楚后,实际你可以看到多个微服务之间的关键接口交互,或者说某个微服务究竟应该暴露哪些能力接口出来也清楚了。

而简单的基于前面谈到的事件风暴方法并不有利于我们梳理微服务间的交互和集成关系,进而进一步的识别和定位API接口服务。

再对上面谈的思路和方法进行总结,就是:

首先基于你面对的业务场景分析核心域和核心领域对象的生命周期阶段,这个已经足够划分出关键的上下文和微服务边界,其次基于业务拆分细化分析详细的命令或叫业务功能操作,确定每一个业务功能操作的边界和数据交互边界。

通过以上分析,我们基本完成微服务架构阶段三个关键输出。
  • 具体包含哪些业务功能实现
  • 该微服务为Owner的数据库表
  • 该微服务需要暴露哪些API接口能力


以上三方面梳理清楚,基本就可以转到独立开发团队承接该微服务。

从用例到操作点的细化分析

当我们在谈用例或命令的时候可以看到,该用例是有明确业务含义并能够被用户所理解的一个概念,比如提交订单,付款订单等。

但是任何一个用例的实现实际上又有一个细化的流程,在软件需求用例建模的时候可以看到一个完整的用例分析包括了业务基本流,扩展流,业务规则。当分析到具体的扩展流和业务规则的时候,你会发现更多细粒度的命令点或业务操作点。

在早期我们进行SOA架构规划咨询项目的时候,往往就会基于业务流程的思路对详细的用例和用例操作点进行分析,在分析的过程中你就会发现一个订单系统中的业务用例点往往会出现会其他领域的领域对象之间的关联和引用依赖关系。
10.png

如果按照前面谈到命令应该基于业务实现逻辑进行进一步的子命令的拆分,那么整体的构建逻辑就如下图的方式进行展开。
11.png

也就是说如果仅仅是简单的分析到用例层级,那么很多内部的业务规则,扩展流,外部依赖点是无法发现的。因此用例应该进一步展开到上图方式的用例点,对于每个用例点仍然可以保留动宾结构来进行定义和描述。

从领域对象到数据建模

在领域建模和分析思路里面是领域对象,偏面向对象的分析思路。而在传统的结构化分析方法是构建概念模型,逻辑模型和物理模型。

一个领域对象往往涉及到多个数据对象,一个数据对象本身又涉及到多张数据库表。

比如对于用户这个领域对象,实际上涉及到用户基本信息,用户地址信息,用户银行账号信息,用户会员等级信息,用户记录变更历史信息等,所有的信息都围绕用户基本信息展开。

以上所有扩展信息表现出一个关键点。

即当用户基本信息不存在,或该用户消亡后,所有的扩展信息都不再存在。这和我们在进行领域建模的时候强调的实体对象和值对象差异分析的思路是一致的。

对于商品信息也是同样的道理,比如商品包括了商品基本信息,详细规格参数信息,商品评论信息,商品可选规格型号SKU扩展信息等。这些信息同样是围绕商品主信息而存在,具备同样的数据生命周期。

比如对于上面场景进行初步数据架构分析如下:
12.png

在完成的数据架构分析你会看到存在聚合根,可以基于聚合根节点进行拆分。这个也是我们谈的数据库拆分的基础。

而数据聚合根要给最大的特点就是将数据模型中的数据对象按类似上下文边界的方法分域后,你会看到各个域之间是很明确的单线连接关系。即按单线连接处剪开,那么形成多片完整的树叶结构。每片树叶仍然是一个完整的子树结构。

所以不管是领域建模还是传统结构化分析思路都可以看到,最终还是殊途同归,核心不是简单的拆分,而是在拆分后的聚合,同时对于聚合后的内容划域。

以上是个人对微服务拆分的一些点滴思考,还不完善供参考。

原文链接:https://www.toutiao.com/i6904810522470842887/

0 个评论

要回复文章请先登录注册