软件项目失败不是因为糟糕的代码,而是因为误解的需求。开发者构建他们认为业务需要的东西。业务利益相关者描述他们认为技术上可行的东西。业务语言与技术实现之间的鸿沟造成摩擦、延迟,以及解决错误问题的系统。
传统软件开发将数据库视为宇宙的中心。设计从数据表和关联开始。业务逻辑散落在存储过程、服务层和 UI 代码中。领域——核心业务问题——成为事后想法,埋藏在技术关注点之下。
领域驱动设计(DDD)颠覆了这种方法。它将领域模型置于中心,将业务逻辑视为系统最重要的部分。技术关注点——数据库、框架、UI——成为服务领域的实现细节。业务和开发团队使用直接出现在代码中的共享语言进行协作。
这种转变听起来简单,但需要团队对软件思考方式的根本改变。DDD 引入了建模复杂业务逻辑的模式、管理大型系统的策略,以及保持代码与业务需求一致的实践。理解 DDD 何时增加价值——以及何时更简单的方法就足够——决定了它是成为强大的工具还是过度工程的负担。
本文追溯从数据库中心到领域中心设计的演进,探索 DDD 的核心模式和实践,检视真实世界的应用,并提供何时采用这种方法的指导。
领域驱动设计时间轴
数据库中心的问题
在 DDD 之前,大多数企业应用程序遵循数据库中心的方法,造成了根本性的问题。
传统数据库优先设计
典型的开发流程从数据库开始:
🚫 数据库中心的问题
设计流程
- 从数据库架构开始
- 创建数据表和关联
- 生成数据访问代码
- 在上面添加业务逻辑
问题
- 数据库结构驱动设计
- 业务逻辑散落各处
- 贫血领域模型(只有 getter/setter)
- 技术关注点主导
后果
- 代码不反映业务概念
- 变更需要数据库迁移
- 业务规则隐藏在多个层次中
- 难以理解和维护
在这种方法中,开发者首先设计规范化的数据库数据表。对象关系映射(ORM)工具从数据表生成类。业务逻辑被添加到任何方便的地方——存储过程、服务层、控制器或 UI 代码。结果系统没有清晰的业务概念表示。
典型的电子商务系统可能有 Order、OrderItem 和 Customer 数据表。Order 类成为具有 getter 和 setter 的数据容器。像"订单超过 $100 免运费"这样的业务规则散落在代码库中。找到业务规则的实现位置需要搜索多个文件。
贫血领域模型反模式
数据库中心设计产生贫血领域模型:
🚫 贫血领域模型
特征
- 只有属性的类
- 领域对象中没有业务逻辑
- 服务包含所有行为
- 对象只是数据容器
示例
public class Order {
private Long id;
private List<OrderItem> items;
private BigDecimal total;
// 只有 getter 和 setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// ... 更多 getter/setter
}
**为什么有问题**
- 违反面向对象原则
- 业务逻辑与数据分离
- 难以维护不变性
- 没有封装
贫血模型将对象视为数据结构而非行为实体。所有业务逻辑都存在于操作这些数据容器的服务类中。这种伪装成面向对象代码的过程式方法使系统更难理解和维护。
沟通鸿沟
数据库中心设计扩大了业务与开发之间的鸿沟:
🚫 语言断层
业务视角
- "客户下订单"
- "订单可以在发货前取消"
- "优质客户获得优先处理"
代码现实
- OrderService.createOrder()
- OrderRepository.updateStatus()
- CustomerTable.premiumFlag
结果
- 业务概念在代码中不可见
- 开发者在语言之间翻译
- 误解累积
- 知识随时间流失
业务利益相关者使用业务术语描述领域。开发者使用技术术语实现。这些语言之间的翻译引入错误,并使代码库对非开发者来说难以理解。
领域驱动设计基础
Eric Evans 2003 年的书《领域驱动设计》引入了一种处理复杂性的综合方法。
核心哲学
DDD 的基础建立在几个关键原则上:
🎯 DDD 核心原则
领域优先
- 业务逻辑是最重要的部分
- 技术关注点服务领域
- 模型反映业务现实
- 代码说业务语言
通用语言
- 业务与开发者之间的共享词汇
- 对话和代码中使用相同术语
- 减少翻译错误
- 随理解演进
迭代建模
- 模型通过协作改进
- 重构以获得更深入的洞察
- 持续学习
- 代码和模型保持一致
DDD 将领域模型视为系统的核心。其他一切——数据库、UI、外部服务——都是为了支持领域而存在。这种优先级的颠倒改变了团队处理设计的方式。
通用语言
最基本的 DDD 实践是创建共享语言:
✅ 通用语言的好处
它是什么
- 领域的共同词汇
- 团队中每个人都使用
- 直接出现在代码中
- 在模型中记录
如何运作
- 业务:"客户下订单"
- 代码:
customer.placeOrder() - 不需要翻译
- 立即理解
影响
- 减少误解
- 使代码自我记录
- 使业务能够阅读代码结构
- 揭示建模问题
当业务利益相关者说"下订单"时,代码有一个 placeOrder() 方法。当他们讨论"运输政策"时,代码有一个 ShippingPolicy 类。会议中的语言与代码中的语言相符。
这种一致性具有深远的影响。开发者停止在业务和技术术语之间翻译。业务利益相关者可以审查类图并理解系统结构。业务理解与代码实现之间的不匹配立即变得可见。
丰富领域模型
DDD 提倡具有行为的丰富领域模型:
✅ 丰富领域模型
特征
- 对象包含数据和行为
- 业务规则存在于领域对象中
- 封装保护不变性
- 表达性、揭示意图的方法
示例
public class Order {
private OrderId id;
private List<OrderLine> lines;
private OrderStatus status;
public void addLine(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException(
"无法修改已提交的订单");
}
lines.add(new OrderLine(product, quantity));
}
public Money calculateTotal() {
return lines.stream()
.map(OrderLine::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
**好处**
- 业务逻辑集中
- 不变性得到强制执行
- 自我记录的代码
- 更容易测试和维护
丰富模型将业务规则封装在领域对象中。Order 类知道如何添加项目、计算总额和强制执行业务约束。业务逻辑不会散落在服务层中——它存在于应该存在的地方。
战略设计模式
DDD 提供战略模式来管理大型系统中的复杂性。
限界上下文
最重要的战略模式是限界上下文:
🎯 限界上下文概念
定义
- 模型的明确边界
- 在边界内,术语具有精确含义
- 不同上下文可以有不同模型
- 通过分离减少复杂性
为什么重要
- "客户"在不同上下文中意味着不同的事情
- 销售上下文:客户有订单、信用额度
- 支持上下文:客户有工单、历史记录
- 运输上下文:客户有送货地址
好处
- 每个上下文保持专注
- 团队可以独立工作
- 模型保持连贯
- 防止"一个模型统治一切"
大型系统无法拥有单一统一模型。"客户"一词对销售、支持和运输团队意味着不同的事情。试图创建一个满足所有上下文的 Customer 类会产生臃肿、不连贯的模型。
限界上下文通过明确分离模型来解决这个问题。每个上下文都有自己针对其需求优化的模型。销售上下文有一个具有订单历史的 Customer。支持上下文有一个具有支持工单的 Customer。这些是不同的模型,这没问题。
- 订单
- 信用额度
- 付款条件] end subgraph Support["支持上下文"] SuC[客户
- 工单
- 支持历史
- 优先级] end subgraph Shipping["运输上下文"] ShC[客户
- 送货地址
- 运输偏好
- 配送历史] end Sales -.->|上下文映射| Support Sales -.->|上下文映射| Shipping Support -.->|上下文映射| Shipping style Sales fill:#e1f5ff,stroke:#333,stroke-width:2px style Support fill:#fff4e1,stroke:#333,stroke-width:2px style Shipping fill:#e8f5e9,stroke:#333,stroke-width:2px
上下文映射
限界上下文必须集成,需要上下文映射:
🗺️ 上下文映射模式
合作伙伴
- 两个上下文紧密协作
- 团队协调变更
- 共享成功标准
客户-供应商
- 上游上下文提供数据
- 下游上下文消费
- 正式接口协议
顺从者
- 下游顺从上游模型
- 当上游不会改变时使用
- 接受他们的模型
防腐层
- 在上下文之间翻译
- 保护领域模型免受外部影响
- 隔离遗留系统
共享核心
- 上下文之间的小型共享模型
- 需要协调
- 谨慎使用
上下文映射定义限界上下文如何关联。防腐层保护你的领域模型免受外部系统影响。客户-供应商关系建立明确的责任。这些模式使集成明确且可管理。
聚合
聚合定义一致性边界:
📦 聚合模式
定义
- 作为单元处理的对象集群
- 一个实体是聚合根
- 外部引用仅指向根
- 在边界内强制执行一致性
规则
- 根实体具有全局标识
- 内部实体具有本地标识
- 外部对象不能持有对内部的引用
- 变更通过根进行
示例
- Order 是聚合根
- OrderLine 是内部实体
- 外部代码引用 Order,而非 OrderLine
- Order 确保所有行的一致性
聚合防止"大泥球",即所有东西都引用所有东西。通过定义明确的边界和访问规则,聚合使系统更易于维护并支持分布式事务。
战术设计模式
DDD 提供战术模式来实现领域模型。
构建块
战术模式形成领域模型的词汇:
🧱 DDD 构建块
实体
- 具有标识的对象
- 标识随时间持续
- 可变状态
- 示例:Customer、Order
值对象
- 由属性定义的对象
- 没有标识
- 不可变
- 示例:Money、Address、DateRange
服务
- 不属于实体的操作
- 无状态
- 领域操作
- 示例:PricingService、ShippingCalculator
仓储
- 持久化的抽象
- 类似集合的接口
- 隐藏数据库细节
- 示例:OrderRepository
工厂
- 复杂对象创建
- 封装构造逻辑
- 确保有效对象
- 示例:OrderFactory
这些模式提供了组织领域逻辑的结构化方式。实体具有标识和生命周期。值对象表示没有标识的概念。服务处理跨越多个对象的操作。仓储抽象持久化。工厂处理复杂的创建。
实体 vs 值对象
理解区别至关重要:
🔍 实体 vs 值对象
实体示例:Customer
public class Customer {
private CustomerId id; // 标识
private String name;
private Email email;
// 基于标识的相等性
public boolean equals(Object o) {
if (!(o instanceof Customer)) return false;
Customer other = (Customer) o;
return id.equals(other.id);
}
}
**值对象示例:Money**
public class Money {
private final BigDecimal amount;
private final Currency currency;
// 不可变
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException(
"无法加总不同货币");
}
return new Money(
amount.add(other.amount),
currency);
}
// 基于值的相等性
public boolean equals(Object o) {
if (!(o instanceof Money)) return false;
Money other = (Money) o;
return amount.equals(other.amount)
&& currency.equals(other.currency);
}
}
实体通过标识进行比较——两个具有相同名称的客户如果有不同的 ID 就是不同的。值对象通过值进行比较——两个具有相同金额和货币的 Money 对象是相同的。
领域事件
领域事件捕获重要的业务发生:
📢 领域事件
目的
- 表示发生的事情
- 过去式命名
- 不可变
- 实现松耦合
示例
public class OrderPlaced {
private final OrderId orderId;
private final CustomerId customerId;
private final Instant occurredAt;
public OrderPlaced(OrderId orderId,
CustomerId customerId) {
this.orderId = orderId;
this.customerId = customerId;
this.occurredAt = Instant.now();
}
}
**好处**
- 明确的业务事件
- 解耦的组件
- 审计轨迹
- 支持事件溯源
领域事件使隐含概念变得明确。系统不是默默更新状态,而是发布 OrderPlaced 事件。系统的其他部分可以反应——发送确认电子邮件、更新库存、触发运输。事件实现松耦合并提供自然的审计轨迹。
真实世界应用
DDD 在特定情境中表现出色,但并非总是正确的选择。
何时 DDD 增加价值
DDD 最适合复杂领域:
✅ 良好的 DDD 候选者
复杂业务逻辑
- 许多业务规则
- 规则以复杂方式交互
- 需要领域专家
- 示例:保险承保、交易系统
协作建模
- 业务专家可用
- 可能进行迭代精炼
- 共享理解有价值
- 示例:定制化企业应用程序
长期系统
- 系统将演进多年
- 可维护性至关重要
- 知识保存重要
- 示例:核心业务系统
战略差异化
- 领域是竞争优势
- 定制化逻辑,而非通用 CRUD
- 业务规则中的创新
- 示例:推荐引擎、定价算法
当领域复杂性证明其合理性时,DDD 的开销是值得的。具有复杂业务规则、多个利益相关者和长生命周期的系统受益于 DDD 的建模严谨性。
何时更简单的方法就足够
并非每个系统都需要 DDD:
⚠️ DDD 可能过度
简单 CRUD 应用程序
- 基本的创建、读取、更新、删除
- 最少的业务逻辑
- 数据管理焦点
- 更好的方法:简单分层架构
技术问题
- 算法密集型系统
- 基础设施工具
- 没有复杂领域
- 更好的方法:技术设计模式
原型和 MVP
- 速度优于结构
- 不确定的需求
- 可能被丢弃
- 更好的方法:快速开发框架
没有领域专家的小团队
- 没有人可以协作
- 有限的领域知识
- 无法建立通用语言
- 更好的方法:更简单的模式
具有基本 CRUD 操作的内容管理系统不需要 DDD。测试市场适应性的原型不应投资于精细的领域建模。DDD 的好处伴随着成本——复杂性、学习曲线和开发时间。
电子商务平台示例
考虑电子商务平台的订单管理:
🛒 电子商务领域模型
限界上下文
- 目录:产品、类别、搜索
- 购物:购物车、结账、付款
- 订单管理:订单、履行、跟踪
- 客户:账户、偏好、历史
关键聚合
- Order(根:Order,包含:OrderLine)
- ShoppingCart(根:Cart,包含:CartItem)
- Product(根:Product,包含:Variant)
领域事件
- OrderPlaced
- PaymentProcessed
- OrderShipped
- OrderCancelled
值对象
- Money(金额 + 货币)
- Address(街道、城市、邮政编码)
- ProductSku(标识符)
这种结构使业务概念明确。Order 聚合确保一致性——你不能有没有订单的订单行。领域事件实现集成——当 OrderPlaced 触发时,库存更新并发送电子邮件。通用语言贯穿始终——业务利益相关者和开发者使用相同的术语。
金融服务示例
交易系统展示了 DDD 的力量:
💰 交易系统领域
复杂业务规则
- 每个交易员的头寸限制
- 风险计算
- 法规遵从
- 市场时间和假日
丰富领域模型
public class Trade {
public void execute() {
if (!market.isOpen()) {
throw new MarketClosedException();
}
if (exceedsPositionLimit()) {
throw new PositionLimitException();
}
if (!passesRiskCheck()) {
throw new RiskLimitException();
}
// 执行交易
}
}
**好处**
- 业务规则集中
- 在代码中强制执行合规性
- 领域专家可以审查逻辑
- 变更追溯到业务需求
金融系统具有复杂、不断演进的规则。DDD 对领域模型的关注使这种复杂性保持可管理。当法规改变时,领域模型改变。代码反映当前的业务理解。
实现策略
采用 DDD 需要实用的策略。
开始使用 DDD
从战略模式开始:
💡 DDD 采用路径
阶段 1:战略设计
- 识别限界上下文
- 创建上下文映射
- 建立通用语言
- 定义核心领域
阶段 2:战术模式
- 建模关键聚合
- 识别实体和值对象
- 定义领域事件
- 实现仓储
阶段 3:精炼
- 重构以获得更深入的洞察
- 演进通用语言
- 调整边界
- 改进模型
从战略设计开始以理解大局。在深入战术模式之前识别限界上下文。这可以防止过早优化并确保努力集中在核心领域。
Event Storming
Event Storming 促进协作建模:
🎨 Event Storming 流程
它是什么
- 基于工作坊的建模技术
- 在墙上使用便利贴
- 协作和可视化
- 快速领域探索
步骤
- 识别领域事件(橙色便利贴)
- 添加触发事件的命令(蓝色便利贴)
- 识别聚合(黄色便利贴)
- 找到限界上下文(边界)
- 发现问题和机会(红色便利贴)
好处
- 吸引整个团队
- 揭示隐藏的复杂性
- 建立共享理解
- 快速且有效
Event Storming 将业务专家和开发者聚集在一起探索领域。可视化、协作的性质快速浮现假设和分歧。几个小时的 Event Storming 可以揭示通过传统需求收集需要数周才能发现的洞察。
避免常见陷阱
DDD 有众所周知的反模式:
⚠️ DDD 反模式
贫血领域模型
- 问题:没有行为的对象
- 解决方案:将逻辑移入领域对象
上帝聚合
- 问题:聚合太大
- 解决方案:拆分成更小的聚合
缺少限界上下文
- 问题:一个模型适用于所有事物
- 解决方案:识别并分离上下文
忽略通用语言
- 问题:代码使用技术术语
- 解决方案:重构以匹配业务语言
过度工程简单领域
- 问题:对 CRUD 应用程序使用 DDD
- 解决方案:使用更简单的方法
最常见的错误是在不理解其目的的情况下应用 DDD 模式。聚合变得臃肿。通用语言被忽略。领域模型变得贫血。成功需要纪律和持续重构以获得更深入的洞察。
DDD 与现代架构
DDD 影响当代架构模式。
微服务与限界上下文
限界上下文自然映射到微服务:
🔗 DDD + 微服务
对齐
- 每个微服务是一个限界上下文
- 明确的边界和责任
- 独立部署
- 团队所有权
好处
- DDD 提供服务边界
- 防止分布式单体
- 实现自主团队
- 自然的服务分解
挑战
- 分布式事务
- 数据一致性
- 集成复杂性
- 运营开销
没有限界上下文的微服务经常失败——服务有不明确的边界和紧密耦合。DDD 的战略模式提供有原则的服务分解。每个限界上下文成为具有明确领域焦点的微服务。
事件溯源与 CQRS
DDD 与事件溯源和 CQRS 配合良好:
📊 事件溯源 + CQRS
事件溯源
- 存储领域事件,而非当前状态
- 通过重放事件重建状态
- 完整的审计轨迹
- 时间旅行调试
CQRS(命令查询责任分离)
- 分离读取和写入模型
- 独立优化每个
- 读取/写入使用不同数据库
- 最终一致性
与 DDD 集成
- 领域事件是一等公民
- 聚合产生事件
- 读取模型服务查询
- 写入模型强制执行不变性
事件溯源使领域事件成为真相来源。CQRS 分离命令处理(写入)和查询(读取)。与 DDD 一起,它们创建业务事件明确、可审计并驱动整个架构的系统。
六边形架构
DDD 自然适合六边形(端口与适配器)架构:
实体、值对象
聚合、服务] end subgraph Ports["端口"] IP[输入端口
用例] OP[输出端口
仓储、服务] end subgraph Adapters["适配器"] REST[REST API] UI[Web UI] DB[数据库] MSG[消息队列] end REST --> IP UI --> IP IP --> DM DM --> OP OP --> DB OP --> MSG style Core fill:#e1f5ff,stroke:#333,stroke-width:3px style Ports fill:#fff4e1,stroke:#333,stroke-width:2px style Adapters fill:#e8f5e9,stroke:#333,stroke-width:2px
🏛️ 六边形架构 + DDD
结构
- 领域模型在中心
- 端口定义接口
- 适配器实现技术细节
- 依赖指向内部
好处
- 领域与基础设施隔离
- 易于测试领域逻辑
- 交换实现
- 技术无关的核心
六边形架构使领域模型独立于技术关注点。数据库、框架和外部服务成为实现细节。领域保持纯粹,专注于业务逻辑。
衡量成功
DDD 的价值出现在特定结果中:
✅ DDD 成功指标
沟通
- 业务和开发者使用相同术语
- 更少的误解
- 更快的需求澄清
- 代码审查包括业务利益相关者
可维护性
- 业务逻辑易于找到
- 变更局限于聚合
- 重构不会破坏一切
- 新开发者快速理解
灵活性
- 业务规则变更简单明了
- 新功能自然契合
- 技术变更不影响领域
- 系统随业务演进
质量
- 业务逻辑中的错误更少
- 不变性得到强制执行
- 边缘案例得到处理
- 领域测试可读
成功不是通过模式采用来衡量,而是通过业务结果。业务利益相关者能理解代码结构吗?变更需要更少时间吗?系统更可靠吗?这些指标揭示 DDD 是否提供价值。
结论
领域驱动设计代表从数据库中心到领域中心软件开发的根本转变。通过将领域模型置于中心、建立通用语言,以及应用战略和战术模式,DDD 创建与业务需求一致且随时间保持可维护的系统。
从传统方法到 DDD 的旅程揭示了重要的教训:
复杂性需要结构:简单的 CRUD 应用程序不需要 DDD。具有复杂业务规则的复杂领域受益于 DDD 的建模严谨性。关键是将方法与问题复杂性相匹配。
语言很重要:通用语言不仅仅是锦上添花——它是基础。当业务和开发者共享词汇时,误解减少,代码变得自我记录。维护这种共享语言的纪律持续带来回报。
边界实现规模:限界上下文防止"一个模型统治一切"的陷阱。通过明确分离关注点,系统保持可理解,团队可以独立工作。随着系统成长,这变得至关重要。
模式服务目的:DDD 的模式——聚合、实体、值对象、领域事件——不是货物崇拜实践。每个都解决特定问题。理解它们解决的问题可以防止误用。
协作驱动质量:当业务专家和开发者持续协作时,DDD 效果最好。Event Storming 和其他协作建模技术比传统需求文档更快地浮现假设并建立共享理解。
采用 DDD 的决定应该是深思熟虑的。对于具有复杂业务逻辑、长生命周期和可用领域专家的系统,DDD 提供多年来回报的结构。对于更简单的系统、原型或技术问题,更轻量的方法就足够了。
现代架构模式——微服务、事件溯源、CQRS、六边形架构——自然与 DDD 原则一致。限界上下文提供服务边界。领域事件实现事件溯源。领域模型保持独立于技术关注点。
DDD 不是银弹。它需要投资、纪律和持续重构以获得更深入的洞察。但对于正确的问题,它将软件开发从在业务和技术语言之间翻译转变为构建直接说业务语言的系统。
成功的最终衡量标准是软件是否有效解决真实业务问题,并能随着这些问题的变化而演进。DDD 提供工具和实践来实现这一目标,但只有在深思熟虑地应用于其复杂性合理的领域时才有效。