#DDD领域驱动设计
#一个订单模块的三年重构史
2019年,我们团队开始做一个电商系统。订单模块最初是这样的:
@Entity
class Order {
private Long id;
private Long userId;
private Double amount;
private String status; // "CREATED", "PAID", "SHIPPED", "COMPLETED"
private List<OrderItem> items;
// getter/setter... 200行
}
@Service
class OrderService {
@Autowired
private OrderRepository orderRepository;
public void createOrder(Long userId, List<OrderItemDTO> items) {
// 500行业务逻辑散落在Service中
}
public void payOrder(Long orderId, Double amount) {
// 300行业务逻辑
}
public void shipOrder(Long orderId) {
// 200行业务逻辑
}
}三年后,这个 Service 积累了 8000 行代码,200 个方法。没人敢动它——每次改一个逻辑都要担心影响其他逻辑。
这就是贫血模型的诅咒:数据和行为分离,业务逻辑散落在 Service 的各个角落,最终变成无人敢碰的"意大利面条"。
#二、DDD的核心思想
DDD(Domain-Driven Design,领域驱动设计)的核心是:把业务逻辑内聚到领域模型中,让软件反映业务概念。
贫血模型: Entity = 数据 + getter/setter
所有行为在 Service 中
DDD 充血模型: Entity = 数据 + 行为(领域逻辑)
Service 只负责编排和协调#三、DDD战术设计:构建块🔴
#3.1 实体(Entity)
实体:有唯一标识的对象,标识贯穿整个生命周期,属性可能变化。
// 订单是实体:即使所有属性都一样,order-001 和 order-002 是两个不同的订单
class Order extends BaseEntity {
private OrderId id; // 唯一标识
private UserId userId;
private OrderStatus status;
private Money totalAmount;
private List<OrderItem> items;
// 充血模型:行为内聚
public void pay(Money amount) {
// 前置校验:订单状态必须是 CREATED
if (this.status != OrderStatus.CREATED) {
throw new OrderCannotPayException(id, status);
}
// 业务规则:支付金额必须等于订单金额
if (!amount.equals(this.totalAmount)) {
throw new AmountMismatchException(amount, this.totalAmount);
}
// 状态转换
this.status = OrderStatus.PAID;
this.paidAt = Instant.now();
// 发布领域事件
DomainEvents.publish(new OrderPaidEvent(this));
}
public void cancel() {
// 前置校验
if (this.status != OrderStatus.CREATED) {
throw new OrderCannotCancelException(id, status);
}
// 业务规则:已支付的订单不能取消,只能退款
if (this.status == OrderStatus.PAID) {
throw new PaidOrderCannotCancelException(id);
}
this.status = OrderStatus.CANCELLED;
DomainEvents.publish(new OrderCancelledEvent(this));
}
}#3.2 值对象(Value Object)
值对象:没有唯一标识,通过属性值确定,不可变。
// Money 是值对象:100元和100元永远相等
// "100元"和"100元"是无法区分的,除非比较属性
class Money {
private final BigDecimal amount;
private final Currency currency;
private Money(BigDecimal amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}
public static Money of(BigDecimal amount, Currency currency) {
return new Money(amount, currency);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException(this.currency, other.currency);
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), this.currency);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.equals(money.amount) && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
}值对象的判断标准:
- 不需要唯一标识
- 不可变(创建后不能修改)
- 可以用其他值对象替换
- 相等性由属性决定,不是引用
#3.3 聚合(Aggregate)
聚合:一组相关对象的集合,作为数据修改的单元。每个聚合有一个聚合根。
// Order 是聚合根,OrderItem 只能通过 Order 访问
class Order extends BaseAggregateRoot {
private OrderId id;
private UserId userId;
private OrderStatus status;
private List<OrderItem> items; // OrderItem 不能单独修改
// 聚合根的方法:对外暴露的唯一入口
public void addItem(Product product, int quantity) {
// 业务规则:订单状态必须是 CREATED
if (this.status != OrderStatus.CREATED) {
throw new OrderCannotModifyException(id, status);
}
// 业务规则:检查是否已存在相同商品
OrderItem existing = findItem(product.getId());
if (existing != null) {
existing.increaseQuantity(quantity);
} else {
items.add(new OrderItem(product.getId(), product.getPrice(), quantity));
}
recalculateTotal();
}
public void removeItem(ProductId productId) {
if (this.status != OrderStatus.CREATED) {
throw new OrderCannotModifyException(id, status);
}
items.removeIf(item -> item.getProductId().equals(productId));
recalculateTotal();
}
private OrderItem findItem(ProductId productId) {
return items.stream()
.filter(i -> i.getProductId().equals(productId))
.findFirst()
.orElse(null);
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
// OrderItem 不能单独存在,必须通过 Order
class OrderItem {
private ProductId productId;
private Money unitPrice;
private int quantity;
// 不能从外部直接创建
OrderItem(ProductId productId, Money unitPrice, int quantity) {
this.productId = productId;
this.unitPrice = unitPrice;
this.quantity = quantity;
}
void increaseQuantity(int delta) {
this.quantity += delta;
}
Money getSubtotal() {
return unitPrice.multiply(quantity);
}
}聚合的规则:
- 通过聚合根访问所有内部对象
- 外部对象不能直接持有聚合内部对象的引用
- 聚合内所有对象保持一致性
- 聚合是事务边界:一个聚合的修改在一个事务中完成
#3.4 仓储(Repository)
仓储:聚合的持久化抽象,只操作聚合根。
// ❌ 错误:暴露了 OrderItem
interface OrderRepository {
void save(Order order);
Order findById(OrderId id);
List<OrderItem> findItemsByOrderId(OrderId id); // 错误:不应该有单独的 Item 查询
}
// ✅ 正确:只操作聚合根
interface OrderRepository {
void save(Order order);
Order findById(OrderId id);
Optional<Order> findByIdForUpdate(OrderId id); // 悲观锁
List<Order> findByUserId(UserId userId);
}#3.5 领域服务(Domain Service)
当某个业务逻辑不属于任何一个实体或值对象时,使用领域服务。
// 计算订单总价:不属于 Order,但属于业务逻辑
class OrderPricingService {
public Money calculatePrice(List<OrderItem> items, Coupon coupon) {
Money subtotal = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
Money discount = coupon.calculateDiscount(subtotal);
return subtotal.subtract(discount);
}
}
// 跨聚合的业务逻辑:转账
class TransferService {
public void transfer(Account from, Account to, Money amount) {
// 这个逻辑涉及两个 Account 聚合,无法放在任何一个聚合中
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException(from.getId(), amount);
}
from.withdraw(amount);
to.deposit(amount);
}
}#3.6 领域事件(Domain Event)
// 定义领域事件
@DomainEvent
class OrderPaidEvent {
private final OrderId orderId;
private final UserId userId;
private final Money amount;
private final Instant paidAt;
public OrderPaidEvent(Order order) {
this.orderId = order.getId();
this.userId = order.getUserId();
this.amount = order.getTotalAmount();
this.paidAt = order.getPaidAt();
}
}
// 在聚合根中发布事件
class Order extends BaseAggregateRoot {
public void pay(Money amount) {
// ... 支付逻辑
DomainEvents.publish(new OrderPaidEvent(this));
}
}
// 订阅事件(异步处理)
@EventListener
class PointsService {
@Async
public void handleOrderPaid(OrderPaidEvent event) {
pointsService.addPoints(event.getUserId(), event.getAmount());
}
}#四、DDD战略设计:限界上下文🔴
#4.1 问题
在大型电商系统中,"用户"在不同上下文中含义不同:
电商上下文:用户 = 买家/卖家 + 购物行为 + 订单关联
鉴权上下文:用户 = 账号 + 密码 + 登录行为
CRM上下文:用户 = 客户 + 联系方式 + 营销标签同一个"用户"实体,在不同上下文中属性和行为完全不同。
#4.2 限界上下文(Bounded Context)
限界上下文是 DDD 的战略设计工具:把整个系统拆分成多个独立的上下文,每个上下文有自己的领域模型和边界。
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 电商上下文 │ │ 鉴权上下文 │ │ CRM上下文 │
│ │ │ │ │ │
│ User(买家/卖家) │ │ Account(账号) │ │ Customer(客户) │
│ Order │ │ Session │ │ Contact │
│ Product │ │ Permission │ │ MarketingTag │
│ │ │ │ │ │
│ 内部数据: │ │ 内部数据: │ │ 内部数据: │
│ 订单历史、 │ │ 密码哈希、 │ │ 联系方式、 │
│ 购物偏好 │ │ 会话Token │ │ 营销数据 │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└────────────────────┼────────────────────┘
│ 通过防腐层转换#4.3 上下文映射(Context Mapping)
不同上下文之间通过上下文映射协作:
// 电商上下文通过防腐层访问鉴权上下文
class UserContextAdapter {
private final AuthService authService; // 鉴权上下文的接口
public UserInfo getUserInfo(UserId userId) {
// 适配:鉴权上下文的 Account → 电商上下文的 UserInfo
Account account = authService.getAccount(userId);
return new UserInfo(account.getId(), account.getUsername());
}
}【架构权衡】 限界上下文的划分没有标准答案。常见的划分方式:
- 按业务能力(电商 vs 库存 vs 物流)
- 按团队边界(康威定律)
- 按数据一致性需求(强一致 vs 最终一致)
划分的粒度太粗会变成"单体架构",太细会增加集成复杂度。DDD 的精髓在于找到业务边界清晰、数据内聚的上下文划分。
#五、CQRS 与 DDD🟡
#5.1 命令查询分离
CQRS(Command Query Responsibility Segregation)将写操作和读操作分离:
// 命令端:DDD 聚合
@Aggregate
class Order {
// 充血模型处理写操作
public void pay(Money amount) { ... }
public void addItem(Product product, int quantity) { ... }
}
// 查询端:简单 DTO
record OrderDetailDTO(
Long orderId,
String userName,
String status,
BigDecimal amount,
List<OrderItemDTO> items,
LocalDateTime createdAt
) {}#5.2 数据同步
读模型如何获取数据?
// 方式一:通过领域事件同步到读模型
@EventListener
class OrderReadModelUpdater {
@Autowired
private JdbcTemplate jdbcTemplate;
@Async
public void handleOrderCreated(OrderCreatedEvent event) {
jdbcTemplate.update(
"INSERT INTO order_read_model ...",
event.getOrderId(), event.getUserId(), event.getAmount()
);
}
}
// 方式二:直接查聚合(简单场景)
@Service
class OrderQueryService {
public OrderDetailDTO getOrderDetail(OrderId id) {
Order order = orderRepository.findById(id);
return convertToDTO(order);
}
}#六、生产避坑清单
#6.1 DDD 过度设计
// ❌ 过度使用:简单 CRUD 也用 DDD
@Aggregate
class Product {
@Id
private Long id;
public void setName(String name) { // 充血模型?不需要
this.name = name;
}
}
// ✅ 正确:简单场景用 CRUD,复杂业务用 DDD⚠️
DDD 不是银弹。简单 CRUD 系统用 DDD 会增加不必要的复杂度。DDD 适用于业务复杂、规则多、变化频繁的核心领域。
#6.2 聚合过大
// ❌ 错误:把所有对象放进一个大聚合
class Order {
private List<OrderItem> items;
private User user; // 关联到 User 聚合
private Product product; // 关联到 Product 聚合
private Warehouse warehouse; // 关联到 Warehouse 聚合
// 一个订单聚合了 10 个其他聚合,性能爆炸
}
// ✅ 正确:用 ID 引用,而不是直接关联
class Order {
private OrderId id;
private UserId userId; // 用 ID 引用
private ProductId productId; // 用 ID 引用
}#6.3 贫血 vs 充血
// ❌ 贫血:所有逻辑在 Service
class OrderService {
public void pay(Order order, double amount) {
if (order.getStatus() != "CREATED") throw new Exception();
if (amount != order.getAmount()) throw new Exception();
order.setStatus("PAID");
}
}
// ✅ 充血:逻辑在 Entity
class Order {
public void pay(double amount) {
if (this.status != OrderStatus.CREATED) throw new Exception();
if (amount != this.amount) throw new Exception();
this.status = OrderStatus.PAID;
}
}#七、面试总结
#7.1 核心追问
- "DDD 和传统三层架构的区别?" —— 充血模型 vs 贫血模型
- "实体和值对象的区别?" —— 唯一标识 vs 属性相等
- "聚合的作用是什么?" —— 事务边界、一致性边界
- "限界上下文怎么划分?" —— 业务边界、数据一致性
- "DDD 什么场景适用?" —— 复杂业务领域
#7.2 级别差异
| 级别 | 期望回答 |
|---|---|
| P5 | 能说出实体、值对象、聚合的概念 |
| P6 | 能区分贫血和充血模型,知道限界上下文的划分思路 |
| P7 | 有实际 DDD 落地经验,能分析 CQRS、事件溯源等进阶实践 |