前言
领域驱动设计断断续续看过几本书,但是都没有系统性的学习完成过,因此这篇文章从三个阶段来完成的记录一下,分别是基础概念、核心知识、高级扩展;
基础概念
领域驱动设计的开发流程
模型的建立:获取行为需求 -> 领域建模。
模型的实现:架构设计 -> 数据库设计 -> 代码实现。
DDD 的基本开发过程:获取行为需求 -> 领域建模 -> 架构设计 -> 数据库设计 -> 代码实现
获取行为需求
获取行为需求指的是,我们首先要分析系统具有哪些功能,这些功能由什么人操作,会产生什么效果。这个过程传统上叫做“捕获行为需求”
常用的获取行为需求的方式有两种,第一种是用例(use case),第二种是事件风暴
2.1 事件风暴
!()[]
事件风暴在实践过程中会遇到来回返工的情况,出现这两种情况一般是两个原因,第一个原因会议偏离主题、第二个原因会议无结论记录,因此在事件风暴这个环节,可以落地的方法是由一个人负责,完成梳理之后进行评审的方式;
事件风暴过程中需要注意的点:
- 对于领域事件的命名,采用完成时+被动语态,例如添加完成订单,在事件风暴中的动作是’订单已添加’、签订好合同,在事件风暴中的动作是’合同已签订’;
- 在DDD的命名中,如果有约定俗成的业务术语,优先使用业务术语;
- 不要把技术事件当作领域事件,例如事务提交、缓存已命中等;
- 查询功能不算领域事件;
可以使用https://boardmix.cn/来做事件风暴
2.2 识别命令
命令(command),就是引发领域事件的操作,我们可以通过分析领域事件得到。除了识别出命令本身以外,我们通常还要识别出谁执行的命令,以及为了执行命令我们要查询出什么数据;
!(命令图列)[]
2.3. 识别领域名词
领域名词,是从领域事件、命令、执行者、查询数据里找到的名词性概念。例如,对于签订合同这个命令而言,受到影响的名词性概念是“合同”;
- 注意实现
- 在事件风暴里只列出主要的、足以用于表达和交流领域知识的步骤
- 事件风暴的粒度原则上宜粗不宜细,有一个大体的轮廓即可
小结:事件风暴就是先确定要做什么事情(领域事件),然后由于什么动作出发(命令),然后找到动作由谁(执行者)进行触发,最后早到出发这个动作需要查询什么数据(查询数据)的过程:
领域建模
目的:
- 将业务知识可视化,准确、深刻地反映为领域知识,并且在业务和技术人员之间达成一致;
- 指导系统的设计和编码,也就是说,领域模型应该能够比较容易地转化成数据库模式和代码实现;
!(模型之间的联系)[]
领域对象表示的是领域事件的作用端,也就是领域名词,例如提交订单这个领域事件对应的领域对象就是订单;
领域对象由三部分组成:
- 领域对象
- 领域对象之间的关系
- 领域对象的关键属性
- 领域对象又可以划分为:实体、值对象
实体对应到代码中就是类对象,实体与实体之间的关系就是类图,类有关键属性;
建立完成领域模型后,需要完善业务规则、建立词汇表
- 模型驱动设计
- 领域模型要和业务需求保持一致
- 系统实现要和领域模型保持一致
领域模型是堆业务进行模拟和提炼,形成浓缩的知识
- 统一语言
统一语言包含了两个层面的含义:一是业务和技术人员之间的语言是统一的,二是开发团队内部各角色之间的语言是统一的。最终结果就是每一行代码都能对应到统一语言,从而与业务保持一致
架构设计
分层架构
分层架构的核心思想就是将代码分成若干层,每一层负责不同的关注点;将不稳定的部分依赖与稳定的部分,常用的分层架构有六边形架构,外层依赖内层,但是内层不能依赖外层;
实践上就是采用分模块和分package的方式进行分层
数据库设计
在传统的软件开发过程中,对于数据库的设计是采用的’主观理解法’俗称拍脑袋法进行设计的,在了解到需求后,通过思考需求得到数据库模型,在根据数据库模型进行实现,这种方式设计的数据库模型很难精确的反映业务领域模型;
采用DDD的方式通过模块到实体,在到数据库模型的方式进行设计的方法,得到的数据库模型更贴近业务领域模型;
代码实现
在对象的定义上有两种模式:面向过程的方式和面向对象的方式,面向过程的方式就是常说的贫血模型,面向对象的方式就是充血模型或者说是富领域模式;在实践过程中并不是面向过程模式就要比面向对象模式低级,在实际研发过程中两者的关系如下所示:
面向对象和面向过程之间有一个广阔的"灰色地带",这里面的变化非常多,难以穷尽,这两个极端都不是我们要追求的,我们需要做的是找到其中的一个平衡点;
原则:
- 依赖层问题
- 依赖倒置原则
提供领域对象的封装性
- 限制getter和setter的数量
- 用有业务含义的接口替代简单的setter和getter
编程风格
- 领域对象不能访问数据库
- 领域服务只能读数据库,领域服务需要读数据库。而写库的功能通常可以由应用服务来做,从而减轻领域层的负担
- 应用服务可以读写数据库
- 用ID表示对象之间的关联
- 领域对象有自己的领域服务
小结
捕获行为需求和事件风暴的关系如下图所示,通过事件风暴的形式捕获到系统的行为需求,从而形成统一语言和模型驱动设计
执行者查询读数据,然后发出命令。命令触发领域事件。
可以从命令、领域事件、执行者、读数据中识别出领域名词。
这张图是领域驱动设计的精华,最核心的是领域模型,领域模型上面衍生出领域对象,下面衍生出业务规则;
业务规则
聚合
聚合指的是实体与实体之间的一种关系,这种关系有两种特点:第一具有整体和部分的关系,第二具有不变规则,而且这种不变规则的并发的时候可能被破坏;
这种关系中心的实体被称为聚合根,聚合根需要有一个全局唯一标识;\
聚合的作用是为一组具有整体部分关系的对象维护不变规则;
减号(-)表示这是一个私有(private)属性、加号(+)表示公有(public)权限、井号(#)表示保护(protected)权限、波浪号(~)表示包级私有(package private)权限
实现对象关联的方法分为两种,一种是对象关联、一种是ID关联,使用对象关联切换的成本较高,比较倾向与使用ID进行关联;
1
对聚合对象进行持久化的时候,是针对整个聚合下的多个对象进行的持久化
Repository.java
public void save(Emp emp) {
// 持久化聚合对象
empRepository.save(emp);
// 插入skill对象
emp.getSkills().forEach(skill -> {
skill.setEmpId(emp.getId()); // 设置关联的ID
skillRepository.save(skill);
});
//插入work对象
emp.getWorks().forEach(work -> {
work.setEmpId(emp.getId()); // 设置关联的ID
workRepository.save(work);
});
}
Repository表示的是对整个聚合进行持久化操作,而DAO表示的是对单个对象进行持久化操作,因此在DDD中Repository是对聚合的操作,而DAO是对单个对象的操作;
对于不变规则的实现,有两个需要注意的点:
- 如果规则的验证不需要访问数据库,那么首先应该考虑在领域对象里实现,而不是在领域服务里实现;
- 对于聚合根的内部对象,对于它们的验证必须是从整个聚合层面才能进行验证的,无法单独进行验证;
聚合在达到一定规模之后也要进行拆分,避免一个大的聚合导致的性能问题;
用事务保证固定规则
由于数据库事务无法完成避免并发修改聚合根的问题,会存在数据库事务的粒度与业务需求不匹配、丢失更新的问题,因此需要使用乐观锁的方式来保证聚合根的并发修改问题;
乐观锁的实现方式:
- 在聚合根中添加版本号字段
- 在更新聚合根时,使用版本号进行校验
- 如果版本号不匹配,则说明有其他事务已经修改了聚合根,需要重新加载聚合根并进行处理
悲观锁的实现方式:
- 在聚合根中添加锁字段
- 在更新聚合根时,使用锁字段进行校验
- 如果锁字段被其他事务占用,则等待或抛出异常
类似于独占锁的方式,悲观锁会导致性能问题,因此在DDD中不推荐使用悲观锁的方式;
没有关联对象的聚合根,被称为单实体聚合,现在我们面向对象的项目对于PO的定义就是单实体聚合;
泛化
泛化是指在领域模型中,将具有相同属性和行为的对象抽象为一个父类,从而减少代码的重复和复杂度;
泛化的实现方式是通过继承的方式,将相同的属性和行为;
直接“借用”系统中已经存在的机制,在短期内虽然达到了目的,但长期来看会导致概念混乱,这种做法是很多开发团队常见的错误。而错误的根源,就在于我们没有掌握一种优雅的方法,来处理不同概念的共性和个性;
可以泛化,不代表必须进行泛化,有三个原则:
第一:如果只有特征值不相同,那么用特征值为对象进行分类即可,不必要进行泛化;
第二:如果特性种类不同,那么很可能需要进行泛化
第三:如果在业务规则、操作接口、操作实现上有共性和个性的化,优先考虑在实现上
限界上下文
限界上下文确实和划分模块、划分子系统一样,是一种分而治之的手段,可以起到分离关注点的作用。但限界上下文增加了一个要点,就是,它的目的还在于维护概念一致性。这里的概念一致性,不在是全局一致性,而是局部一致性;
限界上下文的划分是和组织结构相关的,由于全局一致性已经超过了团队的认知负载了,所有限界上下文不在追求全局一致性;
如何践行领域驱动设计
实施DDD的项目要满足以下几个关键因素:有价值\有痛点\有意愿\有时间;
-
有价值,是指站在企业的角度,这个系统对达成企业的战略目标有较大的意义;或者从业务角度,这个系统能够为公司带来比较大的收益。包括 DDD 在内的任何技术改进过程,都需要一定的成本。只有应用到价值较大系统,才能带来足够的回报
-
有痛点,指的是公司管理层或者开发团队确实遇到了难以解决的困难,需求寻求方法学的帮助。如果目前的开发方法挺顺利的,没有感受到明显的问题,只是“为了引入而引入 DDD”,那么往往会动力不足;
-
有意愿,指的是开发团队确实愿意学习新技能。其中,项目经理、开发组长、技术骨干等角色往往起着决定性的作用;
-
有时间,也很重要。引入任何新技术,总会有些成本。包括学习成本、试错成本等等。关键是看产出是否大于成本;
实施DDD的场景有三种实际场景:
- 新的项目
- 改造现有项目
- 改进现有的研发流程
新的项目
领导希望保证质量,降低风险,觉得需要方法学的支持,因此要引入 DDD
在新的项目中引入DDD要避免的是瀑布型思维,避免在一开始就想要把模型建立的完美;
改造现有项目
改造现有项目主要分为四个步骤:反推领域模型 -> 建立目标领域模型 -> 设计演进路线 -> 迭代实施,在这个过程中,一定不要陷入第一个步骤和第二个步骤中,在这两个步骤中只会有模型产出,没有实际落地的代码,也看不到任何实际的效果,会导致人们失去耐心,最终DDD无疾而终;
建议的做法是,首先选择系统中一个相对独立的小模块,然后按照前面的 4 步,尽快落地到代码并上线,建立最小闭环。通过这个过程,初步掌握 DDD 落地技能并取得实际效果。
同时,这么做也能培养人才,积累经验,建立必要的开发流程。完成之后,再选择下一个切片,逐步扩大范围,并深化 DDD 的技能。