本章的内容十分硬核,使用实际案例-货物运输系统进行说明,如何使用DDD一步步落地。根据本章的内容,这里总结下作者落地DDD的流程。
- 提出业务需求,业务需求展现形式不局限,可能是prd,wiki,或者几句话。
- 消化需求,同PD或者领域专家讨论,提炼需求中的关键知识点。
- 经过多次讨论后,同领域专家建立通用领域语言。一般表现形式为:标准统一的领域单词,或者宽泛的UML类图。
- 模型驱动设计,建立起核心模型。核心模型表现形式一般也是采用UML类图。建立核心模型分为以下几步,这里需要进一步拆分。1)划分entity,value object。2)确定模型类之间的关联关系。3)划分边界,确定aggregate。4)设计repository。5)确定数据库模型。6)根据需求,进行场景走查,逐步迭代精简模型。
- 明确应用层职责和能力,确定最终存储层的设计。在很大程度上这一步已经体现出架构设计。
- 组织编码,按照领域知识拆分包,核心领域层代码编写(entity,value object,aggregate,factory,service)。应用层以及存储层完成具体的编码实现。
第一部分:业务需求
假设我们为货运公司开发软件,最初的需求包含以下三项基本功能:
(1) 跟踪客户货物的主要处理;
(2) 事先预约货物;
(3) 当货物到达其处理过程中的某个位臵时,自动向客户寄送发票。
第二部分:消化需求,提炼关键知识点
2.1 初步分析
(1) 跟踪客户货物的主要处理;
- 跟踪。货物的历史记录,从何处运到何处,通过何种方式运送,在每个阶段是如何处理?
- 货物的主要处理,货物分为哪些处理方式,Cargo Hading可以抽象为不同的处理方式
- 客户。客户是否唯一,客户承担哪些角色,客户与货物的关系?
(2)事先预约货物。
- 提供单独的预约功能,指定预约,从何处送到何处,通过何种方式。
(3) 当货物到达其处理过程中的某个位臵时,自动向客户寄送发票。
- 满足Cargo Hading的某个阶段,以及到达某个指定位置,向客户发送发票。
2.2 提炼出的关键知识点
模型层面
客户模型,Customer,承担多角色,如:托运人,收货人,付款人。一个Cargo只能由一个Customer来承担,限定多对一关系。
作者在这部分内容,提炼出了一个非常核心的模型。Delivery Specification (运送规格),这里的运送规格有点抽象,结合书中内容,具体是指:包括了运送的目的地、到达日期等。
Delivery Specification (运送规格) 对象作用:
- 减轻Cargo对象职责。
- 与运送相关的所有细节,统一由Delivery Specification承担。
Carrier Movement,表示由某个Carrier(车或者船),执行从一个Location到另一个Location的旅程。
Delivery History,运送历史,反映了Cargo实际上发生了什么事情。
应用层功能提炼
- 查询货物跟踪功能。Tracking Query
- 预定货物功能。Booking Application
- 自动发送发票功能。Send Application
第三部分:确定通用领域语言
第四部分:建立核心领域模型
1. 划分entity,value object
entity
- Customer,客户,customer使用纳税号作为唯一标识。
- Cargo,货物,这里可以采用货物码作为唯一标识。
- Carrier Movent,运输活动事件,是真实世界中发生的事件,这里可以采用调度表的唯一ID作为唯一标识。
- Location,位置,可以采用自增主键作为唯一标识。
- Delivery History,运送历史,这是一个比较复杂的对象,与Cargo一对一。唯一标识可以采用自增ID。
- Hading Event,处理事件,唯一标识:Cargo Id + 完成时间 + 类型
value object
- Delivery Specification,假如有两个Cargo去往同一个地点,可以采用同一个Delivery Specification。
2. 确定模型类之间的关联关系
划分边界,确定aggregate
aggregate
- Customer有自己的唯一标识,即自己单独的聚合根。
- Carrier Movent,也是自己的聚合根。
- Location,也是自己的聚合根。
- Cargo这个稍微有点复杂,Cargo的聚合根即货物的唯一ID,但是聚合的边界,包括Delivery History、Delivery Specification。1)Delivery History,因为没人会在不知道Cargo的情况下直接去查询Delivery History。因为Delivery History不需要直接的全局访问,而且它的标识实际上只是由Cargo 派生出的,因此很适合将Delivery History放在Cargo的边界之内,并且它也无需是一个AGGREGATE根。2)Delivery Specification是一个VALUE OBJECT,因此将它包含在Cargo AGGREGATE中也不复杂。
- Hading Event,自己的聚合根。
设计repository
后续的优化,Handing Event作为单独的聚合根,需要有repository。
第五部分:确定分包逻辑
按照,Customer,Shipping,Bill三个包进行划分。这样领域知识会比较清晰。
总结
至此整个实例的模型驱动设计过程,已经展现的比较清楚,此笔记省略了后面的系统交互部分,因为这部分感觉放到六边形架构去说比较合适。
这里再提炼下,根据此篇文章提炼到领域驱动建模的方法论:
- 业务需求消化,业务需求一定要详尽,其中涉及到的领域知识一定要能完整清晰的体现出来。这里对PD的要求比较高。拒绝一句话需求,例如需求:客户可以预定货物。这个需求就太粗狂了,需要对这种需求逐步细化,可以加N多形容词,进行表达疑问。例如:什么样的客户,具有什么角色、权限的客户,通过什么方式去预定,预定货物有什么要求,是全部货物还是指定货物,预定后,是否要通知客户,预定成功,货物的各个流转阶段,是否也要通知客户。预定的这些货物,在后台谁能看到?什么角色的人去维护这些预定的货物。可以看到,根据一句话需求,能衍生出N多问题。
- 解决掉需求中的各种疑问,提炼关键知识点,建立起核心模型的轮廓。
- 与PD或者领域专家,反复交流沟通,基于关键知识点,建立起通用领域语言,这里的通用领域语言,已经是比较宽泛的UML类图了,可能还不是很全面,但是离核心模型已经越来越清晰了。
- 模型驱动设计,建立核心领域模型。区分entity,value object,aggregate,service,factory。划分聚合的边界。采用场景走查方式,反复确认核心模型是否可以满足需求。
- 数据存储设计。基于核心领域模型的entity,去建立底层存储模型。
- 应用层职责明确。围绕PRD,确定应用层接口设计。
- 编码阶段:module划分 + 设计模式。