01 DDD 小传
DDD(Domain-Driven Design)由 Eric Evans 于 2003 年提出,本质是一套用于开发复杂软件的系统化方法学与思想,核心关注“领域建模”。它源于面向对象方法学与敏捷开发,可理解为“把面向对象做对”(OO Done right),并将建模、设计与落地实践更体系化。
传统面向对象在企业应用中常见问题是重技术轻业务、领域建模难以掌握、协作与沟通不足、难以适应频繁变化。DDD 通过模式、原则与实践(如通用语言、限界上下文、模型驱动设计等)促进业务与技术协作沉淀知识,并用“柔性设计/演进”避免过度设计、支持持续重构。
近年因数字化带来更高复杂度与变化,以及敏捷/DevOps 与 Spring Boot、微服务、整洁架构、事件驱动、CQRS 等生态成熟,DDD 才从“屠龙术”变成广泛可用的方法。
02 迭代一 企业管理系统
企业为了满足管理诉求,希望员工每周在系统中填报自己在哪些项目上花了多少时间,也就是所谓的报工时。
| 需求 | 核心对象/范围 | 主要功能(CRUD/流程) | 关键规则/约束 | 关键字段/记录 |
|---|---|---|---|---|
| 多租户 | 租户(企业) | 管理多个租户(企业) | 每个租户代表一个使用 SaaS 的企业 | — |
| 人员与组织管理 | 部门、员工 | 部门:增删改查;员工:增删改查;员工分配部门 | 员工只能从属于一个部门 | 部门层级示例:开发中心 → 开发组;职能部门(人事/财务等) |
| 项目管理 | 客户、合同、项目 | 客户:增删改查;合同:增删改查 + 开始/结束;项目:增删改查 + 开始/结束 | 客户对应客户经理;合同对应销售;项目对应项目经理;合同下可有多个项目 | 合同/项目开始时间、结束时间等 |
| 人员分配 | 项目成员关系 | 为项目分配人员;人员退出项目 | 项目可多人;员工可同时参与多个项目;需记录投入比例 | 投入百分比(人-项目) |
| 工时登记 | 工时(周报/日记录) | 员工每周填报工时;查询;修改;填写备注 | 仅当员工已分配到项目后,才可在该项目报工时 | 日期、项目、投入时长、备注 |
03 事件风暴 识别领域事件
领域事件是什么
**领域事件(Domain Event)**用来表达:在业务流程的某一步完成后,所引发并被业务关心的“结果”。
常见表达方式有一个很实用的习惯:
- 完成时
- 被动语态
在 DDD 的各种命名中,优先使用约定俗成的业务术语,因为这些词本身就承载着业务知识与共识。
领域事件的边界:两条关键提醒
在识别领域事件时,有两点特别容易踩坑:
- 不要把技术事件当成领域事件。领域事件强调的是 业务上发生了什么,而不是 系统内部做了什么。
- 查询功能不算领域事件。查询行为通常不会改变业务世界的状态,因此一般 不构成领域事件。领域事件应当满足一个直觉标准:
- 对某样事物产生了影响
- 该影响是业务要记录/追踪的
- 或者它触发了对外通知(发消息给其他人或系统)
换句话说,领域事件通常对应“业务状态发生改变”或“业务需要对外告知”。
如何识别领域事件:先发散,再收敛
- 个人发散:参与者各自写出自己理解的领域事件(尽量不互相干扰)
- 集体收敛:一起讨论、对齐含义、合并同类项、澄清歧义
这种“先发散、后收敛、反复迭代”的方式,本质上就是一种结构化的头脑风暴:先把可能性铺开,再通过讨论把共识沉淀下来。
统一语言:事件风暴的隐性产出
在上述讨论过程中,其实已经在生成并强化 统一语言(Ubiquitous Language)。统一语言的核心是:
- 业务人员和开发人员使用同一套词。
- 同一个词在不同人心里对应同一个概念。
- 语言一致意味着领域理解一致(语言是知识的载体)。
它不是某个阶段的文档,而是贯穿 DDD 全过程的基础设施。
事件风暴的精髓是“协作”
事件风暴表面上是在贴事件、理流程,但真正关键的价值在于:
- 让不同角色共同参与。
- 通过讨论对齐概念与边界。
- 在协作中形成统一语言与共同理解。
04 事件风暴 识别命令和识别领域名词
命令是什么:从领域事件反推
命令 = 引发领域事件的操作。做法上可以理解为一条简单的逆向链路:
- 先有领域事件(结果)
- 再问:是谁做了什么,才让这个结果发生?
除了命令本身,事件风暴里往往还会顺手补齐两类“命令的上下文信息”:
- 执行者是谁?命令由谁发起/执行:可能是某个用户、某个岗位角色,也可能是系统内的某个领域对象在扮演某种角色,甚至与权限体系的角色有关。
- 执行命令前需要查询什么数据?为了执行该命令,需要先拿到哪些信息(用于校验、决策、填充表单、定位对象等)。
识别领域名词:从贴纸里提取“名词性概念”
这里的领域名词指:从命令、领域事件、执行者、查询数据中提取出来的名词性概念。举例理解:
- 命令:签订合同。被影响的核心名词:合同
- 领域事件:合同已签订。本质上是名词“合同”的状态发生变化,并被业务记录为一个重要结果。
领域规则如何保存:用“领域规则表”沉淀
领域规则是关键领域知识,必须可维护、可追溯。仅靠便利贴或图:
- 难长期保存
- 难版本化管理
- 难持续更新
更可行的方式是建立一个领域规则表:
- 将事件风暴阶段识别出的规则先记录进去
- 在后续领域建模中补充新规则
- 与领域模型放在一起,作为领域知识的核心资产共同维护
05 领域建模实践 上
从事件风暴产出的 领域名词 出发做领域建模:先将名词 暂定为实体,再梳理实体间的 关联类型(1:1 / 1:N / M:N);建模时持续做 抽象提炼 以发现 隐含实体、让模型更贴近业务本质;最后用 注释 与 约束 补齐业务知识,其中 约束必须落到代码或数据库 并纳入业务规则表。
06 领域建模实践 下
通过引入 关联实体 将 多对多 关系拆成两个 一对多,并为实体补齐必要的 业务操作;当模型复杂时,可按 模块拆分 来提升可理解性。领域模型完成后,还要抓住两项落地 DDD 的关键实践:补齐 业务规则、建立 词汇表(统一语言),它们是确保模型可执行、团队认知一致的重点。
07 领域建模原理
DDD 的 领域模型 是对领域知识的 提炼抽象,同时兼顾 业务表达 与 技术落地 ;在 模型驱动设计 下要做到 领域模型 ↔ 业务需求一致、系统实现 ↔ 领域模型一致,从而确保 实现 ↔ 需求一致;而 统一语言 以领域模型与词汇表为基础,通过 持续协作 不断维护 模型、语言与实现 三者的 一致性。
08 数据库设计
数据库三范式(3 Normal Form)。1NF 字段原子性:
- 表中的每一列都必须是不可再分的原子值。
- 不允许在一个字段里存多个值、列表或重复组。
例:主键是(订单ID, 商品ID),若订单日期只依赖订单ID,就不符合 2NF,应把订单信息拆到订单表。
2NF 消除对主键的部分依赖:
- 在满足 1NF 的基础上,要求非主属性必须完全依赖于整个主键。
- 主要针对联合主键的情况:不能只依赖主键的一部分。
例:主键是(订单ID, 商品ID),若订单日期只依赖订单ID,就不符合 2NF,应把订单信息拆到订单表。
3NF 消除传递依赖
- 在满足 2NF 的基础上,要求非主属性不依赖于其他非主属性(即没有传递依赖)。
- 非主属性应当直接依赖主键。
例:员工ID -> 部门ID -> 部门名称,则部门名称是通过部门ID间接决定的,应拆出部门表存部门ID, 部门名称。
DDD 强调用 领域模型驱动数据库设计,以保证 业务、代码、数据一致:实体→表、属性→字段(按需求补充),并通过 词汇表/统一语言 规范命名;关系设计上,一对多用外键(云上常见 “虚拟外键”),多对多用关联表;相较“拍脑袋”式建模,这种方式更容易 与业务对齐、更符合 范式,还能把 静态数据结构 与 动态业务逻辑 统一纳入同一套模型中。
09 分层架构
分层架构的原则是让不稳定的部分依赖稳定的部分:越靠内层越稳定、越靠外层越易变;其中领域层封装领域数据与规则,是系统核心,应用层作为门面把领域能力组织成粗粒度服务并处理事务、日志等横切关注点;外部交互由适配器层承担,主动适配器接收外部请求、被动适配器访问外部资源,二者只是在调用方向上不同,从而把技术细节隔离在外层,使应用层与领域层保持技术无关;数据访问上,仓库以聚合为单位(此处可暂按“一个实体一个仓库”理解),而 DAO 以表为单位;另设 common 层承载工具与框架,为各层提供支撑。
10 代码实现 要“贫血”还是要“充血”
贫血模型(Anemic Domain Model,Fowler 2003)指领域对象 只有数据没有行为,违背面向对象原则;相对的富/充血模型(Rich Domain Model)强调领域对象应同时包含数据 + 行为,更贴近真正的面向对象(可理解为贫血更偏 面向过程、富模型更偏 面向对象),但企业实践中两者并非非黑即白,应结合多种范式;落到编码,核心要求是 代码与模型一致(发现模型问题要 及时改模型),并解决 层间依赖:将 DTO 移到应用层、用 依赖倒置 让适配器层依赖领域层,同时理解 领域模型 vs 设计模型(UML) 的区别,以便明确哪些和业务讨论、哪些仅在工程内部讨论,从而提升抽象分层下的沟通与效率。
11 代码实现 创建领域对象、实现领域逻辑
实现领域逻辑时应使用能表达领域知识的命名,即 DDD 表意接口(Intention-Revealing Interfaces);一旦违背,常见坏味道是 函数过长 与 注释过多,可用 抽取函数 重构解决。需区分 领域逻辑 与 应用逻辑,关键在于是否包含领域专家关心的 领域知识;领域逻辑应优先放入 领域对象,不适合放入对象的则放入 领域服务。复杂对象的创建推荐用 工厂(Factory),参数少可直接调用工厂,参数多可配合 Builder。模块划分有按性质(横切)与按耦合/业务(纵切)两种,建议采用 先横后竖 的策略。
12 代码实现 更加“面向对象”
– EOF –