换个角度:采用时间建模来捕捉时间的流逝 - Mario


建模是软件或系统开发的一个重要部分,然而在试图捕捉系统的行为时,并不常见到时间被使用。当我们这样做时,我们倾向于提取这个概念,并以计划任务的形式将其放在领域之外。

在这篇文章中,我将提倡把时间作为建模的起点,并建议偏离传统的调度器,而采用更加反应式的模型。

传统的方法
传统建模很有可能是以下之一。

  • 类图
  • 实体关系图
  • 数据模型

虽然这些人工制品没有任何问题,但它们都代表或集中于你的系统在其生命周期内将处理的事物的结构。它们未能讲述上述事物是如何被创造并演化为当前状态的故事。

想想领域驱动设计(DDD),在其中你将寻求定义实体、值对象,以及在其行为方面的聚合。即使采用这种方法进行软件设计,时间建模也是一个挑战,因为这些概念很少由领域专家来定义。相反,它们需要通过反复的讨论才能发现。

考虑到上述困难,让我们尝试一种不同的方法,将重点从结构转移到一段时间内发生的事情。

时间性建模
时间模型Temporal Modeling不是试图从结构和关系开始,而是试图通过探索来发现你的领域的复杂性。这导致了结构和关系,但以一种更容易让他人参与讨论的方式。

让我们看一个例子来说明应用这种建模的一种方式。想象一下一个电子商务系统,我们有一个用例,从客户下订单开始。我将使用事件风暴的记号:

当你看我们的例子时,时间被放在了聚光灯下。我们代表了我们的系统在对已经发生的事实做出反应时是如何演化的。你把讨论的重点放在事实上,然后演化出如何命名出现的概念,以及它们的行为的决定。

现在让我们来看看,当时间的流逝本身是一个你可以建模的事件。

时间的流逝
在我们以前的电子商务系统中,经过对客户行为的分析,已经确定X%的客户倾向于在购买后30分钟内改变主意。为了减少在仓库处理这些变化后不必要的复杂性和成本,我们引入了悔改期。然后,决定是这样被捕获的。

请再次注意,这种行为是由一个事实表示的:时间已经过去,悔恨期已经过了。

从实现的角度来看,这通常被表示为一个计划任务,寻找符合我们所寻找的标准的项目。

SELECT orders WHERE creationDate < now - 30min AND status = Approved

这个伪SQL可以被你在持久化机制中使用的任何方法取代。如果我们一直在使用事件,而且我们的应用程序本质上是反应性的,为什么不尝试使用这种方法呢?

重新思考调度程序
使用调度器的解决方案--对于那些利用基于Linux的系统的人来说,cron是一种传统的方法。虽然这种方法可行,但它有一些缺点。

  • 可能是低效的。因为它使用了一个 "轮询 "机制,它可能必须每分钟运行一次,即使没有订单通过悔恨期,也要运行查询。
  • 需要你处理/防止并发的执行。如果你有大量的订单通过了悔恨期,当下一个执行期到来时,计划任务可能还在处理上一批订单。
  • 削弱了领域知识。悔恨期(30分钟)是一个重要的概念,但在领域本身却找不到。在最坏的情况下,它可能分散在领域和一个外部/基础设施元素(调度器)之间。

前两个缺点并不新鲜,可以说有技术解决方案,以辅助库或服务的形式来减轻它们的影响。然而,最后一个问题却超越了普通的产品。

让我们看看一个试图解决所有三个方面的概念实现:

  • 在订单被批准后,我们将在未来发布一个事件RmorsePeriodExpired,其中包含预期的最后期限。
  • 在这个时间用完后,该事件将供订单系统使用,然后它将通过内部锁定订单来防止它在未来被改变。


测试
建模时间的一个副产品体现在定义你的测试时,因为它们变得更加明确,并与用例定义的语言相一致。


情景:如果悔恨期已过,订单应被锁定
鉴于订单已获批准
当悔恨期过后
那么就锁定了订单

正如我们所看到的,它比替代方案更有表现力,也更容易编写,因为替代方案很可能太接近于基础设施。

结论
注重结构的传统方法,即使是领域驱动的,也有局限性,而且往往很难让非技术性的利益相关者参与进来。另一方面,其他基于讲故事的方法,包括基于时间模型的方法,通过将讨论集中在已经发生的事实上,鼓励用户的参与。

事件风暴或故事图是一些被推荐的方法,可以帮助从本质上翻转事情,专注于事件、时间线,并最终导致概念的定义,这些概念将成为你的泛在语言的一部分。

利用这种方法,如果你的系统具有反应性,就不要犹豫,将时间的流逝作为事件来捕捉。在与业务有关的流程中,如果涉及到订单、订阅或支付,这些比你想象的更常见。

最终你会拥有直接或间接代表时间流逝的事件,这不仅使它们成为一流的概念,而且还使用你的应用程序可能已经在使用的相同的消息基础设施。