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

时间:2018-07-09 10:58:24

标签: domain-driven-design ddd-repositories

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

根据马丁·福勒(Martin Fowler)的观点,聚合是域对象的群集,可以将它们视为一个单元。聚合将具有其组成对象之一作为聚合根。

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

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

马丁·福勒(Martin Fowler)使用顺序和订单项类比,但我认为这不是一个好例子,因为订单+订单项实际上是紧密绑定的对象。在该示例中没有太多思考。

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

CarContent将至少包含一个或多个聚合。

例如,我们有这个Ag​​gregateRoot(我将其保持尽可能简单)

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;}
}

替代方法可能是这样(示例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;}
}

可以在没有设备的情况下创建汽车,但在没有设备的情况下不能激活/发布汽车(即,可以在两个不同的交易中填充)

设备可以在示例A中通过CarStructureAggregate引用,也可以在示例B中使用CarEquipmentAggregate引用。

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

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

  • 照片
  • 说明
  • 也许是有关引擎的更多数据

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

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

4 个答案:

答案 0 :(得分:4)

要注意不要太过坚强的面向对象思想。蓝皮书和马丁·福勒(Martin Fowler)的职位有些陈旧,它提供的视野太狭窄。

聚合不必是类。它不需要持久化。这些是实施细节。甚至有时,聚合执行的操作并不意味着更改,而只是暗示“可以执行此操作”。

iTollu的帖子为您提供了一个好的开始:重要的是事务边界。聚合的工作仅仅是一项。在大多数情况下(请记住并非总是如此),请确保操作中的不变式和域规则会更改必须保留的数据。事务边界意味着一旦汇总表明某事可能且已经完成;世界上没有任何东西应与之矛盾,因为如果发生矛盾,则您的集合体设计不当,与集合体矛盾的规则应成为集合体的一部分。

因此,要设计集合体,我通常会非常简单并不断发展。考虑一个静态函数,该函数可以获取检查操作的域规则并返回域事件表示已完成操作所需的所有VO,实体和命令数据(除实体的唯一ID外,几乎都是DTO)。事件的数据必须包含系统必要的所有数据,以保留更改(如果需要),并在事件到达其他聚合(在相同或不同的有界上下文中)时采取行动。

现在开始重构和OO设计。 Supress原始痴迷反模式。添加约束以避免实体和VO的错误状态。用来检查或计算与实体相关的东西的那段代码更好地进入了实体。 Put your events in a diet。将需要几乎相同的VO和实体的静态函数一起检查域规则,以创建一个类作为聚合根。使用存储库以始终有效的状态创建聚合。还有很长的时间等等。只是好的OOP设计,没有DTO,“告诉,不问”前提,责任分工等等。

完成所有工作后,您会发现从域(与受限制的上下文相关)和技术角度完美设计的聚合,VO和实体。

答案 1 :(得分:2)

在设计聚合时要记住的一点是,同一实体在一个用例中可以是聚合,而在另一个用例中可以是普通实体。因此,您可以拥有拥有EquipmentTypes列表的CarStructureAggregate,但是您也可以拥有拥有其他东西并拥有自己的业务规则的EquipmentTypeAggregate。

但是请记住,聚合可以更新自己的属性,但不能更新拥有的对象的属性。例如,如果您的CarStructureAggregate拥有EquipmentType列表,则无法在更新CarStructureAggregate的上下文中更改其中一种设备类型的属性。您必须以其汇总角色查询EquipmentType才能对其进行更改。 CarStructureAggregate只能将EquipmentTypes添加到其内部列表中或将其删除。

另一条经验法则是,除非有更深层次的原因,否则填充聚集的深度只能是一个级别。在您的示例中,您将实例化CarStructureAggregate并填充EquipmentTypes的列表,但不会填充每个EquipmentType可能拥有的任何列表。

答案 2 :(得分:1)

我相信,这里重要的是交易边界。

一方面,您不能将其建立得过于狭窄而无法保持聚合的一致性。

另一方面,您不想将其设置得太大以阻止用户进行并发修改。

在您的示例中,如果用户应该能够同时修改CarStructure和CarEquipment,那么我将坚持实施B。否则,我将坚持使用A。

答案 3 :(得分:0)

用一个非常简单的句子,我可以说:

基本上,在域驱动设计中,旨在改变并由一个或多个相关实体、值对象和不变量组成的业务用例是聚合的。作为一个模型命令很重要,因为如果你只需要阅读,你就不需要聚合。