如何处理(部分)依赖的聚合根?

时间:2018-08-15 14:57:19

标签: domain-driven-design

我有Product的域概念。

Product有一些GeneralDetails,可以说:sku, name, description。 同时,Product有一个ProductCalculations部分,会计师可以在其中输入purchasePrice, stockLevelExpenses, wholeSalesPrice, retailPrice之类的不同值。

到目前为止,Product看起来像这样:

class Product{
   GeneralDetails Details;
   ProductCalculations Calculations;

   ChangeDetails(GeneralDetails details){}
   Recalculate(ProductCalculations calculations{}

}

此设置将使Product成为聚合根。但是现在,我想以产品经理可以输入/更新产品详细信息的方式对其进行拆分,然后让会计师可以介入并间接更改给定产品的计算,而不会出现并发问题。 这建议将其拆分为2个单独的聚合根。

但是,然后删除ProductDetails聚合必须意味着也删除ProductCalculations,并且它应该以事务方式进行。

假设它们是2个聚合根,这意味着它们具有2个带有相应Delete方法的独立存储库,那么如何将其实现为原子事务?

我唯一能想到的就是删除ProductDetails时引发事件,有一个处理程序(DomainService),该处理程序使用一些特殊的存储库来处理多个聚合根上的事务。

该方法是否存在问题,并且/或者是否有更好的处理方法?

PS。 删除ProductDetails时,我不允许最终的一致性。

PS2。 根据来自@Jon的评论,DetailsCalculations的创建和删除应该以创建/删除Details时也应该创建/删除的方式进行同步。 另一方面,它们的更新应该完全独立。

3 个答案:

答案 0 :(得分:2)

我认为您问题的答案在某种程度上取决于您使用的是什么数据存储技术和数据存储模型,因为如果您可以将操作事务性推向数据层,事情会变得容易得多。

如果您使用的是面向文档的数据库(Cosmos DB,MongoDB等),我将建模并存储您的Product聚合(包括DetailsCalculations )作为单个文档,您可以从数据库中免费获得原子事务和并发检查。

如果必须将它们作为单独的文档/记录存储在数据存储中,那么提供原子事务和并发检查将成为您的关注点。多年以来,人们(尤其是使用Entity Framework的人们)一直在使用Unit of Work pattern来批处理多个存储库操作,并将它们作为单个操作(EF-specific UoW implementation)提交给数据库。罗布·科纳里(Rob Conery)建议here更好的选择是使用Command对象来封装需要作为单个事务执行的多部分操作。

无论如何,我鼓励您在Product内保持此操作的管理,以使Product的使用者不知道保存过程中发生了什么-他们只是很乐意地打电话给{{ 1}},他们不需要知道这是导致一条记录更新还是十条记录更新。只要向product.SaveAsync()注入了完成工作所需的存储库,就不需要单独的域服务来协调此操作。 Product侦听其子级引发的事件并做出适当响应是没有问题的。

希望这会有所帮助!

答案 1 :(得分:2)

  

“删除ProductDetails时,我不允许最终的一致性”

为什么不呢?拥有Inventory.Product而没有Finance.Product时,拥有responsedata = await response.Content.ReadAsStringAsync(); 的商业成本是多少?反之亦然?

  

“但是那位会计师可以介入并间接更改给定产品的计算结果”

这几乎就是最终的一致性,不是吗?

如果您真的不能最终实现一致性,则可以使用域服务在单个事务中创建/删除两个不同的聚合根,但是请问问自己,如果信息不是完全由同一个人提供的,您将如何做?最终用户?

答案 2 :(得分:0)

我几乎在所有方面都同意@plalx。但是,我想尽我所能。

我发现,在单个事务(在单个有界上下文中)内创建两个或多个相关的聚合通常花费很少。毕竟,如果这些聚合不存在,那么就不可能有并发冲突,就没有争用,也没有太大的区别。此外,您不需要处理部分创建的状态(认为状态在聚合之间是分开的)。可以使用最终的一致性来做到这一点,并且在某些情况下这是一种更好的方法,但是在大多数情况下并没有很大的好处。甚至弗农(Vernon)在他的《实现域驱动设计》(Implementing Domain-Driven Design)中也提到此用例是“违反规则的有效理由”。

删除多个聚合是另一回事。如果您同时删除并汇总另一个用户正在更新,该怎么办?当您尝试在同一事务中修改/删除更多汇总时,发生这种冲突的可能性就会增加。这些聚合之间始终存在上游/下游关系吗?我的意思是,如果用户删除了A并且必须删除B,请更新B的用户没有“权力”或“声音”来取消删除,因为她正在提供更多有关聚合状态的信息?

这是一个非常棘手的问题,在大多数情况下,这是您需要与领域专家讨论的问题,并且在很少的实际情况下,如果答案是最终的一致性,您将无法承受。我发现在许多情况下,最好放置一个“标记”以将聚合标记为“非活动”,并通知该标记将在一段时间后删除。如果没有足够权限的用户请求再次聚合,则该聚合将被删除。这有助于用户在误删除某些聚合时不会自杀。

您已经提到过,如果有删除操作,您不希望用户花费数小时来修改一个聚合,但这对事务的贡献不大。但是,这在整个架构中非常依赖。该用户可以将聚合加载到自己的内存空间中,然后删除。如果您在事务内部删除,则没关系,用户仍在浪费时间。更好的解决方案是发布一个域事件,该事件会触发某种推送通知给用户,因此她知道发生了删除,并且可以停止工作(或者,如果您采用这种方法,则可以取消该删除)。

对于报告和计算,在很多情况下,这些“脚本”可以跳过兄弟姐妹聚集消失的记录,因此用户不会注意到缺少的部分或尚无完全的一致性。

如果出于某种原因仍需要删除同一事务中的多个聚合,则只需在应用程序服务中启动事务,然后使用存储库执行删除操作即可,类似于创建案例。

所以,总结一下:

  • 创建汇总时,“每笔交易修改一个汇总”的规则并不重要。
  • 许多聚合的删除在大多数情况下都能很好地工作,并且最终具有一致性,而且经常一次禁用一次这些聚合比立即删除要好。
  • 通过适当的通知,比使用交易更好地防止用户浪费时间。
  • 如果确实需要在单个事务中执行这些操作,则应在应用程序中明确管理该事务。使用域服务执行所有必需的操作(大多数事务是与应用程序有关的事务除外),使该逻辑回到域层。