01 DDD 小传

DDD(Domain-Driven Design)由 Eric Evans 于 2003 年提出,本质是一套用于开发复杂软件的系统化方法学与思想,核心关注“领域建模”。它源于面向对象方法学与敏捷开发,可理解为“把面向对象做对”(OO Done right),并将建模、设计与落地实践更体系化。

传统面向对象在企业应用中常见问题是重技术轻业务、领域建模难以掌握、协作与沟通不足、难以适应频繁变化。DDD 通过模式、原则与实践(如通用语言、限界上下文、模型驱动设计等)促进业务与技术协作沉淀知识,并用“柔性设计/演进”避免过度设计、支持持续重构。

近年因数字化带来更高复杂度与变化,以及敏捷/DevOps 与 Spring Boot、微服务、整洁架构、事件驱动、CQRS 等生态成熟,DDD 才从“屠龙术”变成广泛可用的方法。

02 迭代一 企业管理系统

企业为了满足管理诉求,希望员工每周在系统中填报自己在哪些项目上花了多少时间,也就是所谓的报工时。

需求核心对象/范围主要功能(CRUD/流程)关键规则/约束关键字段/记录
多租户租户(企业)管理多个租户(企业)每个租户代表一个使用 SaaS 的企业
人员与组织管理部门、员工部门:增删改查;员工:增删改查;员工分配部门员工只能从属于一个部门部门层级示例:开发中心 → 开发组;职能部门(人事/财务等)
项目管理客户、合同、项目客户:增删改查;合同:增删改查 + 开始/结束;项目:增删改查 + 开始/结束客户对应客户经理;合同对应销售;项目对应项目经理;合同下可有多个项目合同/项目开始时间、结束时间等
人员分配项目成员关系为项目分配人员;人员退出项目项目可多人;员工可同时参与多个项目;需记录投入比例投入百分比(人-项目)
工时登记工时(周报/日记录)员工每周填报工时;查询;修改;填写备注仅当员工已分配到项目后,才可在该项目报工时日期、项目、投入时长、备注

image

03 事件风暴 识别领域事件

image

领域事件是什么

**领域事件(Domain Event)**用来表达:在业务流程的某一步完成后,所引发并被业务关心的“结果”。

常见表达方式有一个很实用的习惯:

  1. 完成时
  2. 被动语态

在 DDD 的各种命名中,优先使用约定俗成的业务术语,因为这些词本身就承载着业务知识与共识。

image

领域事件的边界:两条关键提醒

在识别领域事件时,有两点特别容易踩坑:

  1. 不要把技术事件当成领域事件。领域事件强调的是 业务上发生了什么,而不是 系统内部做了什么
  2. 查询功能不算领域事件。查询行为通常不会改变业务世界的状态,因此一般 不构成领域事件。领域事件应当满足一个直觉标准:
    1. 对某样事物产生了影响
    2. 该影响是业务要记录/追踪的
    3. 或者它触发了对外通知(发消息给其他人或系统)

换句话说,领域事件通常对应“业务状态发生改变”或“业务需要对外告知”。

如何识别领域事件:先发散,再收敛

  1. 个人发散:参与者各自写出自己理解的领域事件(尽量不互相干扰)
  2. 集体收敛:一起讨论、对齐含义、合并同类项、澄清歧义

这种“先发散、后收敛、反复迭代”的方式,本质上就是一种结构化的头脑风暴:先把可能性铺开,再通过讨论把共识沉淀下来。

统一语言:事件风暴的隐性产出

在上述讨论过程中,其实已经在生成并强化 统一语言(Ubiquitous Language)。统一语言的核心是:

  • 业务人员和开发人员使用同一套词。
  • 同一个词在不同人心里对应同一个概念。
  • 语言一致意味着领域理解一致(语言是知识的载体)。

它不是某个阶段的文档,而是贯穿 DDD 全过程的基础设施。

事件风暴的精髓是“协作”

事件风暴表面上是在贴事件、理流程,但真正关键的价值在于:

  • 让不同角色共同参与。
  • 通过讨论对齐概念与边界。
  • 在协作中形成统一语言与共同理解。

04 事件风暴 识别命令和识别领域名词

命令是什么:从领域事件反推

命令 = 引发领域事件的操作。做法上可以理解为一条简单的逆向链路:

  1. 先有领域事件(结果)
  2. 再问:是谁做了什么,才让这个结果发生?

除了命令本身,事件风暴里往往还会顺手补齐两类“命令的上下文信息”:

  • 执行者是谁?命令由谁发起/执行:可能是某个用户、某个岗位角色,也可能是系统内的某个领域对象在扮演某种角色,甚至与权限体系的角色有关。
  • 执行命令前需要查询什么数据?为了执行该命令,需要先拿到哪些信息(用于校验、决策、填充表单、定位对象等)。

识别领域名词:从贴纸里提取“名词性概念”

这里的领域名词指:从命令、领域事件、执行者、查询数据中提取出来的名词性概念。举例理解:

  • 命令:签订合同。被影响的核心名词:合同
  • 领域事件:合同已签订。本质上是名词“合同”的状态发生变化,并被业务记录为一个重要结果。

领域规则如何保存:用“领域规则表”沉淀

领域规则是关键领域知识,必须可维护、可追溯。仅靠便利贴或图:

  • 难长期保存
  • 难版本化管理
  • 难持续更新

更可行的方式是建立一个领域规则表:

  • 将事件风暴阶段识别出的规则先记录进去
  • 在后续领域建模中补充新规则
  • 与领域模型放在一起,作为领域知识的核心资产共同维护

05 领域建模实践 上

从事件风暴产出的 领域名词 出发做领域建模:先将名词 暂定为实体,再梳理实体间的 关联类型(1:1 / 1:N / M:N);建模时持续做 抽象提炼 以发现 隐含实体、让模型更贴近业务本质;最后用 注释约束 补齐业务知识,其中 约束必须落到代码或数据库 并纳入业务规则表

Image

06 领域建模实践 下

通过引入 关联实体多对多 关系拆成两个 一对多,并为实体补齐必要的 业务操作;当模型复杂时,可按 模块拆分 来提升可理解性。领域模型完成后,还要抓住两项落地 DDD 的关键实践:补齐 业务规则、建立 词汇表(统一语言),它们是确保模型可执行、团队认知一致的重点。

Image

07 领域建模原理

DDD 的 领域模型 是对领域知识的 提炼抽象,同时兼顾 业务表达技术落地 ;在 模型驱动设计 下要做到 领域模型 ↔ 业务需求一致系统实现 ↔ 领域模型一致,从而确保 实现 ↔ 需求一致;而 统一语言 以领域模型与词汇表为基础,通过 持续协作 不断维护 模型、语言与实现 三者的 一致性

08 数据库设计

数据库三范式(3 Normal Form)。1NF 字段原子性:

  1. 表中的每一列都必须是不可再分的原子值。
  2. 不允许在一个字段里存多个值、列表或重复组。

例:主键是(订单ID, 商品ID),若订单日期只依赖订单ID,就不符合 2NF,应把订单信息拆到订单表。

2NF 消除对主键的部分依赖:

  1. 在满足 1NF 的基础上,要求非主属性必须完全依赖于整个主键。
  2. 主要针对联合主键的情况:不能只依赖主键的一部分。

例:主键是(订单ID, 商品ID),若订单日期只依赖订单ID,就不符合 2NF,应把订单信息拆到订单表。

3NF 消除传递依赖

  1. 在满足 2NF 的基础上,要求非主属性不依赖于其他非主属性(即没有传递依赖)。
  2. 非主属性应当直接依赖主键。

例:员工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 –