相关代码:
- https://github.com/kibaamor/DDD-smartrm-monolith-study
- https://github.com/kibaamor/DDD-smartrm-micro-services-study
领域驱动设计的背景和来源
02 DDD的核心思想和解决的痛点问题
模型和建模
- 模型是对领域的抽象和模拟
- 建模是针对特定问题建立领域的合理模型
比如,同样的商品:
- 对于超市来说,主要关注商品的条码
- 对于电商来说,主要关注商品的标签、图片等
软件系统复杂性的来源
- 建模过程中的领域知识和业务本身的需求
- 技术实现过程中本身的复杂性
战略设计
02 建模和设计的整体流程
- 挖掘用户故事
- 建立通用语言
- 战略设计
- 战术设计
上面的每一步自开始后会持续项目结束,并且这些步骤都是循环迭代的过程。比如:战术设计时发现之前战略设计有不合理的地方时,需要再次进行战略设计。
参与人员
建模的过程需要下面人员的参与:
- 产品团队(即策划团队)
- 领域专家
- 研发团队
用户故事
用户故事是对问题的描述,而不是解决方案。
通用语言
在讨论模型和定义模型时,团队使用的同一种语言。
为什么需要通用语言?
- 领域知识需要在团队内部高效流转,模型需要转述。
- 通用语言要体现在代码里。
战略设计
DDD 中对问题空间和解决方案空间进行分解的过程就是战略设计。
战略设计的目的是分解模型以控制复杂性。
战略设计也是 DDD 与传统建模和设计方法的核心区别之一。
战略设计主要做 3 件事:
- 领域划分
- 寻找限界上下文
- 确定上下文映射(即各个限界上下文之间的关系)
战术设计
战术设计就是对各个限界上下文的细节设计的过程,确定限界上下文内部的模型结构与完整技术方案。
战术设计是包括编码的。
常用建模方法
Domain Storytelling(领域故事陈述法)
与用例分析法类似,本质没有区别,只是工具更成熟一些。简单易用。
Event Storming(事件风暴法)
最主流、影响最大的方法。
4C(四色建模法)
不是专门为 DDD 提出来的,而且思路会复杂一些。
03 从用户故事到通用语言
用户故事
产品需求不断修改的原因
捕捉用户需求,确定产品特性本身就是很复杂的事,尤其是结合商业环境,竞争对手,乃至技术可行性等要考虑的很多。
对于这么复杂的事,大多数产品(策划)是无法独自做决策的。
好的方式是:
- 避免产品经理独自做决策。
- 先从问题空间出发,让产品(策划)团队和开发团队的关键人员,围绕用户故事进行讨论,确定大致方案后,再各自细化。
- 基于讨论的结果,产品(策划)细化具体的交互设计,开发人员进行建模和技术方案的设计。
这就是用户故事的意义。
避免产品需求不停的修改。
什么是用户故事
在软件开发中,用户故事是一种对软件系统特性的非正式的自然语言描述,是敏捷软件开发中从终端用户的角度对软件系统特性进行捕捉的一种方式。用户故事描述了不同类型的用户需要什么以及为什么需要,它可以帮助我们创建需求的简单描述。
在软件开发和演进过程中,随着产品和开发对产品认识的加深,需求总是在不断变化,所以,过早地进入需求细节以及对细节的描述,是一种时间上的巨大浪费。从这一点来说,用户故事提供了一种恰到好处的粒度,使得产品在需求分析阶段能够极大地节约时间,并且使产品和研发人员始终把注意力集中在关键点,避免他们过早地陷入细节以及被细节所局限,同时给产品功能留出了讨论空间,从而使产品有机会在讨论过程中得到优化。
用户故事的构建一般来说有三个环节:
- 简单描述用户需求;
- 围绕简单描述进行讨论;
- 明确如何验证。
分别对应用户故事的三个元素,也就是 3C:Card(卡片)、Conversation(谈话)、Confirmation(验证)。
卡片(User Story)
“卡片” 就是指对用户故事的简述(传统上人们通过便利贴在白板上构建用户故事),一个好的用户故事卡片包括三个要素:
1.谁:谁需要这个功能;
2.需要什么:想通过系统完成什么事情;
3.为什么:为什么需要这个功能,这个功能带来什么样的价值。谈话
谈话是指用户、领域专家、产品经理、研发之间围绕用户故事进行的讨论,谈话是明确需求细节的必要环节。可以用文字对谈话进行简要记录,此外,也可以基于图形或其他工具进行讨论。
验证(Acceptance Criteria)
验证代表了验收测试,描述了客户或者产品owner怎样确定用户故事已经被实现,且能够满足需求。一般可以用如下模板写Confirmation:
假设我是<角色>,在xxx情况下, 当我<操作>, 那么<结果>。
使用 Domain Storytelling 分析用户故事
学会一种语言的最好方式是听别人说这种语言,尝试重复他们所说的并留意他们的反馈。
什么是 Domain Storytelling
Domain Storytelling 是一种领域分析建模方法,通过这种方法,产品和研发人员可以利用语言学习的相同原理建立或者学习一个领域的通用语言,并且建立领域模型。
Domain Storytelling 图和相关工具
在 storytelling 过程中,一方(通常是架构师)聆听另一方(通常是产品经理或者领域专家)以主谓宾的句型(谁做了什么)讲述用户故事的流程,进行问答和讨论,并以图形的方式快速复述出来。
通用语言
通用语言指一种描述领域模型且基于模型的语言。
团队在进行所有交流时都使用通用语言。
通用语言包括:
- 类和操作的名称
- 施加于模型之上的规则和约束
- 应用于领域模型的模式
06 分解问题:领域划分和子域
什么是领域划分
领域划分是以分离关注点为原则对问题空间的划分。
子域是领域中某个方面的问题和解决它所涉及的一切。
领域就是某个组织所做都事情,及其中所包含的一切。
由于用户故事就是对问题的描述。从这一点来说,用户故事和领域划分其实是很搭的。所以可以对产品中典型的用户故事所描述都问题进行划分或分类来进行初步的子域划分。
为什么要进行领域划分
为了避免问题点和领域知识的重叠。
比如:产品的 A 功能和 B 功能都需要支付功能,如果按照传统的开发方式,将 A 功能和 B 功能分给不同都人做,那么就会重复实现支付功能。
模型都重叠。
比如:产品中有玩家物品这个模型,如果不同的人开发不同功能时同时实现玩家物品这个模型,就会导致产品中出现重复的玩家物品模型。
而领域划分可以解决上面提到的这些传统模式的问题,让不同都子域聚焦不同都问题。
基于故事分解的领域划分
利用用户故事和领域划分的相关性来划分领域。
对于使用 Domain Storytelling 分析用户故事得到的图,也可以被认为是对用户故事都分解,每个序号代表的语句可以认为是一个子用户故事。
进一步细化 Domain Storytelling 图中复杂都子用户故事,细化到每个子用户故事只关注很小的一个领域问题,甚至是一个原子操作。
对这些子用户故事按照关注点进行分类。
总结
- 领域划分是对问题空间的划分,划分的原则是分离关注点。
- 基于领域划分进行分工协作而非基于需求。
- 基于用户故事分解可以让领域划分清晰化。
07 确定系统最核心的部分:核心域和精炼
子域的类型
子域类型:
核心域
代表产品核心竞争力,没有核心域就不是这个产品
通用子域
代表各个产品之间能够通用的功能。比如:支付等。
支撑子域
一个子域既不是核心域,也不是通用子域,那么它就是支撑子域。
核心域和研发力分配
研发力应当主要用于核心域的开发。
精炼
通过精炼,分离出一个领域中普通的部分(通用子域、支撑子域),得到领域中极致精华的核心部分(核心域)。将研发力投入到核心部分都开发,能得到最大的价值。
精炼让团队用尽可能小的代价,换取最大的成功概率。
如何精炼
萃取
随着对领域分析的深入,逐渐发现最能影响产品成功都关键是什么,从而把它单独提取出来。
分离
随着对问题都分析,逐渐剥离出来对领域问题影响不大的部分,剩下都部分就成了更为精华都核心部分。
总结
战略设计要明确核心域,团队尽量减少非核心域投入
从个人发展角度,程序员也要尽量投入核心域的工作
核心域的建立总是伴随着精炼,精炼有两种方法
08 分解模型:限界上下文
什么是限界上下文
“First, a Bounded Context is a semantic contextual boundary.
This means that within the boundary each component of the software model has a specific meaning and does specific things.
The components inside a Bounded Context are context specific and semantically motivated.”—— Vaughn Vernon 《领域驱动设计精粹》
限界上下文是一种语义上的上下文边界。意思是在这个边界里的软件模型组件都有它特定的含义并且做特定的事。一个限界上下文内的组件都是上下文特定的并且语义明确的。
限界上下文是在解决方案空间对模型的分解单位。
比如,对于售卖机来说:
- 在运营眼里,它是一个库存不断被消耗的、又需要计时进行补充的商品容器。
- 而在用户眼里,它是一台吞钱吐商品都机器。
所以,当运营人员或者用户在他们各自的场景或者山下文里提到售卖机这个词时,它的意思其实是有差别的。如果要让”售卖机”这个词具备单一的含义、单一的作用,那就需要明确指出其所处的上下文。换句话说它的限界上下文是什么,这就是限界上下文的本质含义。
为什么需要限界上下文
自然语言具有模糊性
同一个词在不同场合、不同上下文里,通常有不同的含义。
同一个事物面向不同场景有不同模型
软件系统需要分解模型以控制复杂性
限界上下文是分工的单位
模型分解后,天然的具备了高内聚、低耦合的特性。团队的分工协作,非常适合基于限界上下文来进行。
之前也说过,分工协作要基于子域。这里为什么又说要基于限界上下文了。这是因为,在理想的情况下,子域和限界上下文是一一对应的。
子域属于问题空间,而限界上下文属于解决方案空间。
所以,这两种说法并不冲突。而且在建模的过程中,限界上下文是后于子域出来的。所以在前期的分工,确实需要基于子域,但是在后期进入战术设计阶段,就是基于限界上下文了。
虽然子域和限界上下文的关系很多时候不是理想中的一一对应,但是不影响这里的结论。
如何划分限界上下文
- Domain Storytelling (领域故事陈述法)
- Event Storming(事件风暴法)
- 基于子域概念提取
Domain Storytelling 中边界特征
单向联系
比如上图中的:库存计划——配送订单。
语义区别
比如:上图中的售卖机和商品等。
tips: 如果一个 Domain Storytelling 图中两个相同的名词有不同的含义,应当在图中使用不同的图标。
活动的触发方式不同
比如:上图中定时执行的库存计划。
另外,系统的建模和设计并没有一个标准和准确答案,上面的几点判断方法也不是绝对的。
Event Storming
之后才介绍。
基于子域概念提取
结合子域和限界上下文的结果:
限界上下文和微服务
一句话概括,微服务是限界上下文的实现方式。
09 上下文映射和防腐层
上下文映射
字面意思就是:上下文映射是指限界上下文之间的模型映射关系。
更直白的解释就是:描述团队之间的协作关系以及上下文之间的集成关系。
一个上下文是否依赖另一个上下文,如果依赖的话,被依赖的上下文用什么方式提供服务。两个团队之间的关系怎么,谁说了算。
了解了上下文之间的依赖关系,有助于决定上下文之间如何集成以及如何设置防腐层。
之前通过分解模型,划分出了限界上下文以及它们之间的联系后,已经初步确定了各个限界上下文之间的依赖了。
依赖不仅仅是包含各个限界上下文之间的调用关系,还有各个限界上下文中的模型和模型的映射关系。
上下文映射模式
Partenership 合伙人
- 技术无关,是一种团队协作关系
- 两个团队之间可以随时互通有无,协同变更
Shared Kernel 共享内核
- 两个上下文共享部分模型
- 包括但不限于代码、jar包、.so、数据库表等等
- 慎用,仅当团队紧密合作且共享部分稳定
Customer/Supplier 客户/供应商
- 下游上下文可以向上游上下文提需求
- 一般用于核心域与非核心域之间的协作
Conformist 顺从者
- 没有模型到模型的转换
- 一个上下文沿用另一个上下文的部分模型
Anticorruption Layer 防腐层
- 把上游上下文的模型转换成自己上下文的模型
是下游上下文中访问外部模型的一个代理层
Separate Ways 分道扬镳
- 两个上下文无协作,各自独立
- 当两个上下文之间的集成成本过高
Open Host Service 开放主机服务
- 服务提供方为所有消费方提供一套公共的API
针对通用的功能和模型
Published Language 公开语言
- 标准化与协议化的模型
- 所有上下文都可以与公开语言中的模型进行转换
对接了公开语言的上下文之间可以实现组件化对接
举例:蓝牙协议、tcp/ip、SQL。
Big Ball Of Mud 大泥球
- 由混杂的模型构成的糟糕系统,模型不稳定且难于维护
- 与大泥球合作的上下文要确保自身不被污染,设置防腐层
初涉战术设计
03 战术设计分析和交易域依赖准备工作
战术分析——对象间的关系
战术分析挖掘:
一个对象为另一个对象的状态变更提供数据
比如上图中,用户看到的交易上下文中的售卖机商品列表就需要从商品上下文中的商品信息和设备上下文中中的售卖机商品库存获取信息。
一个对象的状态变更导致另一个对象的状态变更
比如上图中,支付上下文中的支付状态的变化会影响交易上下文中订单的状态。
战术分析的整体流程:
- 得到战略分析结果:得到了系统中的各个限界上下文。
- 从角色出发,引出系统中的一些关键对象。考虑这些对象是否需要系统中其他对象提供数据,状态变化是否会导致其他对象状态发生变化。在这些对象之间建立关联。
- 通过跨越上下文的关联关系,已经可以把各个上下文需要提供的服务接口定义出来。
对于 Domain Storytelling 这个建模工具,它是以用户、角色、活动为核心的。
所以 Domain Storytelling 的图中,只有从角色出发的箭头才有标号,而系统内部的这些对象之间的关联发生的时序是没有办法进行标注的。
Domain Storytelling 的图只能展现静态的结构,想要表达复杂的时序关系的话,就需要接触其他的工具,即 UML 中的时序图。
06 实体和值对象
什么是实体和值对象
实体
主要由标识定义的对象被称作 ENTITY
值对象
用于描述领域的某个方面而本身没有概念标识的对象称为值对象
总结:需以 ID 来跟踪状态变化的对象为实体,否则为值对象。
对象的相等性
- 实体:标识符相等性
- 值对象:属性相等性
实体和值对象的特征
实体
- ID 相等性
- 要跟踪状态变化
值对象
- 属性相等性
- 可互换
- 不变性
为什么要区分实体和值对象
尽可能用值对象而不是实体。
- 值对象往往更轻量级
- 值对象不用跟踪变化
实体和值对象在领域中扮演的角色不一样
实体可以作为聚合的聚合根,而值对象不能。
分辨实体和值对象
- 根据上述技术特征
更直观的领域模型特点
- 是否只读
- 生命周期是否跨越活动
08 领域对象的构造
对象构造是谁的职责,如何确保相关对象的一致性
聚合内部的领域对象的构造一般是聚合根来提供,聚合根本身的构造一般是领域服务或者应用层服务中进行。
- 工厂方法模式
- 抽象工厂模式
如何兼顾对象构造的简便性和对象的封装性
- Builder 模式
实体 ID 应该如何生成
基于已有信息的拼接
比如:身份证号码
基于数据库表自增ID
基于独立的ID生成器
比如:UUID
09 资源库与持久化
什么是资源库
为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的”替身”。通过一个众所周知的全局接口来提供访问。
—— Eric Evans《领域驱动设计》
总结:带必要管理功能(增删查改)的领域对象容器,与技术实现无关。资源库是领域模型的一部分,它可以屏蔽存储层的技术细节。
即 Repository 模式。
资源库的意义
- 提供一个管理领域对象的简单模型
- 使领域模型和持久化技术解耦
10 优雅地实现一致性:聚合
什么是聚合
聚合就是一组相关对象的集合,我们把它作为数据修改的单元。每个聚合都有一个根和一个边界。聚合根是聚合所包含的一个特定实体。对聚合而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。
—— Eric Evans《领域驱动设计》
总结:聚合是拥有事务一致性(强一致性)的领域对象组合。
聚合的原则
- 聚合内的实体适用事务一致性
- 聚合之间适用最终一致性
- 不脱离聚合根修改聚合内部对象
- 聚合根有全局唯一标识,聚合内部实体只有局部标识
- 聚合根可以从资源库获取,聚合内部实体不能
聚合解决什么问题
- 优雅地实现一致性
聚合是限界上下文粒度的下限
聚合的识别
实体是否在所有活动中都协同变更
11 独立的业务逻辑:领域服务
什么是领域服务
当领域中的某个重要的过程或转换操作不是实体或值对象的自然职责时应该在模型中添加一个作为独立接口的操作,并将其声明为领域服务,定义接口时要使用模型语言,并确保操作名称是通用语言中的术语。此外应该使领域服务成为无状态的。
—— Eric Evans《领域驱动设计》
总结:
- 领域服务只包含业务逻辑。
- 领域服务中的业务逻辑不属于实体或值对象。
- 领域服务是无状态的。
领域服务的模糊性
业务逻辑是否可以放在实体或值对象内
仅在必要的时候引入领域服务
相关代码是属于领域服务还是应用层
领域服务与应用层服务的区别:领域服务只包含业务逻辑,应用层服务原则上不包含任何的业务逻辑。
12 应用层实现(一)
什么是应用层
定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道。
应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。—— Eric Evans《领域驱动设计》
应用层的职责
接受用户的具体请求,然后调用领域模型来完成这些请求。
应用层是调用领域模型完成用户需求的地方。另外在实现用户需求的过程中,还要实现一些隐形需求,比如:安全、限流等。
常见的职责:
- 事务控制
- 身份认证和访问权限
- 定时任务调度
事件订阅
- 事件监听(适配层)
- 事件处理(应用层)
应用服务和领域服务的区别
深入战术设计
04 领域事件
什么是领域事件
- 领域中发生的任何领域专家感兴趣的事情
领域事件一般由聚合产生
领域服务也可以产生
领域事件不是技术概念
它其实是通用语言中的内容
事件命名和基本属性
命名方法:名词+动词的过去完成时。比如:OrderCanceledEvent、OrderCreatedEvent 等
基本属性
事件 ID
全局唯一的 ID,便于去重。
产生时间
发布和订阅方式
外部系统
- API 定向通知
- API 定时拉取
- 消息队列
内部系统
- 观察者模式
- 数据库流水(binlog)
- 消息队列
事件存储
直接使用消息中间件的存储
注意:
- 冗余机制
- 做好备份
基于数据库
- mongodb
- postgresql
mysql
注意:
推荐使用分布式
- 按时间分区
事件处理的要求
顺序性
注意:
- 聚合 ID
- 存储分片
- 消费分组
幂等性 — 用幂等性代替分布式事务
- 状态判断或去重
领域事件和大数据分析
05 事件风暴建模法
什么是事件风暴
https://www.eventstorming.com/
- 一种协作式的对复杂业务领域进行探索的讨论形式
- 一种灵活易调整的的轻量级的适用于DDD的建模方法
- 由Alberto Brandolini 于2013(或2012)年提出
视频介绍:What is Event Storming? | Paul Rayner
补充:Event Storming - what it is and why you should use it with Domain-Driven Design
事件风暴的应用场景
- 评估已有业务线的健康度并发现优化点
- 探索一个新业务模型的可行性
- 设想为各个参与方能带来最大利益的新服务
- 设计整洁的可维护的软件以支持快速推进的业务
事件风暴核心词汇
- 领域事件
- 聚台
- 决策命令
- 角色
- 读模型
- 策略
- 外部系统
- 问题/热点
组织会议
- 产品经理
- 领域专家
- 研发人员
会议一般 4-8 人,最好有一个主持人来控制节奏。
准备工具
- 便利贴
- 胶水/磁力贴
- 白板/纸
- 笔
事件风暴的过程
时间线
白板/纸上从左向右画一根横向的箭头表示时间线。
列出主要的领域事件
- 使用橙色便利贴
- 事件命名使用动词过去式
- 事件一定和领域专家相关,领域专家关注的事件
收集关注点和问题
使用紫色的便利贴,写出:
- 问题
- 风险/关注点
- 假设
- 讨论点
通过命令深入领域
通过领域事件,反向分析出相关的角色和命令。
- 蓝色便利贴代表命令
- 黄色便利贴代表角色
找到聚合
找出连接角色命令和领域事件的重要元素,也就是进行事件风暴的重要目标,找到聚合。
在事件风暴中,是谁来负责处理用户提交的命令,领域事件又是谁产生出来的,其实就是聚合。
聚合负责处理:
- 处理领域逻辑
- 处理命令
- 产生领域事件
聚合使用浅黄色便利贴。
角色和聚合都是使用的黄色便利贴。如果黄色和浅黄色不易区分,角色可以使用黄色小便利贴,聚合可以使用黄色大便利贴。
找出读模型
- 帮助用户做出决策
- 数据查询
读模式使用绿色便利贴。
策略
接下来就是,围绕选定的领域事件,确定它会触发什么策略。
策略对应的是一段响应式的逻辑。当策略的条件被满足时,它会对对应的事件进行响应,并且它也会触发命令,从而产生出另一个领域事件。所以策略是领域事件和领域事件之间的桥梁。
- 响应式逻辑
- 响应领域事件
- 触发命令
策略使用淡紫色便利贴。
外部系统
- 第三方服务
- 对当前领域来说是外部
外部系统使用粉红色便利贴。
事件风暴的步骤进行完了后,后面的步骤依次是:划分限界上下文,划分子域。
事件风暴的几个任务
Big Picture
事件风暴要描绘出整个领域的 Big Picture,也就是全景图。这种讨论往往横跨领域中各个配合部门。所以一般只会邀请各个团队的关键人员。在这种讨论中,会确定领域最核心的流程。同时也可以把最重要的子域或者限界上下文确定下来。
业务处理流程
明确业务处理流程。邀请具体负责各个业务的人来参与讨论。
软件设计
帮助我们进行软件设计。之前提到,找出聚合就是这个任务中的重要内容。这里需要研发人员参与讨论。
高效事件风暴的注意事项
- 首先关注学习和倾听
- 谈话和例子很关键
- 锚定到具体的业务用例
- 澄清模糊概念
正向驱动
反向驱动
实践中的问题和关键点
03 领域沟通和建模如何避免漏掉重要细节
深层模型
若开发人员识别出设计中隐含的某个概念或是在讨论中受到启发而发现一个概念时,就会对领域模型和相应的代码进行许多转换,在模型中加入一个或多个对象或关系,从而将此概念显式地表达出来。有时这种从隐式概念到显式概念的转换可能是一次突破,使我们得到一个深层模型。
漏掉的原因
Domain Storytelling 的建模以活动作为建模的核心,模型过于偏向业务
解决:不要单纯以角色的行为(活动)为中心进行沟通和建模
漏掉重要分支
解决:领域沟通过程中,研发人员发挥主动性
复杂系统难免漏掉细节
解决:借助场景走查,不断的对模型进行检验