在域驱动设计中强制实施跨多个聚合的不变量(集验证)



为了说明这个问题,我们使用一个简单的案例:有两个聚合 -LampSocket。必须始终强制实施以下业务规则:LampSocket都不能同时连接多个。为了提供适当的命令,我们设想了一个Connector服务,并使用Connect(Lamp, Socket)-方法来插入它们。

由于我们希望遵守一个事务应仅涉及一个聚合的规则,因此不建议在Connect事务中同时设置两个聚合的关联。因此,我们需要一个中间聚合来象征Connection本身。因此,Connect事务只会创建一个具有给定组件的新Connection。不幸的是,在这一点上,麻烦开始了;如何确保连接状态的一致性?许多并发用户可能希望同时插入相同的组件,因此我们的"一致性检查"不会拒绝请求。将存储新的Connection聚合,因为我们只在聚合级别锁定。该系统将不一致,甚至不知道这一点。

但是,我们应该如何设置聚合的边界以确保我们的业务规则呢?我们可以设想一个Connections聚合,它收集所有活动连接(作为Connection实体),从而使我们的锁定算法能够正确拒绝重复的Connect-请求。另一方面,这种方法效率低下且无法扩展,而且在领域语言方面是违反直觉的。

你知道我错过了什么吗?

编辑:总结问题,想象一个聚合User。由于聚合的定义是基于事务的单元,因此我们能够通过锁定每个事务的单元来强制执行不变量。一切都很好。但现在出现了一条业务规则:用户名必须是唯一的。因此,我们必须以某种方式协调我们的总边界与这个新要求。假设数百万用户同时注册,这将成为一个问题。我们尝试确保这种不变性处于非锁定状态,因为多个用户意味着多个聚合。

根据埃里克·埃文斯(Eric Evans)的《领域驱动设计》(Domain-driven Design)一书,一旦单个事务中涉及多个聚合,就应该应用最终一致性。但这里真的是这样吗,确实有意义吗?

在此处应用最终一致性需要注册User,然后使用用户名检查不变性。如果两个User实际上设置了相同的用户名,系统将撤消第二次注册并通知User。想到这种情况让我感到不安,因为它扰乱了整个注册过程。例如,发送确认电子邮件必须延迟等等。

我想我只是忘记了一些事情,但我不知道是什么。在我看来,我需要类似Repository级不变量的东西。

我们可以设想一个连接聚合,它收集所有活动 连接(作为连接实体),从而使我们的 锁定算法,可以正确拒绝重复 连接请求。另一方面,这种方法效率低下,并且 不可扩展,而且在领域方面是违反直觉的 语言

相反,我认为你采用这种方法是正确的。这似乎很复杂,因为您正在使用一个没有任何意义的示例 - 没有现实生活中的系统可以检查灯是否连接到多个插座或插座连接到多个灯。

但是,将这种方法应用于第二个示例会导致您问自己在这种情况下的"连接"聚合是什么,即用户名在哪个范围内是唯一的。在Company?对于给定的TenantCustomer?对于整个<whatever-subdomain-youre-in>System?找到范围的名称,你就有了它 - 一个聚合,用于强制使用唯一名称不变。仔细选择名称,如果它还没有出现在无处不在的语言中,请在领域专家的帮助下发明一个新概念。DDD 不仅尊重现有的域术语,您还可以在实现突破时引入新的术语。

但有时,您会发现对此聚合的并发访问过于密集,并会产生有问题的争用。通过领域专家的同意,您可以在发生冲突时引入最终一致性与补偿操作 - 例如,在昵称后附加后缀并通知用户。或者,您可以将"热"聚合拆分为更小、更智能、更高效的聚合。

您描述的问题称为集合验证。 Greg Young 提出了一个非常好的观点,即一个关键问题是成本/收益分析是否证明在代码中强制实施此约束是合理的。

但让我们假设它确实如此。

我发现从RDBMS的角度考虑集合验证是最有用的。 如果我们用表格做事,我们将如何处理这个问题? 一个可能的候选者是,我们将有某种连接表,带有灯和插座的外键。 然后,我们将定义约束,说明这些外键中的每一个在表中必须是唯一的。

这些外键约束跨越整个表;这是数据库告诉我们整个表表示单个聚合的方式。

因此,如果要将这些约束提升到域模型中,则需要聚合所有连接,以便域模型可以立即裁定是否应允许给定的 Lamp-Socket 连接。

现在,这里有一个重要的警告 - 我们假设域模型是灯和插座之间连接的权威。 如果我们在现实世界中对连接到现实世界中的插座的灯进行建模,那么重要的是要认识到现实世界是权威,而不是模型。

换句话说,如果域模型获得有关现实世界的冲突信息(据报道两盏灯连接到同一个插座),该模型只知道其关于世界的信息不正确 - 也许第一盏灯入,也许第二盏,也许缺少关于灯被拔出的消息。 因此,在这种情况下,您通常会希望允许冲突,并升级到人类来解决。

用户名必须是唯一的

这是集合验证问题中最常见的变体。

基本补救措施是相同的:您现在有一个带有标识符的用户配置文件聚合和一个单独的用户名目录聚合,这可确保每个名称与配置文件唯一关联。

如果您不担心配置文件最多有一个用户名链接到它,那么您可以采取另一种方法,即为每个用户名引入一个聚合,其中包括 profileId 作为成员。 因此,每个聚合都可以强制实施以下约束:只有在上一个分配终止时才能分配名称。

我想我只是忘记了一些事情,但我不知道是什么。

只是约束不是凭空而来的——它们应该有商业动机;有人(领域专家)应该能够记录未能维护提议的约束给业务带来的成本。

例如,如果您已经在收集电子邮件地址,您真的需要一个唯一的用户名吗? 通过在模型中包含用户名,您创造了多少附加价值? 通过使其独一无二,还需要多少...?

例如,如果我们计划一款在线游戏,有数百万用户不断请求游戏,这是一个真正的问题。

是的,确实如此;但这可能表明游戏设计是错误的。 回顾乌迪·达汉(Udi Dahan)对高争用域的讨论,以及他的文章《种族条件不存在》。

但是,需要注意的一点是,如果您确实有一个聚合,则可以独立于系统的其余部分进行扩展。 一个怪物盒专用于管理集合聚合,没有其他任何东西(类似:专用于管理单个表的RDBMS)。

一个更可能的选择是按领域/实例/whatzit进行分片;在这种情况下,每个领域实例都有一个较小的集合聚合。

除了已经提出的建议之外,请考虑其中一些问题与数据库并发问题非常相似。假设您有一个联系人,一个用户更改了姓名,另一个用户更改了此联系人的电话号码。如果编写一个命令,使用修改后的状态更新整个联系人,则除非采取措施,否则两者中的一个将使用旧值覆盖另一个的更改。

但是,如果您编写了"ChangeEmailForContact"命令,那么您将只更改该字段,并且不会与名称更改冲突,这同样是"名称"或"重命名联系人"命令。

现在,如果两个人紧接着另一个人更改电子邮件地址怎么办?一种非常有效的方法是在命令中传递原始值(原始电子邮件地址)以及新值。现在,您可以在更新电子邮件地址时检查原始电子邮件地址是否与当前电子邮件地址相同(因此它是有效的起点),或者新电子邮件地址是否与当前电子邮件地址相同(无需执行任何操作)。如果没有,那么,只有这样,你才处于冲突局势中。

现在,将其应用于您的"设置操作"。灯泡第一次移动到"连接"(也许我称之为灯具)时,它正在从未分配移动到连接1。然后,当一个灯泡被移动时,它必须从连接1移动到连接2,比如说。现在,您可以验证该灯泡是否已分配,是否已分配给连接1,或者在此期间是否发生了更改。

当然,它并不能解决所有问题,但是对于仍然存在的小情况,即两个初始分配发生得足够接近的微小时刻,您要么必须使用分配的用户名的 redis 缓存进行验证,要么为管理员提供一个简单的工具来解决这个非常罕见的情况。例如,您可以进行投影,偶尔报告此类情况,并确保重命名不会太痛苦。

相关内容

  • 没有找到相关文章

最新更新