在域驱动设计

时间:2017-05-31 11:38:02

标签: domain-driven-design aggregate bidirectional boundary

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

因为我们希望遵守一个事务只涉及一个聚合的规则,所以不建议在Connect - 事务中的两个聚合上设置关联。所以我们需要一个代表Connection本身的中间聚合。因此Connect - 事务只会创建一个包含给定组件的新Connection。不幸的是,此时麻烦开始了;我们如何确保连接状态的一致性?可能会发生许多并发用户想要在同一时间插入相同的组件,因此我们的“一致性检查”不会拒绝该请求。新的Connection - 聚合将被存储,因为我们只锁定在聚合级别。即使不知道,系统也会不一致。

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

你知道我错过了吗?

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

根据Eric Evans的“域驱动设计”一书,只要在单个事务中涉及多个聚合,就应该应用最终一致性。但这是否真的如此,并且有意义吗?

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

我想我只是忘记了一般情况,但我不知道是什么。在我看来,我需要Repository级别上的不变量。

3 个答案:

答案 0 :(得分:1)

  

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

相反,我认为你采用这种方法是正确的。这看起来很复杂,因为你使用了一个没有任何意义的例子 - 没有真实的系统可以检查一盏灯是连接到一个以上的插座还是一个插座连接到一个以上的灯。

但是将这种方法应用于第二个例子会让你自己问问&#34;连接&#34;在这种情况下,聚合就是,其中范围用户名是唯一的。在Company?对于给定的TenantCustomer?对于整个<whatever-subdomain-youre-in>System?找到范围的名称,你有它 - Aggregate to enforce the unique name invariant。仔细选择名称,如果它还没有存在于无处不在的语言中,请在领域专家的帮助下发明一个新概念。 DDD不仅仅是尊重现有的域名条款,还可以在实现突破时引入新的域名。

有时候,您会发现对此聚合的并发访问过于密集并且会产生有问题的争用。在域专家同意的情况下,您可以在发生冲突时引入补偿操作的最终一致性 - 例如,为昵称附加后缀并通知用户。或者你可以split the "hot" aggregate变成更小,更聪明,更有效的。

答案 1 :(得分:0)

您描述的问题称为set validation。 Greg Young提出了一个非常好的观点,即一个关键问题是成本/收益分析是否有理由在代码中强制执行此约束。

但我们假设它确实......

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

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

因此,如果您要将这些约束提升到域模型中,您可以通过汇总所有连接来实现,这样域模型就可以立即决定是否允许给定的Lamp-Socket连接

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

换句话说,如果域模型获得有关现实世界的相互矛盾的信息(据报道两个灯连接到同一个插座),模型只知道它的世界信息是不正确的 - 也许是第一盏灯插了in,也许是第二个,也许还有关于灯泡被拔掉的消息。因此,在这种情况下,您通常希望允许冲突,并升级为人类以便解决。

  

用户名必须是唯一的

这是设置验证问题的 单一最常见的变体。

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

如果您不担心配置文件最多只有一个用户名链接到它,那么您可以采用另一种方法,即为每个用户名引入一个聚合,其中包括profileId会员。因此,每个聚合可以强制执行约束,即只有在先前的分配被终止时才能分配名称。

  

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

只有这些限制不会来自任何地方 - 应该有他们的商业动机;某人(领域专家)应该能够记录未能维持建议的约束条件的业务成本。

例如,如果您已经在收集电子邮件地址,那么您真的需要一个唯一的用户名吗?通过在模型中包含用户名,您创建了多少额外价值?通过让它独一无二......还有多少......?

  

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

是的,它是;但这可能表明游戏设计是错误的。回顾Udi Dahan对high contention domains的讨论,以及他的论文Race Conditions Don't Exist

然而,要注意的是,如果您确实有一个聚合,则可以独立于系统的其余部分进行扩展。一个怪物框专门用于管理集合和其他(模拟:专用于管理单个表的RDBMS)。

更可能的选择是通过realm / instance / whatzit进行分片;在这种情况下,您为每个领域实例设置了较小的集合。

答案 2 :(得分:0)

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

但是,如果您编写了“ChangeEmailForContact”命令,那么您只需更改该字段并且不会与名称更改发生冲突,这类似于“名称”或“RenameContact”命令。

现在如果两个人不久之后更改了电子邮件地址怎么办?一种非常有效的方法是将原始值(原始电子邮件地址)与命令中的新值一起传递。现在,如果原始电子邮件地址与当前电子邮件地址相同(因此它是有效的起点),或者新电子邮件地址与当前电子邮件地址相同(无需做任何事情)。如果没有,那么,只有这样,你是否处于冲突状态。

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

它当然没有解决所有问题,但是对于剩下的小案例,两个初始分配发生在一起足够接近的那个微小时刻,你要么必须说出已分配用户名的redis缓存来验证或给出管理员一个简单的工具来解决这个非常罕见的实例。例如,你可以做一个偶尔报告这种情况的投影,并确保重命名不会太痛苦。