目录
一、战略设计:
1、产品愿景
2、场景分析
3、领域建模
1)提取领域对象
2)构建聚合
3)划分界限上下文
4、微服务拆分
二、战术设计
1、分析微服务领域对象
1)服务识别和设计
2)聚合内对象
3)微服务领域对象清单
2、设计微服务代码结构
三、小结
项目基本信息:某在线学习系统,学生预约公开课,预约成功后发送短信通知。
战略设计是根据用户旅程或场景分析,提取领域对象和聚合根,对实体和值对象进行聚类组合成聚合,然后划分界限上下文,建立领域模型。
战略设计采用的方法是事件风暴,包括:产品愿景、场景分析、领域建模和微服务拆分几个主要过程。
产品愿景是对软件产品进行顶层价值设计,对目标用户、核心价值、差异化竞争点等信息达成一致,避免产品设计和建设偏离方向。
本项目产品愿景:为了满足用户在线预约公开课的需求,我们建设一个在线选课系统,该系统是一个在线选课平台,可以自动统计学生的选课情况,并在学生成功选课后发出确认消息。
通过产品愿景分析,项目团队统一了产品名称,明确了项目目标和关键功能。产品原型分析对于初创系统明确系统建设重点、统一团队建设目标和建立通用语言是非常有价值的。如果系统建设目标和需求非常清晰,这一步可以忽略。
场景分析是从用户视角出发,探索业务领域中的典型场景,找出领域中需要支撑的场景分类、用例操作以及不同子域之间的依赖关系,用以支撑领域建模。
用户旅程分析和场景分析是领域建模的主要分析方法,项目团队成员一起用事件风暴的方法完成在线选课的用户旅程分析。根据不同用户角色的旅程和场景分析,尽可能全面地梳理从前端操作到后端业务逻辑产生的所有操作、命令、领域事件以及外部依赖关系等信息。
场景一:学生选课。
1)学生登录系统:验证登录信息
2)选择课程:搜索/浏览感兴趣的课程,确认选择课程。
3)消息提醒:系统自动发出选课成功邮件,提醒学生上课时间。
场景二:取消选课。
1)学生登录系统:验证登录信息
2)取消选课:打开未完成的已选课列表,取消指定课程。
场景分析结果入下图所示:
领域建模是通过对业务和问题域进行分析,找出领域对象以及它们的业务行为和依赖关系,建立领域模型的过程。有了领域模型后,你就可以向上通过界限上下文指导微服务边界设计,向下通过聚合划定微服务内的逻辑边界,指导完成微服务内的实体和值对象设计了。
领域建模是一个收敛的过程,主要分为三步:
第一步:提取领域对象。从业务操作或行为中,抽象并提取领域实体和值对象等领域对象。
第二步,构建聚合。从众多实体中找出聚合根,找出与聚合根依赖的实体、值对象等,建立聚合。
第三步,划分界限上下文。根据业务及语意边界等因素,将多个聚合划分到一个业务上下文环境中,确定领域模型的界限上下文边界。
根据场景分析,分析并找出发起或产生这些命令或领域事件的实体或值对象。将命令和事件与实体或值对象建立关联关系。通过业务行为和领域事件分析,我们提取了产生这些行为和事件的领域对象,如课程、学生选课、认证信息、系统通知等实体和值对象,如下图所示:
在定义聚合前,先找出聚合根。在前面提取的实体中,可以发现“学生选课”、“系统通知”具有聚合根的特征,如:有独立的生命周期,有全局的唯一ID,可以创建和修改其他对象,可以有独立的模块来管理它们。所以,我们将“课程”、“学生选课”和“系统通知”定义为聚合根,然后找出与聚合根紧密依赖的实体和值对象。我们发现课程、用户与学生选课紧密管理,用户与系统通知紧密关联,它们分别形成选课聚合和系统通知聚合。
经过分析,我们建立了选课、系统通知两个聚合,如下图所示:
其中,选课聚合有课程和用户等对象,系统通知聚合有系统通知和用户等对象。
找出所有的聚合后,我们可以根据业务职责和业务语义上下文边界将所有聚合划分到不同的界限上下文边界内。基于上边的分析,可以划分出选课和消息两个界限上下文,建立了选课和系统通知两个领域模型。
理论上,一个界限上下文就可以设计为一个微服务,但还需要综合考虑多种外部因素,比如职责单一性、软件包大小、团队沟通效率、技术异构等非业务要素,以及弹性伸缩、版本发布频率和安全等非功能需求。
在这个项目里,划分微服务主要考虑了职责单一原则,也就是根据界限上下文进行划分,拆分为选课和消息两个微服务。
到这里,战略设计就结束了。通过战略设计,我们建立了领域模型,确定了领域模型内部的对象和它们之间的依赖关系,划分了界限上下文和微服务的边界。下一步就是战术设计了,也就是微服务设计。我们以选课服务为例,来讲解其设计过程。
战术设计是根据领域模型完成微服务设计的过程。这个过程会梳理微服务内的领域对象,梳理领域对象之间的关系,确定它们在代码模型和分层架构中的位置,建立领域模型与微服务模型的映射关系,以及微服务之间的依赖关系。
战术设计主要包括两个阶段:分析微服务领域对象和微服务代码结构。
领域模型里有很多领域对象,但是这些对象带有比较重的业务属性。要完成从领域模型到微服务的落地,还需要进一步细化分析和设计。
在领域模型基础上,我们需要进一步细化这些领域对象以及它们之间的依赖关系,补充事件风暴过程中可能遗漏的业务和技术实现细节。所以,这个分析和设计过程会比事件风暴详细很多。
我们一般会分析以下关键内容:
命令往往是由于外部操作后产生的一些业务行为,一般也是微服务对外提供的服务能力,往往与微服务的应用服务或者领域服务对应。
我们可以将命令作为服务识别和设计的起点,服务识别和设计的具体步骤如下:
a. 由于应用服务主要面向用例,我们可以先根据命令设计应用服务,确定应用服务的基本功能,应用服务应该包含哪些服务,服务的组合和编排方式是什么样的?这些服务主要来源于领域层的领域服务或其他微服务的应用服务。
b. 根据应用服务功能要求设计领域服务,定义领域服务。这里需要注意:应用服务可能是由微服务的多个聚合的领域服务组合而成的。
c. 根据领域服务的功能,确定领域内的实体以及实体自身的业务行为。领域服务主要完成聚合内垮实体的复杂业务逻辑,要注意避免滥用领域服务,一面实体变成贫血模型。
d. 设计实体的基本属性和方法。
我们以确认选课这个动作为例,来说明服务的识别和设计过程。
确认选课的主要流程如下:
a. 验证学生是否已经选择过该课程,如果已选择,则提示不能重复选课。
b. 保存学生选课数据。
c. 向学生发出选课已成功消息。
通过分析,我们需要在应用层和领域层设计以下服务和方法。
应用层:确认选课应用服务
领域层:查询学生选课(验证是否重复选课)服务、保存学生选课服务,位于选课聚合中。
学生选课实体包含查询学生选课方法和保存学生选课方法。
下图是分析出来的服务以及它们之间的依赖关系:
另外,在服务识别时,我们还要考虑是否有领域事件发生,领域事件是否会触发下一步的业务操作,业务操作是发生在微服务内的其他聚合还是其他微服务。对于微服务之间的领域事件,一般建议优先采用领域事件驱动的异步化数据最终一致性方式,尽量避免采用同步服务调用方式,这样可以解耦领域模型和微服务。
至此,服务识别和设计过程就完成了,接下来进入聚合内领域对象的设计过程。
在选课聚合中,学生选课是聚合根。课程和学生来自其他聚合,所以可设计为值对象,操作人是不可变对象,也可以设计为值对象。
因此,选课聚合的值对象包括课程、学生、操作人。
综上所述,我们就可以画出选课聚合领域对象的依赖关系了,如下图所示:
在确定各领域对象的属性后,我们就可以设计各领域对象在代码模型中的代码对象了,如代码对象的包名、类名和方法名等,建立领域对象与代码对象的映射关系。
经过上述分析后,我们可以得到微服务的领域对象清单,如下表所示:
层 |
领域对象 |
领域类型 |
包名 |
类名 |
用 户 接 口 层 |
选课数据传输对象 |
数据传输对象 |
*.selectcourse.interfaces.dto |
SelectCourseDTO |
选课组装类 |
组装类 |
*.selectcourse.interfaces.assembler |
SelectCourseAssembler |
|
选课门面接口类 |
门面接口 |
*.selectcourse.interfaces.facade |
SelectCourseApi |
|
应 用 层 |
选课应用服务 |
应用服务 |
*.selectcourse.application.service |
SelectCourseAppService |
消息服务调用防腐层 |
防腐层 |
*.selectcourse.application.acl |
MessageServiceAcl |
|
领 域 层 |
学生选课 |
聚合根 |
*.selectcourse.domain.aggregate |
StudentCourse |
课程 |
值对象 |
*.selectcourse.domain.valobj |
Course |
|
学生 |
值对象 |
*.selectcourse.domain.valobj |
Student |
|
操作人 |
值对象 |
*.selectcourse.domain.valobj |
Operator |
|
选课已完成领域事件 |
领域事件 |
*.selectcourse.domain.event |
CourseSelectedEvent |
|
选课领域服务 |
领域服务 |
*.selectcourse.domain.service |
SelectCourseService |
|
选课聚合工厂类 |
聚合工厂 |
*.selectcourse.domain.service |
SelectCourseFactory |
|
选课领域服务实现类 |
领域服务 |
*.selectcourse.domain.service.impl |
StudentCourseServiceImpl |
|
选课仓储接口 |
仓储 |
*.selectcourse.domain.repo.repository |
StudentCourseRepository |
|
选课仓储实现类 |
仓储实现 |
*.selectcourse.domain.repo.repository.impl |
StudentCourseRepositoryImpl |
|
选课数据访问对象 |
数据访问对象 |
*.selectcourse.domain.repo.dao |
StudentCourseMapper |
|
选课持久化对象 |
持久化对象 |
*.selectcourse.domain.repo.po |
StudentCoursePO |
|
基 础 设 施 层 |
ID生成器工具类 |
基础设施 |
*.selectcourse.infrastructure.util |
IdGenerator |
根据DDD的代码模型和各领域对象所在的包、类和方法,我们可以定义出选课微服务的代码目录结构,并将代码对象放在合适的层和代码目录结构中,如下图所示:
本文中,我们通过学生在线选课项目,将DDD设计过程完整的走了一遍。
DDD战略设计:从事件风暴开始,提取实体和值对象等领域对象,找出聚合根构建聚合,划分界限上下文,最后建立领域模型。
DDD战术设计:将领域模型作为微服务设计的输入。识别和设计服务,建立各层服务的依赖关系。设计微服务内的实体和值对象,找出微服务内的所有领域对象。建立领域对象与代码对象的映射关系。这样就可以很好地指导项目团队进行微服务开发和测试了。
战略设计和战术设计是DDD设计的两个不同阶段,也是衔接业务和技术实现的两个阶段,我们要做好这两个阶段的衔接工作,确保领域模型在微服务中的完美落地。
参考资料:《中台架构与实现 基于DDD和微服务》