4. 聚合根的设计原则:唯一标识、全局访问入口、内部一致性保证、外部引用规则
聚合根(Aggregate Root)是领域驱动设计中聚合(Aggregate)的入口与边界守护者。设计聚合根时,必须遵循四项核心原则,以确保聚合内部的一致性与微服务边界的清晰性。以下逐一展开。
4.1 唯一标识(Identity)
每个聚合根必须拥有一个全局唯一的标识符(ID),该标识符在聚合的整个生命周期内保持不变。唯一标识是聚合根区别于其他实体的核心特征,也是外部系统引用聚合的唯一凭证。
| 标识类型 | 说明 | 典型示例 |
|---|---|---|
| 自然键 | 业务领域天然存在的唯一值 | 身份证号、车牌号 |
| 代理键 | 系统自动生成的、无业务含义的ID | UUID、自增序列、雪花ID |
| 组合键 | 多个字段组合形成的唯一标识 | 租户ID + 订单号 |
实践建议:优先使用代理键(如UUID)作为聚合根标识,避免自然键因业务规则变更而失效。代理键的生成策略应保证在分布式环境下的全局唯一性。
// 聚合根实体基类示例(Java)
public abstract class AggregateRoot<TId> {
private TId id;
private int version; // 乐观锁版本号
protected AggregateRoot(TId id) {
this.id = id;
this.version = 0;
}
public TId getId() { return id; }
// 重写equals/hashCode基于id
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AggregateRoot<?> that = (AggregateRoot<?>) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
4.2 全局访问入口(Global Access Entry)
聚合根是外部访问聚合内部实体的唯一入口。任何对聚合内部对象的操作都必须通过聚合根的方法进行,不允许外部直接获取内部实体引用后绕过聚合根修改状态。
核心约束:聚合根对外暴露的方法应代表完整的业务操作(Command),而非简单的属性读取。外部调用者只能通过聚合根的方法触发业务逻辑,聚合根负责协调内部实体完成状态变更。
// 正确的聚合根设计:通过方法封装业务操作
public class Order extends AggregateRoot<OrderId> {
private List<OrderLineItem> items;
private OrderStatus status;
private Money totalAmount;
// 外部只能通过此方法添加商品,不能直接操作items列表
public void addItem(ProductId productId, int quantity, Money unitPrice) {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("只能向待支付订单添加商品");
}
OrderLineItem item = new OrderLineItem(productId, quantity, unitPrice);
items.add(item);
recalculateTotalAmount();
}
private void recalculateTotalAmount() {
this.totalAmount = items.stream()
.map(OrderLineItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
// 不对外暴露items的引用
// public List<OrderLineItem> getItems() { return items; } // 禁止!
}
4.3 内部一致性保证(Internal Consistency Guarantee)
聚合根负责维护聚合内部所有实体和值对象之间的业务规则与不变量(Invariants)。任何状态变更都必须经过聚合根的验证逻辑,确保聚合始终处于有效状态。
- 不变量检查:在聚合根的方法入口处验证业务规则,如订单总金额必须大于零、库存不能为负数。
- 事务边界:一个聚合对应一个事务,聚合内部的状态变更要么全部成功,要么全部回滚。
- 版本控制:使用乐观锁(版本号)防止并发修改导致的数据不一致。
常见陷阱:将多个聚合的操作放在同一个数据库事务中,试图保证跨聚合的一致性。这违背了聚合边界原则,应改用最终一致性(事件驱动)处理跨聚合的业务流程。
// 内部一致性保证示例
public class Inventory extends AggregateRoot<InventoryId> {
private int availableQuantity;
private int reservedQuantity;
public void reserve(int quantity) {
// 不变量:可用库存必须足够
if (quantity <= 0) {
throw new IllegalArgumentException("预留数量必须为正数");
}
if (availableQuantity - reservedQuantity < quantity) {
throw new InsufficientInventoryException("库存不足");
}
this.reservedQuantity += quantity;
// 版本号自动递增,用于乐观锁
incrementVersion();
}
public void confirmReservation(int quantity) {
if (quantity <= 0 || quantity > reservedQuantity) {
throw new IllegalArgumentException("确认数量无效");
}
this.availableQuantity -= quantity;
this.reservedQuantity -= quantity;
incrementVersion();
}
}
4.4 外部引用规则(External Reference Rules)
聚合之间的引用必须遵循严格规则,避免产生紧耦合和跨聚合的事务依赖。
| 规则 | 说明 | 示例 |
|---|---|---|
| 引用标识而非对象 | 聚合A引用聚合B时,只保存B的ID,不保存B的对象引用 | Order 中保存 CustomerId,而非 Customer 对象 |
| 最终一致性 | 跨聚合的状态同步通过领域事件实现,不依赖强事务 | 订单创建后发布 OrderCreatedEvent,库存服务消费后扣减库存 |
| 禁止跨聚合修改 | 一个聚合根的方法不能直接修改另一个聚合的内部状态 | Order 不能直接调用 Inventory.reserve(),应通过应用服务协调 |
| 只读查询优化 | 查询场景可建立独立的读模型,绕过聚合根直接获取数据 | CQRS 模式中的查询模型直接查询数据库视图 |
// 外部引用规则:只保存ID,不保存对象引用
public class Order extends AggregateRoot<OrderId> {
private CustomerId customerId; // 正确:只保存ID
// private Customer customer; // 错误:保存了对象引用
// 通过ID获取客户信息(通过应用服务或仓储)
public CustomerId getCustomerId() {
return customerId;
}
}
// 应用服务协调跨聚合操作
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final InventoryRepository inventoryRepository;
private final EventBus eventBus;
@Transactional
public void createOrder(CreateOrderCommand cmd) {
Order order = new Order(cmd.getCustomerId());
order.addItem(cmd.getProductId(), cmd.getQuantity(), cmd.getUnitPrice());
orderRepository.save(order);
// 发布领域事件,由库存服务最终一致性处理
eventBus.publish(new OrderCreatedEvent(
order.getId(), cmd.getProductId(), cmd.getQuantity()
));
}
}
4.5 聚合根设计原则全景图
以下SVG图展示了四项原则在聚合根设计中的协同关系,以及它们如何共同保障聚合边界与微服务一致性。
4.6 原则冲突与权衡
在实际建模中,四项原则之间可能出现冲突,需要根据业务场景进行权衡。
| 冲突场景 | 涉及原则 | 权衡策略 |
|---|---|---|
| 查询性能 vs 全局入口 | 全局访问入口 vs 外部引用规则 | 引入CQRS,查询走独立读模型,命令仍通过聚合根 |
| 强一致性 vs 最终一致性 | 内部一致性 vs 外部引用规则 | 同一聚合内强一致,跨聚合最终一致 |
| 聚合大小 vs 事务性能 | 内部一致性 vs 唯一标识 | 合理拆分聚合,避免超大聚合导致事务冲突 |
决策框架:当原则冲突时,优先保证聚合内部的一致性(原则3),其次是聚合根的入口约束(原则2),再次是外部引用规则(原则4),最后考虑标识设计(原则1)的优化。因为内部一致性一旦破坏,整个领域模型的可靠性将不复存在。
4.7 微服务边界映射
聚合根的设计直接决定了微服务的拆分粒度。一个微服务通常包含一个或多个聚合,但聚合之间的引用必须通过服务接口(API)进行,而非直接共享数据库。
关键结论:聚合根的设计原则与微服务边界高度一致。遵循四项原则设计的聚合根,天然适合作为微服务的核心领域模型,其边界即为微服务的拆分依据。跨聚合的引用通过ID和领域事件实现,保证了微服务之间的松耦合与独立演进能力。