第7章 聚合 - 一致性边界 (Aggregates - Consistency Boundaries)
一、导读
7.1 本章学习目标
通过本章的学习,读者将能够:
理解聚合(Aggregate)在 DDD 中的定义与核心概念
掌握聚合根(Aggregate Root)的职责与设计原则
学会设计聚合的一致性边界
理解聚合内外的引用规则
掌握处理跨聚合业务的方法
能够在实际项目中正确设计和划分聚合
理解聚合与事务、性能的关系
掌握聚合设计的常见反模式及避免方法
7.2 为什么本章如此重要
聚合是 DDD 战术设计中最复杂也最重要的模式之一。它定义了领域模型中的一致性边界,明确了哪些对象应该作为一个整体被修改和持久化。正确设计聚合对于保证数据一致性、提高系统性能、降低耦合度至关重要。
在实际开发中,聚合的设计往往是最具挑战性的部分。太大的聚合会导致性能问题、并发冲突和复杂的锁定机制;太小的聚合则可能导致频繁的事务协调、数据不一致和复杂的业务逻辑。找到合适的平衡点是聚合设计的关键。
许多团队在使用 DDD 时,最容易出错的地方就是聚合设计。常见的错误包括:将所有相关对象都放在一个大聚合中,导致"上帝聚合";或者过度拆分聚合,导致业务操作需要协调多个聚合,增加了复杂性。这两种极端都会带来严重的问题。
聚合还与事务边界密切相关。在传统的数据库应用中,事务通常跨越多个表;而在 DDD 中,一个事务应该只修改一个聚合。这种转变需要开发人员重新思考数据一致性的实现方式,从强一致性转向最终一致性。
此外,聚合设计直接影响系统的可扩展性和性能。良好的聚合设计可以使系统更容易水平扩展,减少分布式事务的需求;糟糕的聚合设计则会导致性能瓶颈和扩展困难。因此,掌握聚合设计是构建高质量领域模型的核心技能。
7.3 核心问题引导
在学习本章之前,请思考以下问题:
什么是聚合?它与普通的对象组合有什么区别?
什么是聚合根?聚合根与普通实体有什么不同?
如何确定聚合的边界?什么应该放在聚合内部,什么应该放在外部?
为什么一个事务应该只修改一个聚合?这个原则的实际意义是什么?
如何处理跨聚合的业务操作?如果不能在一个事务中修改多个聚合,如何保证一致性?
聚合的大小应该如何控制?有什么经验法则吗?
聚合内部的对象如何引用其他聚合?可以直接持有引用吗?
聚合设计与微服务划分有什么关系?
二、核心概念详解
7.1 聚合的定义与本质
聚合(Aggregate)是 DDD 中用于定义一致性边界的模式。它将一组相关的领域对象(实体和值对象)组织在一起,作为一个整体进行数据修改。聚合确保在这个边界内的所有对象满足业务不变量(Invariants)。
聚合的本质在于它是一个事务边界和一致性边界。在聚合内部,所有对象的修改必须保证业务规则的一致性;在聚合外部,只能通过聚合根来访问和操作聚合内部的对象。
Eric Evans 在《领域驱动设计》中提出了聚合的几个关键原则:
首先,每个聚合有一个根(Root),称为聚合根。聚合根是一个实体,它是外部对象与聚合内部交互的唯一入口。外部对象只能持有聚合根的引用,不能直接引用聚合内部的其他对象。
其次,聚合是一个一致性边界。聚合内部的任何修改都必须保证业务不变量的满足。如果某个操作会破坏不变量,整个操作应该失败。
第三,聚合之间的引用应该通过标识(ID)而非对象引用。这样可以明确聚合的边界,避免意外的跨聚合操作。
第四,聚合应该是小型的。大型聚合会带来性能、并发和复杂性方面的问题。
7.2 聚合根的职责
聚合根(Aggregate Root)是聚合中唯一的对外接口,它具有以下职责:
唯一访问入口
外部对象只能通过聚合根来访问聚合内部的对象。聚合根控制对内部对象的访问,确保内部状态不会被意外修改。
不变量守护者
聚合根负责确保聚合内部的所有业务不变量得到满足。当外部请求修改聚合状态时,聚合根验证请求的合法性,确保不会破坏业务规则。
生命周期管理者
聚合根管理聚合内部对象的生命周期。内部对象的创建、修改和删除都由聚合根控制。
持久化单元
聚合作为一个整体被持久化。当聚合根被保存时,聚合内部的所有对象也应该被保存。同样,当聚合根被删除时,内部对象也应该被删除。
需要注意的是,聚合根并不是要包揽所有工作。它可以委托内部对象执行具体的业务逻辑,但最终的验证和责任由聚合根承担。
7.3 聚合的设计原则
设计良好的聚合应该遵循以下原则:
保护不变量
聚合的首要职责是保护业务不变量。不变量是业务规则中必须始终满足的条件,如"订单总额等于各订单项小计之和"、"账户余额不能为负"等。
在设计聚合时,首先要识别相关的不变量,然后确保聚合的边界能够覆盖这些不变量。如果两个对象之间有强一致性的要求,它们应该在同一个聚合中。
保持小型
聚合应该尽可能小。小型的聚合有以下优势:
- 更好的并发性能:锁定的粒度更小,并发冲突更少
- 更简单的事务:单个聚合可以放在一个事务中
- 更容易理解:小型聚合的逻辑更清晰
- 更好的可扩展性:小型聚合更容易分布在不同的节点上
Vaughn Vernon 建议聚合应该只包含 3-5 个对象。这是一个经验法则,但不是绝对的限制。关键是权衡一致性和性能。
通过标识引用其他聚合
聚合之间不应该有对象引用,而应该通过标识(ID)来引用。例如,订单聚合中的客户字段应该存储客户 ID,而不是客户对象的引用。
这样做有几个好处:
- 明确聚合边界:通过 ID 引用清楚地表明了聚合的分离
- 避免加载整个对象图:不需要为了访问一个聚合而加载所有关联的聚合
- 支持分布式部署:不同聚合可以存储在不同的数据库中
聚合根是唯一的外部接口
外部对象只能通过聚合根来访问聚合内部。聚合内部的实体和值对象不应该被外部直接引用。
这意味着聚合内部的对象不需要全局唯一的标识,它们的标识只需要在聚合内部唯一即可。
延迟加载内部集合
如果聚合内部包含大量子对象,考虑使用延迟加载。只有在需要时才加载这些子对象,避免不必要的性能开销。
但要注意,延迟加载可能会破坏聚合的封装。更好的做法是通过聚合根提供查询方法,而不是暴露内部集合。
7.4 聚合边界的确定
确定聚合边界是聚合设计中最具挑战性的部分。以下是一些指导原则:
基于不变量
如果两个对象之间有强一致性的要求,即一个对象的修改必须同时修改另一个对象以保持业务规则,那么它们应该在同一个聚合中。
例如,订单和订单项之间有强一致性:订单项的变化会影响订单总额。因此,它们应该在同一个聚合中。
基于生命周期
如果两个对象的生命周期紧密绑定,即一个对象创建时另一个也创建,一个对象删除时另一个也删除,那么它们可能在同一个聚合中。
例如,订单项的生命周期依赖于订单,没有订单就没有订单项。因此,订单项应该在订单聚合内部。
基于访问模式
如果两个对象总是被一起访问和修改,那么它们可能在同一个聚合中。如果它们经常被独立访问和修改,那么它们可能是不同的聚合。
基于业务概念
聚合应该对应一个完整的业务概念。例如,"订单"是一个完整的业务概念,包括订单头和订单项;"产品"是一个完整的业务概念,包括产品基本信息和规格。
7.5 跨聚合的一致性
当一个业务操作涉及多个聚合时,如何保证一致性是一个重要问题。DDD 推荐的方式是使用最终一致性,而不是强一致性。
领域事件实现最终一致性
当一个聚合的状态发生变化时,它可以发布领域事件。其他聚合可以订阅这些事件,并相应地更新自己的状态。
例如,当库存聚合扣减库存后,发布 StockReduced 事件。订单聚合订阅这个事件,更新订单的库存状态。
这种方式的好处是:
- 每个聚合有自己的事务,避免了分布式事务
- 聚合之间松耦合,可以独立演进
- 支持异步处理,提高系统吞吐量
缺点是:
- 存在短暂的不一致窗口
- 需要处理事件丢失、重复等问题
- 调试和追踪更复杂
Saga 模式
对于复杂的跨聚合业务流程,可以使用 Saga 模式。Saga 是一系列本地事务,每个事务更新一个聚合,并通过事件或命令触发下一个事务。
如果某个步骤失败,Saga 会执行补偿操作,撤销之前步骤的影响。
应用层协调
在一些情况下,可以在应用层协调多个聚合的修改。应用服务依次调用各个聚合的方法,然后在最后提交事务。
这种方式适用于简单的跨聚合操作,但对于复杂的流程,可能会导致长事务和锁定问题。
7.6 聚合与事务
在传统数据库设计中,事务可以跨越多个表。但在 DDD 中,推荐一个事务只修改一个聚合。
这个原则的原因包括:
性能
单个聚合的事务可以更短,锁定时间更少,并发性能更好。
可扩展性
单个聚合的事务更容易在分布式环境中实现。跨聚合的事务可能需要分布式事务,代价高昂。
简化设计
限定事务边界使聚合设计更清晰,减少了复杂的依赖关系。
但这并不意味着完全禁止跨聚合事务。在一些场景下,如果性能和一致性要求允许,可以进行适度的跨聚合事务。关键是权衡利弊。
7.7 聚合的反模式
上帝聚合(God Aggregate)
将所有相关对象都放在一个大聚合中,导致聚合过于庞大。上帝聚合会带来严重的性能问题、并发冲突和复杂性。
症状:聚合包含几十个对象,加载缓慢,经常发生并发冲突。
解决:拆分为多个较小的聚合,通过领域事件实现最终一致性。
贫血聚合
聚合只有数据,没有行为。所有的业务逻辑都在服务层。这违背了面向对象的封装原则。
症状:聚合类只有 getter/setter,没有业务方法。
解决:将业务逻辑移回聚合内部,让聚合成为真正的领域对象。
循环引用
聚合 A 引用聚合 B,聚合 B 又引用聚合 A,形成循环。这会导致序列化问题和内存泄漏。
症状:加载一个聚合时无限递归加载其他聚合。
解决:通过 ID 引用打破循环,或使用单向引用。
暴露内部对象
聚合直接返回内部对象的引用,允许外部修改内部状态。这破坏了聚合的封装。
症状:外部代码可以直接修改聚合内部的集合或对象。
解决:返回不可变视图或副本,或通过聚合根提供操作方法。
三、程序员的视角
7.1 在实际项目中设计聚合
作为程序员,在日常工作中设计聚合时,应该注意以下几个方面:
首先,从业务不变量出发。问自己:哪些数据必须保持一致?哪些规则必须始终满足?根据答案来确定聚合的边界。
其次,保持聚合的小型化。如果一个聚合包含太多对象,考虑是否可以拆分。记住,小型聚合更容易维护和扩展。
第三,确保聚合根是唯一的访问入口。外部对象只能通过聚合根来操作聚合内部。不要在聚合内部的对象上提供公开的可变方法。
第四,使用 ID 引用其他聚合。不要直接在聚合中持有其他聚合的对象引用。这样可以明确聚合边界,避免意外的级联加载。
第五,在聚合根中集中验证逻辑。当外部请求修改聚合状态时,聚合根应该验证请求的合法性,确保不会破坏不变量。
第六,考虑并发场景。如果多个用户可能同时修改同一个聚合,需要设计合适的并发控制机制,如乐观锁。
7.2 常见陷阱与误区
在设计聚合时,常见的陷阱包括:
陷阱一:过度追求强一致性。试图在一个事务中修改多个聚合,导致性能问题和扩展困难。应该接受最终一致性,使用领域事件。
陷阱二:聚合过大。将太多对象放入一个聚合,导致加载缓慢、并发冲突。应该保持聚合小型。
陷阱三:聚合过小。每个实体都是一个独立的聚合,导致业务操作需要协调多个聚合。应该将强一致性的对象放在同一聚合中。
陷阱四:暴露内部状态。直接返回聚合内部对象的引用,允许外部修改。应该返回不可变视图或通过聚合根操作。
陷阱五:忽视不变量。没有在聚合根中验证业务规则,导致数据不一致。应该在聚合根中集中验证逻辑。
陷阱六:混淆聚合与模块。聚合是运行时的一致性边界,模块是代码的组织方式。两者有不同的目的。
7.3 与其他模式的协作
聚合与其他 DDD 模式紧密协作:
与实体协作:聚合根是一个特殊的实体。聚合内部可以包含其他实体和值对象。
与值对象协作:值对象常用于聚合内部,提供不可变的数据结构。
与仓储协作:仓储负责聚合的持久化和检索。仓储的操作单位是聚合,而不是单个实体。
与领域服务协作:领域服务可以协调多个聚合的操作,但不直接修改聚合内部状态。
与领域事件协作:聚合在状态变化时发布领域事件,通知其他组件。
四、代码实战
7.1 Java 示例:订单聚合
/**
* 订单聚合根
*
* 订单聚合包括订单本身和订单项,它们作为一个整体被修改。
* 订单总额必须等于各订单项小计之和,这是一个不变量。
*/
public class Order extends AggregateRoot {
private final OrderId id;
private final CustomerId customerId;
private OrderStatus status;
private final List<OrderItem> items;
private Money totalAmount;
private ShippingAddress shippingAddress;
public Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.CREATED;
this.items = new ArrayList<>();
this.totalAmount = Money.zero();
}
/**
* 添加商品项
* 这个方法确保订单总额的正确性(不变量)
*/
public void addItem(Product product, int quantity) {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("Can only add items to created orders");
}
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
OrderItem item = new OrderItem(product.getId(), product.getName(), product.getPrice(), quantity);
this.items.add(item);
// 重新计算总额,确保不变量
recalculateTotal();
}
/**
* 移除商品项
*/
public void removeItem(ProductId productId) {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("Can only remove items from created orders");
}
boolean removed = this.items.removeIf(item -> item.getProductId().equals(productId));
if (removed) {
recalculateTotal();
}
}
/**
* 确认订单
* 确认后不能再修改
*/
public void confirm() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("Order can only be confirmed from CREATED status");
}
if (items.isEmpty()) {
throw new IllegalStateException("Cannot confirm empty order");
}
this.status = OrderStatus.CONFIRMED;
// 发布领域事件
registerEvent(new OrderConfirmedEvent(id, customerId, totalAmount));
}
/**
* 取消订单
*/
public void cancel(CancellationReason reason) {
if (status == OrderStatus.DELIVERED) {
throw new IllegalStateException("Cannot cancel delivered order");
}
this.status = OrderStatus.CANCELLED;
// 发布领域事件
registerEvent(new OrderCancelledEvent(id, reason));
}
/**
* 更新配送地址
*/
public void updateShippingAddress(ShippingAddress address) {
if (status != OrderStatus.CREATED && status != OrderStatus.CONFIRMED) {
throw new IllegalStateException("Can only update address for created or confirmed orders");
}
this.shippingAddress = address;
}
/**
* 重新计算订单总额
* 这是维护不变量的关键方法
*/
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::subtotal)
.reduce(Money.zero(), Money::add);
}
// Getters - 注意返回不可变视图
public OrderId getId() { return id; }
public CustomerId getCustomerId() { return customerId; }
public OrderStatus getStatus() { return status; }
public List<OrderItem> getItems() {
return Collections.unmodifiableList(items);
}
public Money getTotalAmount() { return totalAmount; }
public Optional<ShippingAddress> getShippingAddress() {
return Optional.ofNullable(shippingAddress);
}
}
/**
* 订单项 - 聚合内部的实体
*
* 订单项只在订单聚合内部有意义,不需要全局唯一标识。
*/
public class OrderItem {
private final ProductId productId;
private final String productName;
private final Money unitPrice;
private int quantity;
public OrderItem(ProductId productId, String productName, Money unitPrice, int quantity) {
this.productId = productId;
this.productName = productName;
this.unitPrice = unitPrice;
this.quantity = quantity;
}
public Money subtotal() {
return unitPrice.multiply(quantity);
}
// Getters
public ProductId getProductId() { return productId; }
public String getProductName() { return productName; }
public Money getUnitPrice() { return unitPrice; }
public int getQuantity() { return quantity; }
}
/**
* 聚合根基类
*/
public abstract class AggregateRoot {
private final List<DomainEvent> events = new ArrayList<>();
protected void registerEvent(DomainEvent event) {
events.add(event);
}
public List<DomainEvent> getEvents() {
return Collections.unmodifiableList(events);
}
public void clearEvents() {
events.clear();
}
}7.2 Python 示例:博客聚合
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Optional
import uuid
@dataclass(frozen=True)
class PostId:
"""值对象:文章ID"""
value: str = field(default_factory=lambda: str(uuid.uuid4()))
@dataclass(frozen=True)
class AuthorId:
"""值对象:作者ID"""
value: str
@dataclass(frozen=True)
class CommentId:
"""值对象:评论ID"""
value: str = field(default_factory=lambda: str(uuid.uuid4()))
@dataclass(frozen=True)
class Tag:
"""值对象:标签"""
name: str
def __post_init__(self):
if not self.name or len(self.name.strip()) == 0:
raise ValueError("Tag name cannot be empty")
class Comment:
"""聚合内部实体:评论
评论只在文章聚合内部有意义,不需要全局唯一标识。
"""
def __init__(self, comment_id: CommentId, author_id: str, content: str):
self._id = comment_id
self._author_id = author_id
self._content = content
self._created_at = datetime.now()
self._is_deleted = False
@property
def id(self) -> CommentId:
return self._id
@property
def author_id(self) -> str:
return self._author_id
@property
def content(self) -> str:
return self._content
@property
def is_deleted(self) -> bool:
return self._is_deleted
def delete(self):
"""软删除评论"""
self._is_deleted = True
class BlogPost:
"""聚合根:博客文章
文章聚合包括文章本身和评论。
评论数量必须与实际评论数一致,这是一个不变量。
"""
MAX_COMMENTS_PER_POST = 100 # 每篇文章最多100条评论
def __init__(self, post_id: PostId, author_id: AuthorId, title: str):
self._id = post_id
self._author_id = author_id
self._title = title
self._content = ""
self._tags: List[Tag] = []
self._comments: List[Comment] = []
self._comment_count = 0
self._published = False
self._created_at = datetime.now()
self._updated_at = self._created_at
@property
def id(self) -> PostId:
return self._id
@property
def title(self) -> str:
return self._title
@property
def published(self) -> bool:
return self._published
def update_content(self, content: str):
"""更新文章内容"""
if self._published:
raise RuntimeError("Cannot modify published post content")
self._content = content
self._touch()
def add_tag(self, tag: Tag):
"""添加标签"""
if tag not in self._tags:
self._tags.append(tag)
self._touch()
def remove_tag(self, tag: Tag):
"""移除标签"""
if tag in self._tags:
self._tags.remove(tag)
self._touch()
def publish(self):
"""发布文章"""
if not self._content:
raise RuntimeError("Cannot publish empty post")
self._published = True
self._touch()
def add_comment(self, author_id: str, content: str) -> Comment:
"""添加评论
确保评论数量不超过限制(不变量)
"""
if not self._published:
raise RuntimeError("Cannot comment on unpublished post")
if len(self._comments) >= self.MAX_COMMENTS_PER_POST:
raise RuntimeError("Comment limit reached")
if not content or len(content.strip()) == 0:
raise ValueError("Comment content cannot be empty")
comment = Comment(CommentId(), author_id, content)
self._comments.append(comment)
self._comment_count = len(self._comments) # 维护不变量
self._touch()
return comment
def delete_comment(self, comment_id: CommentId):
"""删除评论"""
comment = next((c for c in self._comments if c.id == comment_id), None)
if comment:
comment.delete()
# 重新计算评论数,确保不变量
self._comment_count = sum(1 for c in self._comments if not c.is_deleted)
self._touch()
def get_comments(self) -> List[Comment]:
"""获取有效评论列表"""
return [c for c in self._comments if not c.is_deleted]
@property
def comment_count(self) -> int:
return self._comment_count
@property
def tags(self) -> List[Tag]:
return list(self._tags) # 返回副本
def _touch(self):
"""更新时间戳"""
self._updated_at = datetime.now()
# 使用示例
def blog_post_example():
"""演示博客文章聚合的使用"""
# 创建文章
post = BlogPost(PostId(), AuthorId("A001"), "My First Post")
# 更新内容
post.update_content("Hello, World!")
# 添加标签
post.add_tag(Tag("Python"))
post.add_tag(Tag("DDD"))
# 发布文章
post.publish()
# 添加评论
comment = post.add_comment("U001", "Great post!")
print(f"Comment count: {post.comment_count}")
# 删除评论
post.delete_comment(comment.id)
print(f"Comment count after deletion: {post.comment_count}")7.3 跨聚合操作用领域事件
/**
* 库存聚合根
*/
public class Inventory extends AggregateRoot {
private final ProductId productId;
private int availableQuantity;
public Inventory(ProductId productId, int initialQuantity) {
this.productId = productId;
this.availableQuantity = initialQuantity;
}
/**
* 扣减库存
*/
public void reduceStock(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
if (quantity > availableQuantity) {
throw new InsufficientStockException(productId, availableQuantity, quantity);
}
this.availableQuantity -= quantity;
// 发布领域事件
registerEvent(new StockReducedEvent(productId, quantity, availableQuantity));
}
public ProductId getProductId() { return productId; }
public int getAvailableQuantity() { return availableQuantity; }
}
/**
* 库存扣减事件
*/
public class StockReducedEvent extends DomainEvent {
private final ProductId productId;
private final int reducedQuantity;
private final int remainingQuantity;
public StockReducedEvent(ProductId productId, int reducedQuantity, int remainingQuantity) {
this.productId = productId;
this.reducedQuantity = reducedQuantity;
this.remainingQuantity = remainingQuantity;
}
// Getters
public ProductId getProductId() { return productId; }
public int getReducedQuantity() { return reducedQuantity; }
public int getRemainingQuantity() { return remainingQuantity; }
}
/**
* 订单应用服务
*
* 协调订单聚合和库存聚合的操作
*/
@Service
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final InventoryRepository inventoryRepository;
private final DomainEventPublisher eventPublisher;
@Transactional
public CreateOrderResult createOrder(CreateOrderRequest request) {
// 创建订单聚合
Order order = orderFactory.createOrder(request);
// 对每个商品,扣减库存(单独的聚合)
for (OrderItem item : order.getItems()) {
Inventory inventory = inventoryRepository.findByProductId(item.getProductId());
inventory.reduceStock(item.getQuantity());
inventoryRepository.save(inventory);
}
// 保存订单
orderRepository.save(order);
// 发布订单创建事件
for (DomainEvent event : order.getEvents()) {
eventPublisher.publish(event);
}
return CreateOrderResult.success(order.getId());
}
}
/**
* 库存事件订阅者
*
* 订阅库存扣减事件,更新其他相关数据
*/
@Component
public class StockReducedSubscriber implements EventSubscriber {
private final ProductSearchIndex searchIndex;
@Override
public boolean supports(Class<? extends DomainEvent> eventType) {
return StockReducedEvent.class.isAssignableFrom(eventType);
}
@Override
public void handle(DomainEvent event) {
StockReducedEvent stockEvent = (StockReducedEvent) event;
// 更新搜索索引中的库存信息
searchIndex.updateStock(
stockEvent.getProductId(),
stockEvent.getRemainingQuantity()
);
}
}五、深入思考
7.1 历史背景
聚合的概念源于数据库中的事务和一致性理论。在传统的关系数据库中,事务可以跨越多个表,保证 ACID 属性。但随着系统规模的扩大,这种强一致性的代价变得难以承受。
Eric Evans 在《领域驱动设计》中引入了聚合的概念,将一致性边界限定在较小的范围内。他指出,大多数业务场景不需要强一致性,最终一致性就足够了。这一观点对后来的分布式系统设计产生了深远影响。
随后,Pat Helland 发表了著名的论文《Life Beyond Distributed Transactions》,进一步阐述了为什么在大规模系统中应该避免分布式事务。这篇论文成为聚合设计的重要理论基础。
7.2 前沿方向
聚合的实践正在向以下几个方向发展:
首先是更小的聚合。随着微服务和分布式系统的普及,聚合变得越来越小。一些实践者甚至建议聚合只包含一个实体,通过领域事件实现一致性。
其次是事件溯源与聚合的结合。在事件溯源中,聚合的状态由事件推导而来。这使得聚合可以更轻量,因为不需要持久化当前状态。
第三是 CQRS 模式的应用。在 CQRS 中,写模型使用聚合保证一致性,读模型通过投影提供高效的查询。这种分离简化了聚合的设计。
第四是与响应式编程的结合。通过响应式流,可以更优雅地处理聚合发布的事件,实现异步的反应链。
7.3 延伸阅读
Eric Evans, 《领域驱动设计》第6章 - 原始论述
Vaughn Vernon, 《实现领域驱动设计》第10章 - 实用指南
Pat Helland, 《Life Beyond Distributed Transactions》- 论文
Martin Fowler, 《Aggregate Root》- 在线文章
Udi Dahan, 《Don't Create Aggregate Roots》- 博客文章
六、实验笔记
实验一:分析和重构现有聚合
实验目的:通过分析现有系统,练习识别聚合设计的问题。
实验步骤:
选择一个现有项目,找出其中的领域对象。
识别当前的聚合边界:哪些对象属于同一个聚合?聚合根是谁?
分析以下问题:
- 聚合是否过大或过小?
- 是否有跨聚合的强一致性要求?
- 聚合之间是否有对象引用?
- 是否有暴露内部状态的情况?
提出改进方案:如何重新划分聚合?如何使用领域事件替代强一致性?
实施重构,验证改进效果。
预期收获:培养识别聚合设计问题的能力,掌握重构技巧。
实验二:设计一个电商系统的聚合
实验目的:从零开始设计聚合,练习聚合边界的确定。
实验步骤:
确定电商系统的核心聚合:订单、产品、库存、客户等。
对每个聚合,确定:
- 聚合根是谁?
- 内部包含哪些实体和值对象?
- 有哪些不变量需要保护?
- 如何引用其他聚合?
设计跨聚合的业务流程,如"下单"涉及订单、库存、支付等多个聚合。
使用领域事件实现跨聚合的最终一致性。
编写代码实现关键聚合,包括不变量验证。
编写测试用例验证聚合的行为。
预期收获:掌握聚合设计的完整流程,学习处理跨聚合业务。
七、本章小结
本章详细介绍了 DDD 中的聚合模式。我们首先定义了聚合的本质:一致性边界,将相关对象组织在一起作为一个整体被修改。然后,我们探讨了聚合根的职责:唯一访问入口、不变量守护者、生命周期管理者。
聚合的设计原则是本章的重点内容。我们强调了保护不变量、保持小型、通过标识引用其他聚合、聚合根是唯一外部接口等原则。这些原则帮助设计出高质量的聚合。
聚合边界的确定是聚合设计中最具挑战性的部分。我们提供了基于不变量、生命周期、访问模式和业务概念的指导原则。
跨聚合的一致性通过领域事件实现最终一致性,而不是强一致性。我们讨论了领域事件、Saga 模式和应用层协调等方法。
通过代码实战,我们展示了如何实现订单聚合、博客聚合,以及跨聚合操作用领域事件。这些示例体现了聚合的最佳实践:不变量保护、小型化、ID 引用。
最后,我们回顾了聚合的历史背景,展望了前沿方向,并提供了两个实验任务帮助读者实践所学内容。
聚合是 DDD 战术设计的核心。正确设计聚合可以保证数据一致性、提高系统性能、降低耦合度。希望读者能够在实际项目中应用本章的知识,设计出优雅的聚合。
八、习题
概念理解题:用自己的话解释什么是聚合,它的核心作用是什么?
辨析题:以下对象组合哪些应该在同一个聚合中?说明理由。
- 订单和订单项
- 用户和用户地址
- 产品和产品评论
- 航班和乘客预订
- 博客文章和标签
设计题:为一个图书馆系统设计聚合。包括图书、会员、借阅记录等概念。确定聚合边界、聚合根和不变量。
代码分析题:以下聚合设计有什么问题?如何改进?
public class Order {
private Long id;
private Customer customer; // 直接持有对象引用
private List<OrderItem> items;
public Customer getCustomer() {
return customer; // 暴露内部引用
}
public List<OrderItem> getItems() {
return items; // 暴露可变集合
}
}思考题:为什么推荐一个事务只修改一个聚合?这个原则在实际项目中是否总是适用?
实践题:在你当前参与的项目中,找出一个聚合,分析它的设计是否合理。如果有问题,提出改进方案。
论述题:聚合与数据库事务有什么区别?在分布式系统中,聚合的一致性如何保证?
设计题:设计一个"会议室预订"聚合。包括会议室、时间段、预订人等概念。考虑并发预订的场景,设计合适的不变量和并发控制。
开放题:在微服务架构中,聚合与服务边界有什么关系?一个微服务可以包含多个聚合吗?
综合题:为一个在线拍卖系统设计完整的聚合模型。包括商品、出价、用户、拍卖会话等概念。画出聚合图,说明每个聚合的不变量和跨聚合的一致性策略。