打破 DDD 聚合根引用规则

Breaking DDD aggregate root reference rule

根据我的阅读,DDD 中的建议是聚合根不应包含对另一个聚合根的引用。最好只保留对 ID 的引用。我想弄清楚我的情况是否值得违反规则。

以会计系统为例,假设您有一张发票作为聚合根。该发票有行和付款,这些可以是根目录下的实体。但随后也会将发票分配给买方和供应商。行项目也链接到可以有预算的帐户。买方、供应商、预算,这些都是独立的实体,需要管理并有自己的一套规则。但它们也会影响处理发票的业务规则。当然,我可以创建一个域服务并使用它来单独加载内容,但这不会使我的域对象性能降低并且更加贫乏吗?

如果我可以保留对实体的引用,我可以 运行 单个查询并获取我需要的所有数据(我将使用 Entity Framework .NET 中的核心)。如果我继续持有对外键的引用,那么我需要 运行 调用我需要的每个其他聚合。然后我的域对象不再像它自己那样丰富,因为如果没有一些外部协调器(域服务)它就无法处理它需要的所有业务规则。

我想知道的另一件事是,这些项目根本不会被聚合根修改,并且在该上下文中基本上是只读的,这是否意味着我可以将它们放在同一个聚合中有限模型(严格的最小值),然后它们也将拥有自己的聚合根(例如,我将有两个预算实体,一个在发票根下,一个在预算根下)。我的想法是 DDD 并不真正关心底层存储,所以这似乎是一个有效的选择。如果 buyer/supplier/budget 的实体是发票聚合根下的实体,它们也可能会大大简化,而它们的聚合根版本将复杂得多,具有更多属性、业务逻辑等。

我认为最好的选择可能是通过服务来完成。发票需要来自其他领域的部分逻辑,而不是全部。如果那些 entities/aggregate root 不会被修改,你只需要一些过程的结果。此外,您可以在那里使用 hexagonal architecture 并从它们相互通信的方式抽象域。通信可以通过 http、队列以及代码中的引用进行。通过将聚合根添加到其他聚合,您将坚持最后一个。

Another thing I wonder is given that these items are NOT going to be modified by the aggregate root at all and are essentially read-only in that context, does that mean I could have them in the same aggregate with a more limited model (the strict minimum), and then they would also have their own aggregate root (so for example I'd have two Budget entities, one under the Invoice root and one under the Budget root).

您应该审阅的一篇论文是 Pat Helland 的 Data on the Outside vs Data on the Inside

重要的问题不是数据是否会被修改,而是数据是否需要锁定以防止修改,而主聚合是[=37] =]宁.

如果数据不需要锁定以防止修改,那么您可以将其视为外部数据,也就是说您将所需数据的未锁定副本传递给您的聚合。粗略地翻译一下,这意味着您的应用程序代码获取数据的副本,然后将其作为参数传递给聚合。

如果数据 确实 需要锁定以防止修改,那么您确实在 内部 获得了数据。所以现在你需要弄清楚锁定机制是如何工作的:

如果您的域模型是 运行 在单个执行线程中,那么您可能没问题:线程一次只能做一件事这一事实意味着您有效地锁定了“一切。

如果所有数据都存储在同一个 RDBMS 中,那么您可以将数据“锁定以供读取”作为事务的一部分,这样,如果有并发修改,事务本身就会失败。

近年来,我们一直在尝试在具有多个执行线程和多个数据存储的环境中工作,但这些限制都不成立。因此,相反,人们倾向于重新设计他们的聚合以支持这种情况:如果在我们更改值 B 时必须锁定值 A 以防止修改,那么 A 和 B 必须 是一部分属于同一聚合(并受同一聚合根保护)。

这里的“必须”定义很模糊。另一个可能更有效的框架是考虑“失败对业务的影响是什么?” (Greg Young, 2010)。如果失败的成本很低,那么我们可以使用未锁定的数据副本,而不是试图将数据整合到一个聚合中。

the recommendation in DDD is that an aggregate root should not hold references to another aggregate root. It's preferred to just hold a reference to the ID. I'm trying to figure out if my case would warrant breaking the rule or not.

没有。聚合的要点是以“事务”方式封装业务逻辑和实现该业务逻辑所需的数据。您不能允许聚合的那一部分在根的控制之外被修改。如果您的聚合 A 需要聚合 B 来完成它的工作,您可以说聚合 B 是聚合 A 的一部分。鉴于聚合 B 可以自行更改(这就是它聚合的原因),这意味着聚合 A 的一部分是改变其根的控制之外。对不起,这是一个绕口令...

有多种原因会导致您发现自己的聚合需要的不仅仅是另一个聚合的 ID:

一种可能是您根本不需要它们。为什么您的发票需要的不仅仅是买家 ID 和供应商 ID。 Invoice 在做什么业务逻辑需要买方和供应商的详细信息?如果有 none 并且唯一的原因是能够显示买家和供应商信息(或打印 PDF),那么这不是聚合的问题。两个解决方案是 UI composition and a persisted read model.

另一种可能性是您实际上需要另一个聚合生成的一些数据,而不是聚合本身。例如,您的发票需要产品价格来计算总金额,但不需要产品名称、描述和图片。它不需要产品的当前价格。它需要购买时的价格。对于这些情况,您可以将 DTO 传递给聚合的方法,例如,AddInvoiceLine 将期望 InvoiceLineDto 具有 Invoice 所需的属性。 Use CaseApplication Service operation 将生成此 DTO 从一个或多个位置获取数据(例如,部分来自用户输入,部分来自数据提供者)。

所以,我认为你最后一段的方向是正确的,但你首先需要弄清楚,为什么你首先处于这个位置。