使用DDD聚合发现隐藏的业务规则的案例分析:数据库事务的业务实现 - Nick Tune


在现实世界中,我们可能会对我们的业务规则和流程含糊不清。我们可以设置例外,也可以绕过一些步骤以适应我们从未想到的特殊情况。
想象一下一个业务规则,即所有客户都必须具有名字,中间名和姓氏。如果某人访问实体商店时没有中间名甚至没有姓氏的,则可以写下他们的名字。
在软件中,无法实时应对意外情况。我们必须在代码中非常精确地指定我们的业务规则是什么,计算机将完全按照我们的指示来应用它们。
将模糊的现实世界业务规则转换为精确的计算机代码时,可能会出现一系列问题,从而极大地影响客户体验,创收甚至公司声誉。
如果某个系统无法为来自不同文化,具有不同命名约定的人提供服务,则这不是很好的生意或宣传。

为了避免此类问题,需要最小化定义业务需求与将它们转换为软件之间的差距。这是组织应采用领域驱动的思维方式的地方。技术专家和业务专家共同协作进行领域建模,以识别实际的业务规则
领域驱动设计的聚合模式用于弥合差距。它充当协作工具,可以平衡对业务正确性,用户体验和技术效率的需求。

本文中使用的医疗保健示例基于我最近与Kacper Gunia进行的基于实际客户需求的讨论。这不是一个虚假的例子,它是我们与客户合作时面临的典型挑战。

事务和不变业务规则
为了在软件系统中实施业务规则,需要确定事务边界:哪些业务规则必须一起成功或失败。这在业务和技术层面都具有重大意义。
例如,用户必须在创建帐户之前指定其名字,中间名和姓氏。如果没有这三个名称,他们将无法创建帐户。当用户输入详细信息时,如果他们不满足所有条件,则他们创建帐户的请求(即交易)将被拒绝。
业务规则是,在任何情况下,没有名字,中间名和姓氏的客户都不应存在于系统中。我们将此称为不变业务规则。

定义聚合边界
确定特定的业务运营应该成功还是失败听起来像是一个简单的需求收集练习。当您在职业生涯中获得经验时,您会意识到并非如此。
考虑用于医疗保健实践的预约计划系统的场景。每天都有60个时段,每个时段持续10分钟。在软件系统中,我们需要确定业务事务边界(又称为DDD聚合)。
开发人员决定将一个10分钟的时间段作为其总边界。他们选择它是因为它较小,并且应该更快地加载和保存在数据库中。
一切似乎都很好,直到开发人员开始收到一些患者在同一天竟然预约多次的投诉,但是业务规则是不允许这样做的。

数据库事务的限制
开发人员尝试在代码中添加一条规则:即无论何时患者请求预约,如果他们在同一天已经进行了预约,则第二次预约将被拒绝。
但是有一个问题:数据库事务。如果患者尝试几乎同时预订两个预约,则可以跳过该规则,并且将确实可以进行同时两个预订成功。这是在添加防止重复预订的业务规则之后还会发生,怎么办呢?
患者是可能会预订多个预约,因为数据库事务与聚合事务不符,两者边界不对齐,因为聚合边界是一个10分钟时间段,数据库需要两个事务来更新两个时间段。如果第二个事务在第一个事务之后但在第一个事务完成之前开始,它将不会知道第一个预约已安排好,它将保存第二个事务。(banq注:患者预约和医院安排确认预约是流程中的两个步骤,今天患者预约,明天医院才能安排确认,这是一个长时间的事务过程。)

增加聚合大小以增强业务正确性
您可能已经注意到,这里有一个简单的解决方案:使聚合边界更大,而不是一个10分钟的时间段,聚合可以是以日程来安排:一天中的所有时间段,聚合的边界不是10分钟时间段,而是“一天”。
如果患者现在尝试在同一天预订2个预约,则数据库事务将阻止安排第二个预约。如果尚不清楚,请尽力了解数据库ACID工作原理…
第一个事务将开始第一次预约,然后第二个事务将开始第二次预约。第一个事务将提交并保存第一个预约。但是,当第二笔事务试图保存第二笔预约时,它将失败。
第二个事务失败,是因为数据库可以看到对第一个事务内的聚合进行了更改,并且如果不覆盖第一个事务中的数据,则提交第二个事务可能并不安全。这就是所谓的乐观并发
看来问题已解决。病人已经不能在同一天安排两次预约了。

并发冲突影响用户体验
通过增加聚合边界的大小,带来的意外副作用的风险也会增加。以下示例说明了这一点。
副作用之一是引入了新的“ 不变业务规则”,该规则规定,如果患者的预约被诊所取消安排,则应自动将其安排为另一次预约。如果预约没有进入安排,则任何患者都不得取消预约(banq注:这里预约和安排确认是一个业务流程中两个环节,患者预约了医院不一定安排确认,但是医院安排确认了的预约,患者就不能取消)。
当前的聚合将不允许强制执行这种不变性,不变性规则是跨数天,而聚合的边界是单日内。(banq注:这种不变性其实是流程的不变性,是跨天长时间的,今天预约,明天才被安排确认)
因此,又再次提出简单的解决方案:聚合现在可以是按周Week Schedule或按月Month Schedule。
实际上,为什么不将聚合设计为按年度“ Year Schedule”为边界呢,因为“没有患者在一年内可以有20个以上的预约”也是一条业务规则啊。
答案是性能,但更重要的是并发冲突。这不是技术问题,这是损害用户体验的业务问题。
如果聚合为“ Week Schedule”,则也可能会影响两位试图在同一周预订不同时段的患者。第二位患者的请求将被拒绝,因为聚合已经更新。使聚合更大会使问题更加严重。
业务中没有人希望阻止两名完全分开的患者在不同的日期进行预订。但是业务专家不懂软件。只是看到那些不称职的开发人员在为技术而烦恼。

发现真实的业务规则
对于初级或幼稚的开发人员而言,聚合问题似乎是技术问题。但是解决这些问题的方法实际上只能是更好地了解领域
了解执行不变业务规则对性能和并发性的影响后,我们可以问业务,如果患者在同一天预订两次预约或未立即提供重新安排会发生什么情况?
如果我们取消了患者的预约,并且又花了10分钟才重新安排了新的预约,该怎么办?
飞机会从天上掉下来吗?医疗用品会用完吗?世界海洋会枯竭吗?
从软件竞争条件到最终的业务规则
是通过拒绝患者预约使用户烦恼?还是推迟10分钟重新安排患者的预约,企业很可能会发现:不变性规则并不是真正的不变性,而只是最终不变的业务规则(有时开发人员会假设业务规则是不变的)。
确定放置聚合边界的位置直接决定系统中可能发生的竞争条件的类型。通过探索聚合边界并明确业务规则是真正的不变还是最终不变,对于找到良好的聚合边界绝对必要。
如果您是开发人员,并且不打算与领域专家一起探索聚合界限,那么您可能会遇到次优界限(banq注:不解决根本问题的修修补补的边界划定)。

选择聚合边界的公式
从根本上来说,选择聚合边界是一个简单的方程式,在此我们可以权衡以下条件:

  • 正确性:执行永远不应该违反的业务规则
  • 并发性:确保用户可以并行工作而不会互相影响
  • 复杂性:大型的复杂聚合或异步流程(对于最终业务规则通常是必需的)可能会增加维护成本和软件应用程序的可靠性
  • 性能:无需加载和保存数据库中的大数据有效负载,从而优化了系统的响应能力

找到正确的平衡是挑战所在,这是一项技能。
也许聚合较大会更好:医疗保健机构可能只有一名医生,每小时仅需要1或2个约会请求。性能和并发性无关紧要。因此,正确性和简单性是可取的。
也许小聚合比较好:每天有100位医疗保健从业人员,而一组呼叫中心的工作人员则在不断进行预订。性能和并发性至关重要,因此必须牺牲正确性和简单性。
也许介于两者之间比较好:有20位医疗保健从业人员和一些呼叫中心人员。定期制定“ 年度时间表”会导致并发冲突,因此太大了。但是,具有“日程安排”将需要分配更复杂的重新安排工作流程。
通过与企业讨论,您可能会了解到所有重新安排的99%仅影响单个日历周。如果通过将“ 周计划”定义为聚合大小仍然很好并且没有并发问题,那么这似乎是满足要求的不错选择。
也许有一个真正不变业务规则:他们真的存在吗?

随着时间的推移不断发展聚合
聚合设计所依据的初始标准可能会随时间而变化。因此,您必须不断评估和挑战您的聚合界限。
如果医疗保健机构雇用了更多的医疗从业人员和呼叫中心人员,则突然的性能和并发性问题可能会成为真正的问题(这确实确实发生了)。
在这种情况下,平衡从需要高正确性和低简单性转变为需要高性能和并发性,因此应重新设计聚合。

了解您的领域以查找良好的聚合体
希望现在很清楚,聚合的DDD概念不仅仅是软件开发人员所追求的技术模式。聚合鼓励我们深入研究该领域并提供更好的用户体验。
那么,如何找到一个好的聚合呢?

  1. 为您的领域建模以发现潜在的不变性
  2. 用“假设”问题挑战不变量,以确定补偿动作是否足够
  3. 分析任何现有数据,以确定聚合对性能和并发的影响
  4. 建立正确性,并发性和性能的最低级别
  5. 确定满足最低要求级别和最低复杂性级别的基本设计
  6. 探索增加复杂性但提供业务收益的替代模型
  7. 预测未来趋势
  8. 创建仪表板以可视化性能和并发级别
  9. 永不停止探索更深的领域见解

领域驱动设计提供了一套以帮助您实现:EventStorming,领域讲故事更多

(banq注:患者预约和医院确认安排预约是一个流程的两个步骤,如果患者的预约被诊所取消安排,则应自动将其安排为另一次预约;如果预约没有被医院安排,则任何患者都不得取消预约,这是一个流程事务过程,聚合只能设计作为流程中一个步骤,无法涵括整个流程,否则就变得太大,流程的长时间事务必须通过业务流程自身来设计,而不是一味寻求数据库事务或聚合来完成,但是通过聚合设计,可以发现时间维度上隐藏的概念,天是时间段的集合聚合,周是天的集合聚合,月是天的集合聚合,具体以哪个聚合大小设计,需要结合业务特点和流程,不要试图用聚合做流程的事情,也不要让聚合做基本时间段内微观的事情)