如何在 DDD 中正确定义聚合?

mko*_*mko 6 domain-driven-design ddd-repositories

在 DDD 中设计聚合时的经验法则是什么?

根据 Martin Fowler 的说法,聚合是一组可以被视为单个单元的域对象。聚合将其组件对象之一作为聚合根。

https://martinfowler.com/bliki/DDD_Aggregate.html

在设计了大约 20 个 DDD 项目后,在选择将创建聚合的域对象时,我仍然对经验法则感到困惑。

Martin Fowler 使用订单和订单项进行类比,我认为这不是一个很好的例子,因为订单+订单项确实是紧密绑定的对象。在那个例子中不需要考虑太多。

让我们尝试用汽车类比,其中 CarContent 是汽车经销商域的子域。

CarContent 将由至少一个或多个聚合组成。

例如,我们有这个 AggregateRoot(我尽可能保持简单)

class CarStructureAggregate
{
     public int Id {get; private set;}
     public ModelType ModelType {get; private set;}
     public int Year {get; private set;}
     public List<EquipmentType> {get; private set;}
}
Run Code Online (Sandbox Code Playgroud)

替代方案可能是这个(示例B)

class CarStructureAggregate
{
     public int Id {get; private set;}
     public ModelType ModelType {get; private set;}
     public int Year {get; private set;}
}

class CarEquipmentAggregate
{
    public int Id {get; private set;}
    public List<EquipmentType> {get; private set;}
}
Run Code Online (Sandbox Code Playgroud)

可以在没有设备的情况下创建汽车,但没有设备就无法激活/发布(即,这可以通过两个不同的事务填充)

可以通过示例 A 中的 CarStructureAggregate 或使用示例 B 中的 CarEquipmentAggregate 引用设备。

EquipmentType 可以是枚举,也可以是具有更多类、属性的复杂类。

在示例 A 和 B 之间进行选择时的经验法则是什么?现在想象一下汽车可以有更多的信息,比如

  • 相片
  • 描述
  • 也许更多关于引擎的数据

和 CarStructureAggregate 可能是一个非常大的类

那么是什么让我们将 Aggregate 拆分为新的 Aggregate 呢?尺寸?事务的原子性(尽管这不是问题,因为通常同一子域的聚合通常位于同一服务器上)

jlv*_*ero 5

小心不要有过于强烈的 OO 心态。蓝皮书和马丁福勒的帖子有点旧,它提供的视野太窄了。

聚合不需要是一个类。它不需要持久化。这些是实现细节。甚至,有时,聚合做的事情并不意味着改变,只是意味着“可以完成这个动作”。

iTollu 帖子为您提供了一个良好的开端:重要的是交易边界。聚合的工作只是其中之一。确保操作中的不变量和域规则在大多数情况下(请记住并非总是如此)更改必须持久化的数据。事务边界意味着一旦聚合表明某事可能并且已经完成;世界上没有任何东西应该与它相矛盾,因为如果发生矛盾,则您的聚合设计得很糟糕,与该聚合相矛盾的规则应该是聚合的一部分。

所以,为了设计聚合,我通常从非常简单的开始并不断发展。考虑一个静态函数,它接收检查操作的域规则所需的所有 VO、实体和命令数据(除了实体的唯一 ID 之外,几乎所有这些都是 DTO),并返回一个域事件,表明某些事情已经完成。事件的数据必须包含您的系统需要的所有数据,如果需要,系统需要保留更改,并在事件到达其他聚合(在相同或不同的有界上下文中)时采取相应的行动。

现在开始重构和面向对象设计。抑制原始的痴迷反模式。添加约束以避免实体和 VO 的错误状态。用于检查或计算与实体相关的某些内容的那段代码更好地进入实体。把你的活动放在节食中。将需要几乎相同的 VO 和实体来检查域规则的静态函数放在一起,创建一个类作为聚合根。使用存储库以始终有效的状态创建聚合。还有很长的等等。你知道;只是好的 OOP 设计,没有 DTO,“告诉,不要问”的前提,责任分离等等。

当您完成所有这些工作时,您会发现您的聚合、VO 和实体是从域(有界上下文相关)和技术视图完美设计的。


iTo*_*llu 1

我相信,这里重要的是事务边界。

一方面,您不能将其设置得比足以保持聚合的一致性更窄。

另一方面,您也不希望将其设置得太大而导致用户无法进行并发修改。

在您的示例中,如果用户应该能够同时修改 CarStructure 和 CarEquipment - 那么我会坚持使用实现 B。如果不能 - 使用 A 会更简单。