DDD:在单个事务中创建具有共享生命周期的多个聚合

Jor*_*ker 7 domain-driven-design

据我所知,一般规则是每个事务只应修改一个聚合,主要是为了并发和事务一致性问题。

我有一个用例,我想在单个事务中创建多个聚合: a RestaurantManager、 aRestaurant和 a Menu。它们看起来像是一个单一的聚合体,因为它们的生命周期一起开始和结束:在域内创建RestaurantManager没有 a 的是没有意义的Restaurant,反之亦然;Restauranta和 a也是如此Menu。此外,如果RestaurantRestaurantManager被删除(未注册),则应将它们全部一起删除。

但是,我已将它们拆分为单独的聚合,因为一旦创建,它们就会单独更新,维护自己的不变量,并且我不想将它们全部加载到内存中只是为了更新 上的一个属性Restaurant

将它们联系在一起的唯一因素是它们的生命周期。

我的问题是,这是否代表可以违反每笔交易只能在单个聚合上运行的“规则”。

我还想知道我是否应该通过让每个聚合根保存它所依赖的聚合根的标识符来强制它们在域模型中的共享生命周期,即通过 require RestaurantaMenuId作为构造函数参数,同样对于MenuRestaurantId,这样就无法在没有另一个的情况下创建任何一个。然而,这仍然不会强制应用程序服务将它们保存在一起,因为它可以将它们全部创建在内存中,然后仅保存Menu,例如。

Sub*_*han 6

恕我直言,您的需求是 DDD 中非常正常的用例。总是有多个聚合协同工作来支持应用程序,并且它们在生命周期中是相互关联的。但建模概念仍然有效。让我尝试借助一些 DDD 规则来解释您的模型是什么样子:

聚合是事务边界

聚合确保业务不变量在任何时候都不会被破坏。这意味着,如果您将多个聚合串在一起作为一个事务的一部分,则必须将它们全部加载到内存中以进行验证。

当您的应用程序数据丰富并将数据存储在分区、分布式的数据库集群(例如 Mongo 或 Elasticsearch)中时,这尤其是一个问题。作为单个事务的一部分,您将遇到从可能不同的集群加载数据的问题。

聚合被完整加载

聚合及其关联的数据对象被完整地加载到内存中。这意味着交易的不必要的对象(例如,餐厅下个月的时间表)可能会加载到内存中。就其本身而言,这不是问题。但是当多个聚合聚集在一起时,需要考虑加载到内存中的数据量。

聚合通过其唯一标识符相互引用

这个很简单,意味着每个聚合通过标识符存储其引用的聚合,而不是将其他聚合的数据包含在其中。

跨聚合的状态更改通过域事件处理

如果您希望一个聚合中的状态更改对其他聚合产生副作用,您可以发布域事件,然后订阅者在后台处理其他聚合上的更改。这就是您希望处理级联删除需求的方式。


通过遵循这些规则,您实际上是一次放大一个聚合,并确保复杂性保持在较低水平。当您串联多个聚合时,尽管在第一天就很清楚且易于理解,但最终应用程序往往会变成一个大泥球,因为依赖项和不变量开始相互交叉。

  • 完全没问题,我经常在应用程序中使用两种消息代理:一种是内存中的,另一种是分布式的(RabbitMQ、ZeroMQ、Kafka)。内存中的内容将根据测试配置注入,分布式代理将注入生产中。在开始开发时,我们也经常使用内存代理。唯一需要记住的是,当我们切换到最终一致性时,我们可能必须添加缺失的“纠正策略”,因为较早的事务将会完成。 (3认同)

Jor*_*ker 0

正如 @plalx 所指出的,在根据事务创建聚合时,争用并不那么重要,因为它们还不存在,因此不能参与争用。

至于强制域中多个聚合的相互生命周期,我认为这是应用程序层(即应用程序服务或用例)的责任。

也许我的想法更接近干净或六边形架构,但我认为尝试将每个业务规则推入“域模型”是不可能的,甚至是不明智的。对我来说,域模型的要点是将问题域划分为小块(聚合),其中封装一起更改的常见业务数据/操作,但应用程序层的责任是正确使用这些聚合以实现业务。最终目标(即整个应用程序),包括协调聚合之间的操作并控制其生命周期。

因此,我认为这个东西属于应用程序服务。话虽如此,在每个用例中频繁更新多个聚合可能是域边界不正确的标志。