在软件开发领域,尤其是在处理复杂的业务逻辑时,领域驱动设计(DDD)提供了一个强大的框架来管理复杂性。

DDD 中的关键概念之一是聚合的概念。这篇博文旨在揭开聚合的神秘面纱,让初学者更容易理解这个强大的概念并将其应用到他们的项目中。

什么是领域驱动设计(DDD)?

在深入探讨聚合之前,我们先简要介绍一下什么是领域驱动设计 (DDD)。

DDD 是一种软件开发方法,强调技术专家和领域专家(了解业务的人)之间的协作。

它专注于创建对业务领域的共同理解并构建准确反映这种理解的软件模型。

 什么是聚合?

在 DDD 中,聚合是被视为单个单元的领域对象的集群。

聚合由一个或多个属于在一起的实体(可能还有值对象)组成。

聚合有助于增强一致性并维护集群内的不变量。每个聚合都有一个根实体,称为聚合根,它是外部对象可以与之交互的唯一实体。

骨料的主要特征:

  1. 一致性边界:聚合内的所有更改都是一致的。这意味着聚合中所有实体的状态在事务结束时是一致的。
  2. 事务边界:聚合定义事务的范围。对聚合的操作应该是原子的。
  3. 身份:只有聚合根才具有全局身份。聚合内的其他实体具有本地身份。
  4. 封装性:外部对象只能与聚合根交互。这有助于保持聚合的完整性。

 聚合示例

让我们考虑一个电子商务应用程序的示例来说明聚合。

示例 1:订单聚合

Order 聚合可能包括:

  • 订单实体(聚合根)
  • OrderItem 实体(聚合的一部分)
  • 支付主体(部分合计)
<span id="4bd7" data-selectable-paragraph="">class Order  
{  
    private $orderId;  
    private $orderItems = [];  
    private $payment;  
  
    public function __construct($orderId)  
    {  
        $this-&gt;orderId = $orderId;  
    }  
    public function addItem(OrderItem $item)  
    {  
        $this-&gt;orderItems[] = $item;  
    }  
    public function setPayment(Payment $payment)  
    {  
        $this-&gt;payment = $payment;  
    }  
    public function getOrderId()  
    {  
        return $this-&gt;orderId;  
    }  
}  
class OrderItem  
{  
    private $itemId;  
    private $quantity;  
    public function __construct($itemId, $quantity)  
    {  
        $this-&gt;itemId = $itemId;  
        $this-&gt;quantity = $quantity;  
    }  
}  
class Payment  
{  
    private $paymentId;  
    private $amount;  
    public function __construct($paymentId, $amount)  
    {  
        $this-&gt;paymentId = $paymentId;  
        $this-&gt;amount = $amount;  
    }  
}

在此示例中, Order 实体是聚合根。外部对象可以与 Order 交互以添加商品或设置付款,但不能直接与 OrderItemPayment 交互。

使用聚合的好处

  1. 保持一致性:聚合确保边界内的所有实体保持一致。
  2. 简化事务:通过定义清晰的事务边界,聚合有助于管理复杂的事务。
  3. 封装:聚合封装内部细节,使管理变更和执行业务规则变得更加容易。

常见陷阱以及如何避免它们

过大的聚合:避免创建包含太多实体的聚合。这可能会导致性能瓶颈和复杂的事务逻辑。

  • 解决方案:将大的聚合体分解为更小、更易于管理的聚合体。确保每个聚合都有明确的目的和重点。

不一致的边界:不一致的聚合边界可能会导致数据不一致,并使维护不变量变得困难。

  • 解决方案:明确定义每个聚合的边界。确保所有相关实体都封装在同一个聚合中。

忽略性能注意事项:太大或访问太频繁的聚合可能会导致性能问题。

  • 解决方案:设计时要考虑性能。考虑读取或写入聚合的频率,并进行相应的优化。

有关聚合的常见问题

1. 总量应该有多大?

聚合应该尽可能小,但又足够大以封装完整的业务概念。如果聚合太大,可能会导致性能问题并增加复杂性。相反,如果它太小,您可能最终会遇到一致性问题和复杂的事务边界。

2. 聚合如何保证一致性?

聚合通过在其边界内强制执行业务规则和不变量来确保一致性。只有聚合根才能修改聚合的状态,这确保了所有更改都受到控制和验证。

3. 如何处理聚合之间的引用?

聚合不应直接引用其他聚合。相反,使用唯一标识符来引用其他聚合。这种方法有助于保持每个聚合的独立性和自主性。

4. 聚合如何与数据库交互?

聚合应作为整体单元加载和保存。使用存储库抽象持久性逻辑并处理聚合的存储和检索。

<span id="eb17" data-selectable-paragraph="">class OrderRepository  
{  
    private $storage = [];  
    public function save(Order $order)  
    {  
        $this-&gt;storage[$order-&gt;orderId] = $order;  
    }  
    public function getById($orderId)  
    {  
        return $this-&gt;storage[$orderId];  
    }  
}

5. 一个聚合可以包含其他聚合吗?

不,聚合不应包含其他聚合。每个聚合都定义了自己的一致性边界,嵌套聚合将违反此原则。相反,使用唯一标识符来引用其他聚合。

 技巧和窍门

  • 保持聚合较小:目标是封装单个业务概念的聚合,以使其易于管理。
  • 使用域事件:发出域事件将聚合内的更改传达到系统的其他部分。
  • 避免 Getters 和 Setters:使用反映业务操作的有意义的方法,而不是简单的 getters 和 setters。