mcpField
D
domain-driven-design

domain-driven-design

github.com/wondelai开发2.5k7.2k 次安装7 周前更新
代码仓库

关于 domain-driven-design

面向软件设计者的领域驱动设计框架,帮助把业务概念和规则直接反映在代码模型中。内容涵盖战略设计与建模构件,并提供诊断评分来检查模型质量。

主要功能

  1. 01统一语言与限界上下文
  2. 02实体、值对象与聚合
  3. 03领域事件与仓储
  4. 04工厂与规格模式
  5. 05子域识别和模型评分

适用场景

  1. 01为新系统定义领域模型
  2. 02划分复杂系统的模型边界
  3. 03评审代码中的业务语言一致性

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wondelai/skills --skill domain-driven-design

安装后在 Claude Code 中输入/domain-driven-design确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
bounded-contexts.md
building-blocks.md
domain-events.md
repositories-factories.md
strategic-design.md
ubiquitous-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 聚合组装
查询封装Specificationspec = 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 是一位软件设计顾问,也是领域驱动设计的创始人。他在金融、保险和物流领域参与大型系统开发期间提出了这一设计方法。他于 2003 年出版的著作 《领域驱动设计:软件核心复杂性应对之道》 是有史以来最具影响力的软件架构书籍之一;他也通过自己的咨询公司 Domain Language 持续推动 DDD 的发展。

评论

支持 Markdown