【问题标题】:Domain Driven Design: reference between entities of different aggregates领域驱动设计:不同聚合的实体之间的引用
【发布时间】:2021-03-21 19:15:05
【问题描述】:

我正在尝试使用领域驱动设计规则和模式开发我的第一个应用程序。

在我的业务场景中,我必须管理客户列表并跟踪发送到特定客户目的地的所有包裹。 所以,按照规则,结果是:

  • 客户是一个实体,也是一个 AggregateRoot。
  • 目标是客户的子实体(因为没有客户就没有意义)
  • 包是一个实体。它也是另一个 AggregateRoot 因为是不同的事务边界 我使用 AddDestination(string destinationName ...) 方法开发了客户聚合,该方法负责为客户创建新目标。

现在我需要开发 Package 类,我必须在其中维护对包裹运送目的地的引用。按照 DDD 规则,我不能直接引用子实体 id,因为我不能引用另一个聚合的子实体。

我该怎么办?

  1. 我是否必须使用 Destination 的所需数据创建一个新的 ValueObejct 并公开它? (通过这种方式,我能够保留只能通过 Customer AggregateRoot 以书面形式访问的 Destionation 的封装和规则)。通过这种方式,外部类可以访问目标的字段并使用这些字段执行逻辑/检查,但不能更改目标的状态。
  2. 我是否必须添加 DestinationNumber 字段(在客户中渐进式),创建一个包含 CustomerId 和 DestinationNumber 的 ValueObject 并在 Package 聚合中使用它?
  3. ?

有人可以帮我吗?请详细说明回复,因为我想了解更多这种情况。

【问题讨论】:

  • 您写道,没有客户,目的地就没有意义,后来听起来您只想指向目的地。哪一个是真的?根据答案,我建议要么将所有目标数据作为值对象复制到 Package 中(如果客户在这里无关紧要,则选择 1)或存储 CustomerId + DestinationNumber 组合(如果没有目的地没有意义,则选择 2客户)。
  • 谢谢@urmaul 的回复。没有客户,我的通用语言中的目的地没有意义,因为每个目的地都与一个客户相关(而客户可以有多个目的地)。目的地有自己的 id所以我可以引用它还是不能,因为没有客户,Destination 就没有意义?
  • 我的数据库架构是这样的:TAB_Customers(ID, name), TAB_Destionations(ID, CustomerId, name), TAB_Packages(ID, DestinationId, Date)。如果我只在我的普遍语言中引入 DestinationNumber 是可以的,因为我需要在我的值对象中使用它?将目的地视为客户的子实体,我做错了吗?因为我只能从我的包中引用它,所以我需要将它视为另一个 AggregateRoot?

标签: domain-driven-design dddd


【解决方案1】:

如果我理解正确,我会假设这个包裹 必须送到某个地方,一个目的地

我还假设 Customer 确实是一个实体,也是一个聚合根,因为它会有更改名称等行为。Package 也是一个实体,以及自身的聚合根,因为它与 Customer 具有不同的事务边界,并且还具有一些行为(如 addDestination)。

在这里,我不认为 Destination 应该是一个实体,因为它本身没有任何行为或身份,而只是包含一个事实,即这个 Destination strong> 是指客户。所以,我会将 Destination 建模为一个值对象,使用 CustomerId 属性(可以这样做,因为 Customer 是一个聚合根)。

PackageaddDestination 参数将是 Destination 的值对象。如果 Package 必须更改目的地,它可以创建另一个 Destination 实例并丢弃旧的。

此外,如果 DestinationCustomer 的子代,则没有任何意义,因为可能有一些 Customers 没有交付任何东西然而。相反,我会将 Destination 建模为 Package 的子项,因为每个 Package 的创建都应该有一个 目的地(假设必须交付任何包裹)。所以 PackageDestination 在同一个聚合中。

【讨论】:

  • 在我的域中,每个 Customer 必须至少有一个 Destination 才能在 Package department 子系统中“可见”,因此正确有 CustomersDestinations 子级。现在,可以在我的 Destination ValueObject 中使用诸如 CusotmerId 这样的判别式,因为我需要一个键来区分两个不同 Customers 的相同目的地名称,或者更好地对待目的地作为实体?因为有了这个“技巧”,我可以通过将组成键的属性插入到 ValueObject 中来将所有实体视为值对象。这正确吗?
  • 包是否在另一个子系统中?如果是这样,Customer 子系统中“Destination”的含义是否与 Package 子系统中“Destination”的含义略有不同?如果它们在单独的有界上下文中,您可以将 Destination 作为值对象创建为 Customer 子系统中 Customer 的子对象,然后只需创建另一个 Destination 作为包子系统内的聚合根,因为“目的地”的含义略有不同
【解决方案2】:

DDD 中的聚合更多地是关于事务边界,而不是表示一个实体的存在在没有另一个实体的情况下是否有意义。


假设 A - 目的地 应该是一个聚合

如果在您的域中修改 Destination 在它自己的事务中是有意义的,而不必考虑 客户 那么它可能是您错过了 Destination 作为一个整体。

如果目的地将在客户之间共享或从一个客户转移到另一个客户(当然是通过身份参考),这也是有意义的。


假设 B - Destination 绝对是 Customer

子实体

另一方面,如果只有客户聚合可以确保始终满足客户目的地的业务不变量,则目的地应仅在客户的交易边界内更改 并因此保留客户的子实体。

在您的 Package 聚合中,您当然只能记住 Destinations id,如果您只需要它用于阅读目的,例如。

但认为这不是一个好主意。另一方面,在参考中不包括客户的 id 会使这种关系不那么明确。另一方面,如果 Destination 确实是一个实体(而不仅仅是一个值对象),则 Destination 的 id 只需要在相应客户边界内的域中是唯一的。

因此,如果 Destination 确实是 Customer 的子实体,我将使用您的 选项 2,方法是引入一个参考值对象,例如,例如CustomerDestination 包括客户 ID 和任何适合在特定客户范围内识别目的地的标识符。

注意:这不会影响您的数据库模型,根据您的设计方式,您可能仍需要数据库中的一些全局唯一代理 ID。

【讨论】:

  • 好的,谢谢。基本上,您在 Assumption B 中的建议是,如果我可以为 Destination 找到一个复合键,那将是一个实体,否则它只是一个值对象?根据 DDD,可以创建一个值对象来保护子实体的不变量吗?例如,我可以创建一个具有所有实体属性的值对象,只是为了将其暴露在保护实体不变性之外吗?示例:具有 OrderId、OrderLineId、Price 属性的 OrderRowValueObject 外露的行的订单(当然我假设 OrderRow 是 Order 的子实体)?
  • 是的,只公开您需要的属性而不是 id 引用来按需获取也是可以的。这真的取决于你的情况。如果您不在值对象中存储引用 ID,而是在具有所有属性的值对象中存储,您还必须注意数据同步 - 即,如果为客户更新了目标,则还需要更新包(直接或使用一些后台工作)。如果您更经常只需要在涉及包的用例中使用参考 ID,我会选择...
  • ...但是如果您确实有很多 写入 到您还需要目标数据的包,那么最好始终保持所需的目标属性在已经打包了。但也请不要忘记,对于仅读取数据,您不应该通过域模型类(聚合)进行查询,而是直接从数据存储中读取最合适和最高效的方式。因为阅读时您不需要遵守任何业务规则,因为您没有更改任何内容,因此您不能将模型置于不一致的状态。
  • 是的,我正在应用 CQRS,所以我将问题提交给了写方。我想我没有很好地解释我的意思。想象一下,您有一个带有子实体 (CE1) 的实体(E1,这也是一个 AggregateRoot)。现在,由于某些原因,我需要从另一个聚合(E2)中引用 CE1。我可以公开一个具有 CE1 的所有属性的 ValueObject 以访问(并引用 id)来自 E2 的数据还是不正确?我正在考虑相同的财务实体在两个聚合之间拆分不变量的情况。尽管我只有一个 pshiscal 实体,但我应该拆分同一个实体吗?
猜你喜欢
  • 2015-11-28
  • 1970-01-01
  • 2019-06-27
  • 1970-01-01
  • 2010-11-01
  • 2013-09-16
  • 1970-01-01
相关资源
最近更新 更多