在 DDD 中设计聚合时的经验法则是什么?
根据 Martin Fowler 的说法,聚合是可以被视为单个单元的域对象的集群。聚合将其组件对象之一作为聚合根。
https://martinfowler.com/bliki/DDD_Aggregate.html
在设计了大约 20 个 DDD 项目之后,我仍然对选择创建聚合的域对象时的经验法则感到困惑。
Martin Fowler 使用订单和行项目进行类比,我认为这不是一个很好的例子,因为订单+行项目实际上是紧密绑定(bind)的对象。在这个例子中不需要考虑太多。
让我们尝试用汽车做类比,其中 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;}
}
替代方案可能是这样的(示例 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 可能是一个非常大的类
那么是什么让我们将聚合拆分成新的聚合呢?尺寸?事务的原子性(尽管这不是问题,因为同一子域的聚合通常位于同一服务器上)
最佳答案
小心不要有过于强烈的面向对象思维。蓝皮书和 Martin Fowler 的帖子有点旧了,而且它提供的视野太狭窄。
聚合不一定是类。不需要坚持。这些是实现细节。甚至,有时,聚合所做的事情并不意味着改变,只是意味着“好的,可以完成这个操作”。
iTollu 帖子为您提供了一个良好的开端:重要的是事务边界。聚合的工作只是其中之一。确保操作中的不变量和域规则,在大多数情况下(记住并非总是如此),更改必须持久的数据。事务性边界意味着一旦总体表明某件事可以并且已经完成;世界上没有任何东西应该与它相矛盾,因为如果发生矛盾,那么你的聚合设计得很糟糕,与聚合相矛盾的规则应该是聚合的一部分。
因此,为了设计聚合,我通常从非常简单的开始并不断发展。想象一个静态函数,它接收检查操作的域规则所需的所有 VO、实体和命令数据(几乎是 DTO 所有数据,除了实体的唯一 ID),并返回一个域事件,表明某件事已经完成。事件的数据必须包含系统保存更改(如果需要)所需的所有数据,以及当事件到达其他聚合(在相同或不同的有界上下文中)时采取相应行动的数据。
现在开始重构和OO设计。抑制原始的痴迷反模式。添加约束以避免实体和 VO 的错误状态。用于检查或计算与实体相关的某些内容的代码更好地放入实体中。 Put your events in a diet 。将需要几乎相同的 VO 和实体来检查域规则的静态函数放在一起,创建一个类作为聚合根。使用存储库创建始终有效状态的聚合。还有很长一段时间等等。你知道;只是良好的 OOP 设计,走向无 DTO、“告诉,不要问”前提、责任分离等等。
当您完成所有工作后,您会发现您的聚合、VO 和实体是从域(有界上下文相关)和技术 View 完美设计的。
关于domain-driven-design - 如何在 DDD 中正确定义聚合?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51243959/