领域驱动设计(DDD)
DDD 是一种以业务领域为核心的软件设计方法,通过统一语言连接业务与技术,解决复杂业务系统的建模难题。
1. DDD 核心思想
┌────────────────────────────────┐
│ 统一语言 Ubiquitous │
│ Language │
└────────────────────────────────┘
↓ ↑
┌────────────────────────────────┐
│ 领域模型 Domain Model │
│ ┌─────┐ ┌─────┐ ┌────────┐ │
│ │实体 │ │值对象│ │ 聚合 │ │
│ └─────┘ └─────┘ └────────┘ │
└────────────────────────────────┘
核心:技术与业务使用同一套语言进行沟通,模型直接反映业务概念。
2. 战略设计
子域(Subdomain)
将复杂业务划分为不同类型:
| 类型 | 特征 | 示例 |
|---|---|---|
| 核心域 | 竞争优势所在,投入最多 | 电商的推荐算法 |
| 支撑子域 | 需要定制但不独特 | 订单管理 |
| 通用子域 | 可外购或标准化 | 用户认证、通知服务 |
限界上下文(Bounded Context)
模型一致性的边界。同一个概念在不同上下文可以有不同的含义。
┌─────────────────┐ ┌─────────────────┐
│ 商品上下文 │ │ 库存上下文 │
│ Product │ │ Product │
│ - name │ │ - sku │
│ - description │ │ - quantity │
│ - price │ │ - warehouse │
│ - category │ │ - location │
└─────────────────┘ └─────────────────┘
↑ ↑
「商品」是销售实体 「商品」是库存单位
上下文映射
┌──────────────┐ 合作关系 ┌──────────────┐
│ 订单上下文 │ ←─────────────→ │ 支付上下文 │
└──────────────┘ └──────────────┘
│
│ 上游/供应商
↓
┌──────────────┐
│ 库存上下文 │
└──────────────┘
| 关系 | 含义 |
|---|---|
| 合作关系 | 任一方失败影响另一方 |
| 客户-供应商 | 上游优先满足下游需求 |
| 跟随者 | 复用上游模型 |
| 防腐层 (ACL) | 隔离外部模型影响 |
| 开放主机 | 定义清晰接口供外部使用 |
3. 战术设计
实体(Entity)
有唯一标识且通过标识区分的对象,生命周期中属性可变。
public class Order {
private OrderId id; // 唯一标识
private CustomerId customerId;
private List<OrderItem> items;
private OrderStatus status;
public void pay() { // 业务行为
if (status != PENDING) throw new IllegalStateException();
this.status = PAID;
}
public void ship() { ... }
}
值对象(Value Object)
属性决定身份,不可变,无生命周期概念。
public record Money(BigDecimal amount, Currency currency) {
public Money add(Money other) {
if (!this.currency.equals(other.currency))
throw new IllegalArgumentException("Currency mismatch");
return new Money(this.amount.add(other.amount), this.currency);
}
}
public record Address(
String province,
String city,
String street
) {}
聚合(Aggregate)
一致性边界内的实体与值对象集群。
// Order 是聚合根(Aggregate Root)
public class Order {
private OrderId id;
private List<OrderItem> items; // 聚合内实体
private ShippingAddress address; // 值对象
// 所有修改通过聚合根
public void addItem(ProductId productId, int quantity, Money price) {
items.add(new OrderItem(productId, quantity, price));
recalculateTotal();
}
}
聚合设计原则:
- 一个事务只修改一个聚合
- 聚合根负责维护不变性
- 聚合间通过 ID 引用(避免直接对象引用)
领域服务(Domain Service)
不属于某个聚合的业务逻辑。
public class PricingService {
public Money calculateDiscount(Order order, Customer customer) {
if (customer.isVIP()) {
return order.getTotal().multiply(0.8);
}
return order.getTotal();
}
}
领域事件(Domain Event)
领域内发生的有意义的事情,实现最终一致性。
public class OrderCreatedEvent {
private final OrderId orderId;
private final CustomerId customerId;
private final Instant occurredOn;
// 用于其他上下文处理后续业务
}
// 发布
public class Order {
public void create() {
// ...
registerEvent(new OrderCreatedEvent(this.id, ...));
}
}
4. 事件风暴(Event Storming)
Alberto Brandolini 发明的协作建模工作坊。
流程
1. 领域事件(橙色) → OrderCreated, PaymentReceived
2. 命令(蓝色) → CreateOrder, ReceivePayment
3. 聚合(黄色) → Order, Payment
4. 策略(紫色) → 如果支付失败则取消订单
5. 外部系统(粉色) → 支付网关、物流系统
产出物
- 限界上下文划分
- 聚合边界
- 领域事件流
- 服务拆分候选
5. DDD 分层架构
┌─────────────────────────────────────┐
│ 用户接口层(UI/API) │
│ 适配器模式、DTO、输入校验 │
├─────────────────────────────────────┤
│ 应用层(Application) │
│ 用例编排、事务控制、事件发布 │
├─────────────────────────────────────┤
│ 领域层(Domain)← 核心 │
│ 实体、值对象、聚合、领域服务、事件 │
├─────────────────────────────────────┤
│ 基础设施层(Infrastructure) │
│ 数据库、消息队列、外部 API 适配 │
└─────────────────────────────────────┘
依赖方向:外层依赖内层,领域层无外部依赖。
6. DDD 与微服务
限界上下文 → 微服务边界
聚合根 → 服务内模块
领域事件 → 服务间通信
防腐层 → API Gateway / Adapter
| DDD 概念 | 映射微服务 |
|---|---|
| 核心域 | 独立团队维护的核心服务 |
| 通用子域 | 共享基础设施服务 |
| 限界上下文 | 服务部署边界 |
| 领域事件 | 事件驱动架构 |
总结
| 层面 | 关键产出 | 工具 |
|---|---|---|
| 战略设计 | 子域划分、限界上下文 | 事件风暴 |
| 战术设计 | 聚合、实体、领域服务 | 代码模型 |
| 技术实践 | 分层架构、事件驱动 | 框架实现 |
7. DDD 实施的常见陷阱与应对
DDD 在实践中常被误用或过度设计,以下是几个典型问题及应对策略。
7.1 “贫血领域模型"症候群
这是 DDD 实践中最常见的问题:实体只有 getter/setter,所有业务逻辑散落在 Service 层,领域模型沦为数据容器。
// ❌ 贫血模型
public class Order {
private Long id;
private String status;
// 只有 getter/setter
}
// Service 层充斥业务逻辑
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderRepo.findById(orderId);
if (!"PENDING".equals(order.getStatus())) {
throw new IllegalStateException("Cannot cancel non-pending order");
}
order.setStatus("CANCELLED");
orderRepo.save(order);
}
}
修正方向:将状态校验、计算等逻辑下沉到实体中,让 Order 自己知道自己的业务规则。
7.2 聚合粒度过大
初学者常将整张领域大图塞进一个聚合,导致事务范围膨胀、并发冲突严重。正确的原则是一个事务只修改一个聚合,聚合间通过异步事件最终一致。
// ❌ 错误:订单聚合包含物流、支付等跨域信息
public class Order {
private List<OrderItem> items;
private Payment payment; // 不应在同一聚合
private Shipment shipment; // 不应在同一聚合
}
// ✅ 正确:订单聚合只包含订单自身的核心信息
public class Order {
private List<OrderItem> items;
private Money totalAmount;
// 支付和物流通过领域事件异步关联
}
7.3 统一语言流于形式
“统一语言"需要持续投入:每次需求评审都要确认术语定义,每次发现"这个词我们理解不一样"都要更新词汇表。建议维护一份在线的领域词汇表(Glossary)。
7.4 何时不应使用 DDD
| 场景 | 建议方案 | 原因 |
|---|---|---|
| CRUD 为主的简单应用 | 事务脚本 / MVC | 领域复杂度低,DDD 过度设计 |
| 技术验证原型(MVP ) | 快速迭代,后期再建模 | 初期需求不稳定 |
| 纯数据报表系统 | SQL + ETL | 无复杂业务规则 |
| 团队缺乏 OOP 经验 | 先补基础再引入 DDD | DDD 依赖扎实的面向对象设计能力 |
DDD 适用于业务逻辑复杂、领域模型会随业务发展持续演进的系统。对于简单应用,DDD 的建模范式反而会增加不必要的认知负担。
DDD 的价值不在于严格按照名词定义编码,而在于培养以业务领域为核心思考问题的能力。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。