领域驱动设计 (DDD) 学习笔记
什么是 DDD?
领域驱动设计 (Domain-Driven Design, DDD) 是一种软件开发方法,通过将软件系统看作业务流程的反映,而非仅仅交付物。它更关注业务流程、业务术语和业务实践,技术关注点排在第二位。
核心思想:以纯净的领域模型来反映业务需求,代码即为需求文档。
培训资料
注:PDF 浏览览需要 HTML5 浏览器支持
为什么需要 DDD?
传统开发方式的问题
| 开发方式 | 特点 | 问题 |
|---|---|---|
| 瀑布式 | 预先设计,按阶段交付 | 领域知识单向流动,缺乏反馈 |
| 用例驱动 | 用例图描述需求 | 设计像事务脚本,功能重复 |
| 敏捷方法 | 小任务迭代,快速反馈 | 缺乏设计原则,可能过度设计 |
DDD 的区别
DDD 与其他方法论的本质区别:
传统方法:技术关注点 > 业务关注点
DDD 方法:业务关注点 > 技术关注点
DDD 将软件系统视为业务流程的反映
分层架构
DDD 将系统分为四层,职责清晰分离:
┌─────────────────────────────────────┐
│ 用户界面层 (User Interface) │
│ 负责与用户交互,展示信息 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 应用层 (Application) │
│ 组织任务,协调各方 │
│ 不包含业务规则,只作为协调作用 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 领域层 (Domain) ★ │
│ 表达业务概念及规则,软件的核心 │
│ 不包含任何与业务无关的逻辑 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 基础设施层 (Infrastructure) │
│ 为上层提供技术能力(持久化、AOP等) │
└─────────────────────────────────────┘
核心概念:通用语言 (Ubiquitous Language)
为什么需要通用语言?
领域专家和技术团队的工作性质不同,沟通时各自倾向于自己擅长的语言方式,削弱了交流效果。
通用语言的定义
通用语言是开发人员与领域专家之间的交流工具:
| 角色 | 使用方式 |
|---|---|
| 开发人员 | 描述系统中的工作、任务及功能 |
| 领域专家 | 讨论需求、开发计划等 |
关键原则:认识到对通用语言的更改就是对模型的更改。
UML vs 领域模型
| 对比项 | UML | 领域模型 |
|---|---|---|
| 用途 | 沟通解释手段 | 持久化的代码 |
| 完整性 | 不可能完全描述所有需求 | 代码即需求文档 |
| 同步成本 | 巨大 | 自动同步 |
| 定位 | 抛弃型产物 | 核心资产 |
UML 属于分析过程中的产物,相对适合表述核心业务,但不应追求完全描述。
什么是模型?
模型的特征
| 特征 | 说明 |
|---|---|
| 🧩 多部分组成 | 由不同部分组合而成 |
| 🎯 特定目的 | 用于特定目的 |
| 📐 抽象系统 | 对现实的抽象 |
| 🧠 认知工具 | 帮助理解和思考 |
| 📝 多种表示 | 语言、代码、图解等 |
| 🔢 多个共存 | 一个系统包含若干模型 |
新旧模型的区别
| 旧模型 | DDD 新模型 |
|---|---|
| 关注基础架构和技术概念 | 完全集中在核心领域 |
| 关注技术实现 | 关注领域概念和问题 |
战术设计:领域模型元素
1. 实体 (Entity)
特征:具有全局唯一标识
传统数据对象 → 贫血对象 (POCO) → 充血对象 (DDD Entity)
2. 值对象 (Value Object)
定义:没有唯一标识,通过属性值判断相等性
举例:地址
| 场景 | 类型 | 原因 |
|---|---|---|
| 两个室友订购 Vancl 货物 | Value Object | 是否在同一地点不重要 |
| 两人申请宽带服务 | Entity | 运营商需要区分同一地点 |
3. 聚合 (Aggregate)
定义:一组相关对象的集合,作为数据修改的单元
聚合根 (Aggregate Root) 的规则:
| 规则 | 说明 |
|---|---|
| 🎯 全局标识 | 根具有全局唯一标识 |
| 🔒 本地标识 | 根内部实体具有本地标识(根内唯一) |
| 🚫 外部隔离 | 根的外部不得引用其内部对象 |
| 📤 临时引用 | 只能通过根传递,临时使用或值对象副本 |
| 🔍 查询限制 | 只有根可直接通过数据库查询 |
| 🔗 根间引用 | 根内部可保持对其他根的引用 |
| 🗑️ 级联删除 | 父子对象关系,删除根时级联删除 |
| ✅ 约束完整性 | 提交修改时,整个根的所有规则必须通过 |
4. 领域服务 (Domain Service)
何时使用:当重要的进程或转换操作不属于任何实体或值对象时
// 领域服务示例
public class TransferService
{
public void TransferMoney(Account from, Account to, Money amount)
{
// 这个操作不属于单个 Account
// 涉及多个实体协作
}
}
设计原则:
| ✅ 应该 | ❌ 避免 |
|---|---|
| 具有领域意义 | 把所有功能都放在服务中 |
| 无状态 | 让实体变成贫血对象 |
| 跨实体协作 | 领域职责与领域主体分离 |
警告:服务是容易被滥用的概念。贫血模型把所有功能放在服务中,导致代码充斥着方法拷贝。
5. 工厂 (Factory) 和仓储 (Repository)
| 模式 | 职责 | 逻辑类型 |
|---|---|---|
| 工厂 | 创建复杂对象 | 全是领域逻辑 |
| 仓储 | 持久化和查询 | 全是数据访问逻辑 |
协作方式:
工厂创建对象 → 仓储持久化对象
仓储读取数据 → 工厂重建领域对象
柔性设计 (Supple Design)
设计目标
问题:复杂软件缺乏良好设计时,重构和组合变得困难
目的:使设计让人们乐于使用,而且易于作出修改
定义:柔性设计是一些具体模式,能够揭示深层次的底层模型,指导突破复杂性限制。
1. 释意接口 (Intention-Revealing Interfaces)
原则:命名类和操作时要描述效果和目的,而不是实现方式
| ❌ 不好的命名 | ✅ 好的命名 |
|---|---|
process() |
calculateTotalPrice() |
doIt() |
validateOrder() |
handle() |
sendEmailNotification() |
好处:
- 客户开发人员不必理解内部细节
- 与通用语言保持一致
- TDD 可促使站在客户角度思考
2. 无副作用的函数 (Side-Effect-Free Function)
问题:嵌套调用难以预测结果,产生意外副作用
解决方案:
| 操作类型 | 特点 | 示例 |
|---|---|---|
| 函数 | 只返回结果,无副作用 | calculateTax(amount) |
| 命令 | 引起状态改变,不返回信息 | saveOrder(order) |
原则:尽可能把逻辑放到函数中,把命令隔离到简单操作中
3. 断言 (Assertion)
定义:明确表述操作的后置条件和类/Aggregate 的规则
契约式设计:
| 断言类型 | 说明 |
|---|---|
| 后置条件 | 描述操作的副作用和结果 |
| 前置条件 | 表明需要满足的条件 |
| 固定规则 | 规定结束时对象的状态 |
实现方式:
- 编程语言直接编写断言
- 自动化单元测试
- 文档或图
4. 概念轮廓 (Conceptual Contour)
原则:把设计元素分解为内聚的单元,与领域中的重要划分相匹配
| 两种极端 | 问题 |
|---|---|
| 所有元素在一个大结构 | 功能重复,意义难懂 |
| 过度分解 | 更复杂,概念可能丢失 |
目标:得到一组可逻辑组合的简单接口
5. 孤立的类 (Standalone Class)
定义:低耦合的极致,类完全孤立,可单独研究和理解
好处:
- 减轻理解模块的负担
- 低耦合是减少概念过载的基本办法
实现:把复杂计算提取到独立的值对象中
6. 闭合操作 (Closure Of Operation)
定义:操作的返回类型与参数类型相同
示例:
// 闭合操作
public Money Add(Money other) { ... }
public Money Multiply(decimal factor) { ... }
// 非闭合操作
public string ToString() { ... }
好处:提供高层接口,不引入其他概念依赖
领域特定语言 (DSL)
什么是 DSL?
语言是一套共同采用的沟通符号、表达方式与处理规则。
DSL (Domain Specific Language) 是针对特定领域的语言。
DDD + DSL
通过 DSL 的方式来调整业务:
- 将通用语言转化为可执行的 DSL
- 代码即为业务规则
- 业务专家可以直接”阅读”代码
总结
DDD 的核心价值
| 价值 | 说明 |
|---|---|
| 🎯 业务优先 | 技术服务于业务 |
| 🗣️ 统一语言 | 开发与业务同频 |
| 📐 清晰分层 | 职责明确分离 |
| 🔧 柔性设计 | 易于维护和扩展 |
DDD vs 传统方法
传统:业务需求 → 技术设计 → 代码实现 → 需求丢失
DDD: 业务需求 → 领域模型 → 代码实现 = 需求文档
推荐阅读
- 《领域驱动设计》- Eric Evans
- 《实现领域驱动设计》- Vaughn Vernon
- 《领域驱动设计精粹》- Scott Millett
持续学习,持续实践! 📚