7领域驱动设计

聚合

一致性边界

聚合聚合根一致性边界

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 示例:订单聚合

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 示例:博客聚合

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 跨聚合操作用领域事件

java
/**
 * 库存聚合根
 */
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 战术设计的核心。正确设计聚合可以保证数据一致性、提高系统性能、降低耦合度。希望读者能够在实际项目中应用本章的知识,设计出优雅的聚合。


八、习题

概念理解题:用自己的话解释什么是聚合,它的核心作用是什么?

辨析题:以下对象组合哪些应该在同一个聚合中?说明理由。

- 订单和订单项

- 用户和用户地址

- 产品和产品评论

- 航班和乘客预订

- 博客文章和标签

设计题:为一个图书馆系统设计聚合。包括图书、会员、借阅记录等概念。确定聚合边界、聚合根和不变量。

代码分析题:以下聚合设计有什么问题?如何改进?

java
public class Order {
    private Long id;
    private Customer customer;  // 直接持有对象引用
    private List<OrderItem> items;
    
    public Customer getCustomer() {
        return customer;  // 暴露内部引用
    }
    
    public List<OrderItem> getItems() {
        return items;  // 暴露可变集合
    }
}

思考题:为什么推荐一个事务只修改一个聚合?这个原则在实际项目中是否总是适用?

实践题:在你当前参与的项目中,找出一个聚合,分析它的设计是否合理。如果有问题,提出改进方案。

论述题:聚合与数据库事务有什么区别?在分布式系统中,聚合的一致性如何保证?

设计题:设计一个"会议室预订"聚合。包括会议室、时间段、预订人等概念。考虑并发预订的场景,设计合适的不变量和并发控制。

开放题:在微服务架构中,聚合与服务边界有什么关系?一个微服务可以包含多个聚合吗?

综合题:为一个在线拍卖系统设计完整的聚合模型。包括商品、出价、用户、拍卖会话等概念。画出聚合图,说明每个聚合的不变量和跨聚合的一致性策略。

7/12聚合
0%