关于 domain-driven-design
面向软件设计者的领域驱动设计框架,帮助把业务概念和规则直接反映在代码模型中。内容涵盖战略设计与建模构件,并提供诊断评分来检查模型质量。
主要功能
- 01统一语言与限界上下文
- 02实体、值对象与聚合
- 03领域事件与仓储
- 04工厂与规格模式
- 05子域识别和模型评分
适用场景
- 01为新系统定义领域模型
- 02划分复杂系统的模型边界
- 03评审代码中的业务语言一致性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wondelai/ skills --skill domain-driven-design安装后在 Claude Code 中输入/domain-driven-design确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/bounded-contexts.mdbuilding-blocks.mddomain-events.mdrepositories-factories.mdstrategic-design.mdubiquitous-language.md详细介绍
领域驱动设计框架
通过围绕业务领域建模代码来应对软件复杂性的框架。软件最大的风险并非技术故障,而是构建出无法反映业务实际运作方式的模型。
核心原则
模型就是代码;代码就是模型。 软件应体现对业务领域深入且共同的理解。当领域专家和开发者使用同一种语言,并将这门语言直接体现在代码库中时,复杂性就变得可控,系统也能随着业务变化而平稳演进。
评分
目标:10/10。 根据快速诊断中的每项(共 7 项)计 1 分,此外,深度部分最多加 3 分:如果核心领域有真正丰富的模型(而非只有 CRUD),加 1 分;如果不变量位于聚合内部,而非服务中,加 1 分;如果通用语言在交流、代码和测试中保持一致,加 1 分。评分区间:9-10 = 名称清晰易懂,显式划分上下文边界并设有 ACL,聚合规模小,实体富含行为,通过事件实现跨聚合流程,并已识别核心领域;5-6 = 有一些领域语言,但边界泄漏或对象贫血;<=3 = 使用技术性命名,一个模型用于所有场景,逻辑散落在服务中。报告分数及未通过的具体诊断项。
框架
1. 通用语言
核心概念: 开发者与领域专家之间共享且严谨的语言,并在交流、文档和代码中始终如一地使用。当语言发生变化时,代码也随之变化——代码中生硬的命名也会反过来推动语言改进。
有效原因: 歧义是大多数建模失败的根源。当开发者说“订单”,而专家指的是“采购申请”时,错误就难以避免;通用语言会促使代码中的每个名称都对应业务认可并验证过的概念。
关键见解:
- 语言源于深入协作,而非事后强行附加的术语表
- 如果某个概念难以命名,模型可能有误——命名困难是设计信号
- 技术术语(
DataProcessor相比ClaimAdjudicator)会让领域逻辑对本可纠正它的专家隐而不见 - 不同限界上下文可以用同一个词表达不同含义——这完全没问题
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 类/方法命名 | 根据领域概念和动词命名 | LoanApplication、policy.underwrite()——而非 RequestHandler、process() |
| 模块结构 | 按领域概念组织 | shipping/、billing/——而非 controllers/、services/ |
| 代码审查 | 拒绝纯技术性名称 | 将 Manager、Helper、Processor、Utils 标记为命名异味 |
另请参阅:references/ubiquitous-language.md,在开展建模会议或维护术语表时使用——其中介绍了语言如何演进并反馈到代码中。
2. 有界上下文与上下文映射
核心概念: 有界上下文是一个明确的边界,在此边界内适用特定的领域模型。同一个词(“Customer”)在不同上下文中可能有不同含义;上下文映射定义了它们之间的关系和转换策略。
为何有效: 试图维护单一统一模型的大型系统最终难免陷入不一致。有界上下文承认业务的不同部分需要不同的模型;上下文映射负责管理它们之间的集成。
关键见解:
- 有界上下文不是微服务——它是一个语言和模型边界,可能包含多个服务
- 上下文边界通常与团队边界一致(康威定律)
- 九种上下文映射模式描述了团队之间的政治和技术关系
- 防腐层是最重要的防御模式——绝不能让外部模型渗入核心领域
- 共享内核会耦合两个团队;应保持其精简,并明确治理方式
- 先映射现状(大泥球),再定义目标边界
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 服务集成 | 防腐层 | 在边界处将外部 API 响应转换为领域对象 |
| 遗留系统迁移 | 遵奉者 / ACL | 在遗留系统外封装一个适配器,使其使用领域语言进行交互 |
| API 设计 | 开放主机服务 + 已发布语言 | 提供文档完善、采用规范架构的 REST API |
参见:references/bounded-contexts.md,了解九种映射模式和集成策略。
3. 实体、值对象与聚合
核心概念: 实体具有跨状态变化而持续存在的标识。值对象完全由其属性定义,且不可变。聚合是由实体和值对象组成的簇,具有一个用于维护一致性边界的单一根对象。
为何有效: 如果不区分这些概念,所有东西都会变成可变且带有标识的对象——状态纠缠、更新不一致、并发脆弱。聚合划定了界限:内部的一切都保证一致;外部的一切最终会达到一致。
关键见解:
- 实体测试:“即使我的所有属性都变了,我还是同一个东西吗?”(一个人改了姓名和地址——仍然是同一个人)
- 值对象测试:“我是否仅由自己的属性定义?”(任何一张 10 美元钞票都可以与另一张互换)
- 大多数东西都应该是值对象,而不是实体——优先选择不可变性
- 保持聚合精简(一个根对象加上最少的成员);通过 ID 引用其他聚合,而不是使用对象引用
- 只在聚合内部保证即时一致性;聚合之间应按最终一致性来设计
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 标识跟踪 | 带 ID 的实体 | 由 orderId 标识的 Order,在状态变化后仍然存在 |
| 不可变属性 | 值对象 | Address(street, city, zip)——替换,不要修改 |
| 一致性边界 | 聚合根 | Order 是根;OrderLine 项只能通过它存在 |
| 并发控制 | 对根对象使用乐观锁 | Order 上的版本字段;两个编辑操作发生竞争时会产生冲突 |
参见:references/building-blocks.md,了解聚合设计规则和一致性边界。
4. 领域事件
核心概念: 领域事件记录领域中专家关心的已发生之事,名称使用过去时(OrderPlaced、PaymentReceived)——即已经发生的事实。
为什么有效: 领域事件将原因与结果解耦。发布 OrderPlaced 后,配送、计费和通知会各自独立响应,而无需让订单上下文了解它们——耦合更少、最终一致性,以及自然形成的审计轨迹。
关键见解:
- 事件是不可变的事实——一经发布,就不能更改或撤回
- 领域事件仅限于有界上下文内部;集成事件则跨越上下文边界
- 事件支持时间解耦:生产者无需等待消费者
- 事件溯源将完整的事件历史作为事实来源,通过重放推导当前状态
- 并非每次状态变更都值得发布事件——只发布领域关心的内容
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 状态转换 | 在领域操作时引发事件 | order.place() 引发 OrderPlaced |
| 跨上下文集成 | 发布集成事件 | OrderPlaced 在配送上下文中触发 ShippingLabelRequested |
| 最终一致性 | 异步事件处理器 | 库存处理器在 OrderPlaced 后异步更新库存 |
参见:references/domain-events.md,了解事件命名、事件溯源和集成事件。
5. 仓储与工厂
核心概念: 仓储提供内存中领域对象集合的假象,并隐藏持久化细节。工厂封装复杂的创建逻辑,确保聚合始终以有效状态创建。
为什么有效: 当持久化和组装细节泄漏到领域代码中时,每次存储变更都会波及业务规则,而且聚合可能在半有效状态下被构造。仓储将 SQL/ORM 相关内容限制在基础设施层,使领域代码能够在内存中进行测试;工厂确保创建聚合的唯一途径能够强制执行其不变条件,因此无法表示无效实例。
关键见解:
- Repository 接口属于领域层;其实现属于基础设施层
- 仓储方法使用通用语言:
findPendingOrders(),而不是getByStatusCode(3) - 面向集合的仓储模拟
add/remove;面向持久化的仓储则使用save - 复杂规则或多部分组装适合使用工厂;只有两个字段的值对象只需一个构造函数
- Specification 模式将查询条件封装为领域对象:
OverdueInvoiceSpecification
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 数据访问抽象 | Repository 接口 | 领域层中的 OrderRepository.findByCustomer(customerId);基础设施层中的 PostgresOrderRepository |
| 复杂创建 | 工厂方法 | Order.createFromQuote(quote) 验证并从 Quote 聚合组装 |
| 查询封装 | Specification | spec = OverdueBy(days=30); repo.findMatching(spec) |
参见:references/repositories-factories.md,了解 Repository、Factory 和 Specification 模式。
6. 战略设计与提炼
核心概念: 系统的各个部分并非同等重要。战略设计旨在识别核心域——竞争优势所在之处,并将其与支撑子域(必不可少,但不构成差异化)和通用子域(商品化功能)区分开来。
有效原因: 在所有领域都采取同样严格的做法,会分散最优秀人才的精力,还会对商品化功能过度设计。识别核心域,可以将最优秀的开发人员和最深入的建模集中在最重要的地方。
关键要点:
- 核心域:投入最优秀的人才,进行最深入的建模;支撑子域:需要构建,但不要过度设计;通用子域(身份验证、电子邮件、支付):购买或使用开源方案
- 提炼可以从周边复杂性中提取并突出核心域
- 领域愿景声明是一页纸的说明,描述核心域的价值主张
- 随着业务发展,重新审视哪些部分属于“核心”——今天的差异化优势可能成为明天的商品化功能
代码应用:
| 场景 | 模式 | 示例 |
|---|---|---|
| 自建还是购买 | 对子域类型进行分类 | 自建定价引擎(核心域);使用 Stripe 处理支付(通用子域) |
| 团队分配 | 让最优秀的开发人员负责核心域 | 资深开发人员为核保规则建模;初级开发人员集成电子邮件服务 |
| 代码组织 | 将核心与通用部分分开 | domain/pricing/(深入的模型)与 infrastructure/email/(精简的适配器) |
请参阅:references/strategic-design.md,了解如何决定工程投入方向——子域分类和提炼技术。
常见错误
| 错误 | 失败原因 | 修正方法 |
|---|---|---|
| 使用技术名称而非领域语言 | 逻辑隐藏在 DataManager 后面;领域专家无法验证模型 | 重命名为领域术语(ClaimAdjudicator);如果没有对应的领域术语,概念可能有误 |
| 一个模型统管所有领域 | 用于计费、配送和营销的单个 Customer 类会变得臃肿且自相矛盾 | 使用限界上下文:每个上下文都有自己的 Customer,只包含所需属性 |
| 聚合过大 | 导致并发冲突、加载缓慢和事务瓶颈 | 保持聚合精简;通过 ID 引用;在聚合之间采用最终一致性 |
| 贫血领域模型 | 对象只是数据袋;规则散落在各个服务中并重复出现 | 将行为放入实体和值对象;服务只负责协调 |
| 没有防腐层 | 外部模型渗入系统;代码与外部架构耦合 | 将每个外部系统都封装在转换层之后 |
| 把限界上下文等同于微服务 | 过早拆分;引入分布式复杂性却没有收益 | 上下文是模型边界,而非部署单元;从单体中的模块开始 |
| 跳过领域专家 | 开发人员凭空设计出与现实不符的模型;导致昂贵的返工 | 定期开展建模会议,直到专家表示“是的,实际运作方式就是这样” |
快速诊断
| 问题 | 如果否 | 行动 |
|---|---|---|
| 领域专家能否读懂你的类名并理解其含义? | 技术术语掩盖了模型 | 将类、方法和事件重命名为通用语言 |
| 是否明确定义了限界上下文的边界? | 模型相互渗透;同一术语含义不同 | 绘制上下文映射;定义边界和翻译方式 |
| 聚合是否足够小(一个根对象 + 最小集群)? | 加载缓慢、出现并发问题 | 拆分聚合;通过 ID 引用;接受最终一致性 |
| 领域对象是否包含行为,而不只是数据? | 贫血模型;逻辑散落在各个服务中 | 将业务规则移入实体和值对象 |
| 是否使用领域事件进行跨聚合通信? | 耦合紧密、形成同步调用链 | 引入事件;让聚合异步响应 |
| 每个外部集成处是否都有防腐层? | 外部模型污染了你的领域 | 在每个边界添加翻译层 |
| 是否已确定哪个子领域是核心? | 最优秀的人才分散各处 | 对子领域分类;将深入建模的重点放在核心领域 |
延伸阅读
如需了解完整的方法论、模式和更深入的见解:
- 《领域驱动设计:软件核心复杂性应对之道》,作者 Eric Evans
关于作者
Eric Evans 是一位软件设计顾问,也是领域驱动设计的创始人。他在金融、保险和物流领域参与大型系统开发期间提出了这一设计方法。他于 2003 年出版的著作 《领域驱动设计:软件核心复杂性应对之道》 是有史以来最具影响力的软件架构书籍之一;他也通过自己的咨询公司 Domain Language 持续推动 DDD 的发展。