如何创建一个 ddd 聚合并在同一事务中使用其引用更新其他聚合?

How to create a ddd aggregate and update other aggregate with its reference in the same transaction?

我想问一下我在游戏实现中使用DDD遇到的问题。

我有一种情况,根据系统收到的消息,为玩家创建了一个新的聚合。问题来自这个新聚合必须由另一个聚合(table 聚合)使用其 id 引用的事实。第二个聚合保留了一个值对象,其中包含有关 table 处玩家的信息,例如他的位置或选择的颜色。玩家聚合存储玩家操作,因为根据玩家数量和他们发出的操作,table 聚合可能会变得庞大,如果我将所有内容都放在 table.为了让玩家能够加入 table,必须先通过一些验证,例如未取颜色、已达到最大玩家数或玩家已处于非活动状态的游戏,这就是存储此值对象的原因在 table 上有来自玩家的信息。

因此,基于此,我仅在他发出要存储在 table 之外的第一个动作时创建玩家聚合。问题是 table 可以保存引用尚未创建的聚合的值对象,这使得代码看起来很难看,因为它强制它检查空引用。另一个选项,即首先创建玩家聚合,也打破了设计,因为创建玩家的验证首先发生在 table 上。由于系统的并发性质,首先在 table 上验证,然后创建播放器聚合,然后在从创建中接收到事件后,更新 table,可能会导致许多竞争条件。我唯一能想到的是在同一事务中更新两个聚合,但这违反了 DDD 规则。

有什么好的解决办法吗?我想最终这将与设计有关,但我想不出在不引入性能问题的情况下它可以工作的任何方式。

谢谢。

与往常一样,您的部分问题可能会通过从领域和通用语言的角度来看问题来解决。

  • Udi Dahan 有一个 excellent article 关于聚合根如何不会凭空出现,而是作为必须是第一个 class 无处不在的域动作的结果语言公民。在您的情况下,Player 可能不是由应用程序服务创建的,而是由 Table AR 最接近域源创建的。

    请注意,我不一定像 Udi 的示例中那样提倡对其他根的完整引用,但您可以将其替换为 Table.ReceivePlayer(...) 返回新创建的播放器,然后可以将其添加到 PlayerRepository 由应用程序服务。由于 Player 不扮演聚合根的角色(即修改和不变执行的单一入口点),而是在那种情况下扮演简单实体的角色,你可以合法地将所有这些包装在一个事务中。

  • 推动聚合建模的力量基本上是真正的不变量、并发性和性能。为了证明您的建模方式的合理性,您声明 table 聚合可能会变得庞大,并且如果我将所有内容都保留在 table[=40= 上,数据库中的锁争用将成本太高].

    我认为进一步探索这个争论的真正含义并将领域术语置于其背后,丰富您的无处不在的语言会有所收获。 table 是大规模并发吗?为何如此 ?游戏是否有玩家修改的 mutable 共享状态?玩家是否需要根据其他玩家的行动做出决定,如果同时采取并发行动,会引入某种偏见?所有这些在领域术语中是如何翻译的?额外的 Player 聚合能为您带来什么?

    就性能而言,巨大的 Table 聚合会是什么样子,它会如何影响系统?

  • "no transaction spanning multiple aggregates" 规则只是将聚合作为一致性边界的必然结果,它并不是一成不变的。制作小的、独立一致的聚合并在单个事务中对其中的多个聚合进行大量修改是没有意义的,因为您会产生不必要的争用,并且随之而来的是潜在的并发问题。但是在除了唯一 Table AR 之外不存在并发问题的情况下,创建时肯定是这种情况,因为没有其他人知道正在添加 Player,就好像您只是修改了一个聚合,规则不再有效。

    如果您在 99% 的修改案例中遵守规则,但在 1% 的创建案例中不遵守规则,您仍然在队列中。