第 216 章:DDD 与领域驱动设计
学习目标
- 理解 DDD 核心思想
- 掌握战略设计方法
- 学会战术设计工具
- 在 SpringBoot 中应用 DDD
一、DDD 简介
领域驱动设计(Domain-Driven Design)由 Eric Evans 在 2003 年提出,核心是让软件模型与业务领域对齐。
1.1 为什么需要 DDD
| 问题 | DDD 解决 |
|---|---|
| 业务理解不一致 | 通用语言(Ubiquitous Language) |
| 模型与代码割裂 | 模型即代码 |
| 业务逻辑分散 | 聚合封装不变性 |
| 微服务拆分难 | 限界上下文作切分依据 |
1.2 两层设计
二、战略设计
2.1 领域与子域
| 子域类型 | 特征 | 投入 |
|---|---|---|
| 核心域 | 差异化竞争 | 最多资源 |
| 支撑域 | 必要但非核心 | 中等资源 |
| 通用域 | 通用功能 | 考虑外包或 SaaS |
2.2 限界上下文(Bounded Context)
业务边界,上下文内的模型有明确含义。
电商系统
├── 销售上下文(下单、订单)
├── 商品上下文(SPU、SKU)
├── 库存上下文(库位、扣减)
├── 支付上下文(交易、退款)
└── 用户上下文(账户、积分)同一概念在不同上下文含义不同:
- 商品 在销售上下文 = SPU 信息
- 商品 在库存上下文 = SKU + 库位
2.3 上下文映射
| 模式 | 说明 |
|---|---|
| Shared Kernel | 共享同一子集 |
| Customer/Supplier | 上下游 |
| Conformist | 下游强依赖上游 |
| ACL(防腐层) | 隔离外部系统 |
| OHS(开放主机服务) | 定义协议供外部调用 |
| Published Language | 共享交换格式(JSON/XML) |
2.4 通用语言(Ubiquitous Language)
业务专家与技术团队共同使用的术语。
❌ 错误:
业务方说"下单",开发说"创建交易单"
✅ 正确:
双方都使用"下单"或 trade → order三、战术设计
3.1 实体(Entity)
有唯一标识,生命周期内可变。
java
public class Order {
@Getter
private OrderId id; // 唯一标识
private Money amount;
private OrderStatus status;
public void pay() {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("订单状态不允许支付");
}
this.status = OrderStatus.PAID;
registerEvent(new OrderPaidEvent(this.id));
}
}3.2 值对象(Value Object)
无标识,不可变,相等由属性决定。
java
public class Money {
private final BigDecimal amount;
private final String currency;
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("币种不同");
}
return new Money(amount.add(other.amount), currency);
}
// equals / hashCode 由属性计算
}常用值对象:
Money(金额+币种)Address(地址)DateRange(日期范围)Email/PhoneNumber
3.3 聚合(Aggregate)
一致性边界,根实体管理内部对象。
规则:
- 聚合内强一致,聚合间最终一致
- 跨聚合通过 ID 引用,不直接操作
- 外部只能通过聚合根访问内部
java
public class Order {
private OrderId id;
private List<OrderItem> items; // 内部对象
private CustomerId customerId; // 外部引用 ID
public void addItem(Product product, int qty) {
// 业务规则校验
items.add(new OrderItem(product, qty));
}
}3.4 领域服务(Domain Service)
跨实体、跨聚合的业务逻辑。
java
@Service
public class PricingService {
public Money calculatePrice(Order order, Customer customer) {
Money base = order.itemsAmount();
Money discount = customer.discountFor(order);
return base.subtract(discount);
}
}判断标准:实体放不下时,放领域服务。
3.5 领域事件(Domain Event)
聚合内发生的业务事件,用于解耦。
java
public class OrderPaidEvent {
private OrderId orderId;
private Money amount;
private Instant occurredOn;
}java
// 聚合内发布
public class Order {
private List<DomainEvent> events = new ArrayList<>();
public void pay() {
// ...
events.add(new OrderPaidEvent(id, amount));
}
public List<DomainEvent> getEvents() {
return Collections.unmodifiableList(events);
}
}订阅方:
java
@Component
public class InventorySubscriber {
@EventListener
public void on(OrderPaidEvent event) {
inventoryService.deduct(event.orderId());
}
}3.6 仓储(Repository)
聚合的持久化抽象,对外隐藏实现细节。
java
public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
void delete(OrderId id);
}java
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@PersistenceContext
private EntityManager em;
@Override
public Order findById(OrderId id) {
return em.find(Order.class, id.value());
}
@Override
@Transactional
public void save(Order order) {
// Order 是聚合根,级联保存 OrderItem
em.merge(order);
// 发布事件
order.getEvents().forEach(eventPublisher::publish);
}
}四、分层架构
4.1 四层架构
| 层 | 职责 |
|---|---|
| 用户接口层 | 接收请求、参数校验、返回响应 |
| 应用层 | 用例编排、事务控制、权限 |
| 领域层 | 业务逻辑、领域模型、领域服务 |
| 基础设施层 | 持久化、消息、缓存等实现 |
4.2 SpringBoot 项目结构
src/main/java/com/example/order/
├── interfaces/ # 用户接口层
│ ├── web/ # REST Controller
│ │ └── OrderController.java
│ └── dto/ # DTO
│ └── CreateOrderRequest.java
├── application/ # 应用层
│ ├── service/
│ │ └── OrderApplicationService.java
│ └── event/
│ └── OrderEventHandler.java
├── domain/ # 领域层
│ ├── model/ # 实体 + 值对象
│ │ ├── Order.java
│ │ ├── OrderItem.java
│ │ └── Money.java
│ ├── repository/ # 仓储接口
│ │ └── OrderRepository.java
│ ├── service/ # 领域服务
│ │ └── PricingService.java
│ └── event/ # 领域事件
│ └── OrderPaidEvent.java
└── infrastructure/ # 基础设施层
├── persistence/ # JPA 实现
│ └── OrderRepositoryImpl.java
├── mq/ # 消息队列
└── config/4.3 代码示例
Controller(接口层):
java
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderApplicationService orderService;
@PostMapping
public Result<OrderResponse> create(@RequestBody @Valid CreateOrderRequest req) {
OrderId id = orderService.createOrder(
new CustomerId(req.customerId()),
req.items()
);
return Result.ok(OrderResponse.of(id));
}
}ApplicationService(应用层):
java
@Service
@RequiredArgsConstructor
public class OrderApplicationService {
private final OrderRepository orderRepo;
private final PricingService pricingService;
@Transactional
public OrderId createOrder(CustomerId customerId, List<OrderItemReq> items) {
Order order = Order.create(customerId);
items.forEach(i -> order.addItem(i.productId(), i.quantity()));
Money price = pricingService.calculatePrice(order, ...);
order.confirm(price);
orderRepo.save(order);
return order.id();
}
}Order(领域层):
java
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderItem> items = new ArrayList<>();
private Money totalAmount;
private OrderStatus status;
public static Order create(CustomerId customerId) {
Order order = new Order();
order.id = OrderId.generate();
order.customerId = customerId;
order.status = OrderStatus.PENDING;
return order;
}
public void addItem(ProductId productId, int qty) {
items.add(new OrderItem(productId, qty, ...));
}
public void confirm(Money totalAmount) {
if (items.isEmpty()) {
throw new IllegalStateException("订单必须包含商品");
}
this.totalAmount = totalAmount;
this.status = OrderStatus.CONFIRMED;
registerEvent(new OrderConfirmedEvent(id, totalAmount));
}
public void pay(Money paid) {
if (!paid.equals(totalAmount)) {
throw new IllegalArgumentException("支付金额不匹配");
}
this.status = OrderStatus.PAID;
registerEvent(new OrderPaidEvent(id));
}
}五、聚合设计
5.1 划分原则
- 强一致性:必须一起修改的对象放同一聚合
- 业务不变性:由聚合根强制
- 事务边界:聚合即事务边界
- 不要过大:聚合越大,性能越差
5.2 案例对比
❌ 错误:一个大订单聚合,含订单 + 商品 + 配送 + 支付 + 评价
java
public class Order {
private List<Item> items;
private Shipment shipment;
private Payment payment;
private Review review;
}✅ 正确:拆分多个聚合
java
// Order 聚合(订单基本信息)
public class Order {
private OrderId id;
private CustomerId customerId;
private ShipmentId shipmentId; // 引用 ID
private PaymentId paymentId; // 引用 ID
}
// 独立的 Shipment / Payment 聚合5.3 ID 引用 vs 对象引用
java
// ❌ 跨聚合直接引用
class Order {
private Shipment shipment; // 直接引用对象
}
// ✅ 通过 ID 引用
class Order {
private ShipmentId shipmentId; // 仅引用 ID
}六、事件风暴(Event Storming)
一种团队工作坊方法,用于领域探索。
| 元素 | 颜色 | 含义 |
|---|---|---|
| Event | 橙色 | 已发生的事 |
| Command | 蓝色 | 触发事件的命令 |
| Aggregate | 黄色 | 处理命令 |
| Policy | 紫色 | 业务规则 |
| External System | 粉色 | 外部系统 |
| Persona | 黄色便签 | 角色 |
七、案例:电商下单
7.1 流程
7.2 限界上下文
下单上下文(订单中心)
├── Order 聚合
└── PricingService
库存上下文(WMS)
└── Inventory 聚合
支付上下文(支付网关)
└── Payment 聚合
通过 ACL/事件 进行跨上下文协作7.3 跨限界上下文通信
java
// 库存上下文订阅订单事件
@Component
public class OrderEventSubscriber {
@EventListener
public void on(OrderCreatedEvent event) {
inventoryService.lockStock(event.orderId(), event.items());
}
}
// 或通过消息队列异步处理
@KafkaListener(topics = "order-events")
public void handle(OrderCreatedEvent event) {
inventoryService.lockStock(event.orderId(), event.items());
}八、贫血模型 vs 充血模型
8.1 贫血模型(Anemic)
java
// ❌ 只有 getter/setter,业务在 Service
@Data
public class Order {
private Long id;
private BigDecimal amount;
private String status;
}
@Service
public class OrderService {
public void pay(Order order) {
if (!"PENDING".equals(order.getStatus())) {
throw new RuntimeException();
}
order.setStatus("PAID");
}
}8.2 充血模型(Rich)
java
// ✅ 实体含业务行为
public class Order {
private OrderStatus status;
public void pay() {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException();
}
this.status = OrderStatus.PAID;
registerEvent(new OrderPaidEvent(id));
}
}DDD 必须用充血模型。
九、常见误区
| 误区 | 纠正 |
|---|---|
| 把 DDD 当万能解药 | 简单 CRUD 用 DDD 是过度设计 |
| 盲目使用战术工具 | 先战略,后战术 |
| 忽视通用语言 | 业务与代码术语必须一致 |
| 聚合过大 | 按业务不变性切分 |
| 把 Repository 当 DAO | Repository 是集合操作,非单表 |
| Service 越来越胖 | 把业务迁到 Entity |
十、本章小结
| 概念 | 作用 |
|---|---|
| 子域 | 划分问题空间 |
| 限界上下文 | 划分解决方案空间 |
| 通用语言 | 团队沟通准则 |
| 实体 | 有生命周期 |
| 值对象 | 不可变描述 |
| 聚合 | 一致性边界 |
| 领域服务 | 跨实体逻辑 |
| 领域事件 | 解耦通信 |
| 仓储 | 持久化抽象 |
动手练习
- 找一个业务场景,识别核心域 / 支撑域 / 通用域
- 用四色贴纸做事件风暴工作坊
- 为"下单流程"设计聚合(明确根、边界、不变性)
- 用 SpringBoot 实现一个微聚合(SKU + 商品目录)
推荐阅读
下一章:第 217 章:微服务入门