领域事件与事件溯源的区别

19-01-20 banq
                   

为什么领域事件domain events和事件溯源event sourcing不应混淆。

领域事件与事件溯源有什么共同之处?

共同点是名称中的“事件”一词。但除此之外,在与项目,会议或培训中的建筑师和开发人员交谈时,我经常听到领域事件与事件溯源相关,事件溯源是领域事件的理想来源。在这篇博文中,我想概述为什么我个人不赞同这个观点。

在我争论为什么不分享这个观点之前,我想确保充分了解领域事件与事件溯源:

领域事件

在领域驱动设计中,领域事件被描述为领域中发生的事情,这对领域专家很重要。无论领域在软件系统中是否实现或在何种程度上实现多少,这种事件通常都会发生。它们也独立于技术。因此,领域事件具有高价值语义,但是必须以领域专家所说的语言表达。例子可以是:

  • 用户已注册
  • 订单已收到
  • 付款截止日期已过期

领域事件在有界上下文和跨域有界的上下文中都是相关的。领域事件也非常适合于通知其他有界上下文:关于在自己的有界上下文中发生的特定业务相关事件,从而以事件驱动的方式集成多个有界上下文。

事件溯源

Martin Fowler在其原始博客文章中描述了事件溯源的关键特征如下:

事件溯源采用确保应用程序状态的所有更改都存储为一系列事件。

并不是直接在数据库的表中按字段保存应用程序中的当前状态,然后在需要时重新加载状态,后续如果应用中状态更改就覆盖这个数据表字段值,现在事件溯源中是按时间顺序排列事件列表,然后可以使用它来重建当前状态、记忆中的状态。

事件溯源是一般概念,但通常在与聚合相关的领域驱动设计的上下文里面进行讨论。因此,我使用聚合的持久性作为事件源的使用示例。

以下序列显示了使用事件源来保持和恢复聚合状态时的相关步骤:

1. 客户端调用现有聚合上与业务相关的操作,此聚合已经保留了两个先前的事件。

2.在处理调用之前,将创建聚合的空实例,并在聚合上重播先前保留的事件。聚合仅从相应事件中读取状态,并且不执行任何业务逻辑。完成后,聚合再次在内存中包含其当前状态。

3.聚合接受客户端的调用,根据其当前状态进行验证并进行处理,即执行相应的域逻辑。此时,聚合的内部状态尚未更改 - 只有在处理调用期间创建的事件时才会执行此操作。

4.作为处理调用的结果,聚合生成一个事件(或多个事件),包括以后在聚合中重建状态所需的状态。该事件被持久化,以便它可以用于将来对此聚合的调用,以再次重建当前状态。

通常会列出以下优点来使用事件溯源:

  • 存储的事件不仅描述当前状态,还描述了如何达到此状态。
  • 通过仅在某个时间点重放事件,可以在任何时间重建过去的任何状态。
  • 可以想象使用事件源来处理先前事件的不正确处理或延迟事件的到达。

话虽如此,事件溯源的实施也需要一定的概念和技术复杂性。一旦持久化,事件不应该改变,而领域逻辑通常会随着时间的推移而发展。因此,代码必须能够处理非常旧的事件。快照是必要的,以便能够以执行方式基于大事件历史重建状态。

此外,例如来自欧盟通用数据保护法规(GDPR)的要求的实施对事件溯源提出了真正的挑战,因为事件溯源要求不删除任何持久性事件。

事件溯源的事件≠领域事件

那么为什么我认为这两个概念并不是那么自然地融合在一起呢?

让我们考虑以下示例:在自行车共享的域中,用户想要注册以租用自行车。当然,还必须支付费用,这是通过使用钱包的预付费方式完成的。

此域的上下文映射的相关部分可能如下所示:

注册过程如下:

  • 用户通过移动应用程序输入他/她的手机号码。
  • 用户收到SMS代码以确认电话号码。
  • 用户输入确认码。
  • 用户输入其他详细信息(例如全名或地址)并完成注册。

此过程UserRegistration在有界上下文中聚合实现Registration。用户在注册过程中多次与聚合UserRegistration的实例交互。UserRegistration逐步建立状态,直到注册成功完成。完成后,用户应该可以为钱包充电并租一辆自行车。

现在,如果使用事件溯源来管理UserRegistration聚合的状态,则会创建以下事件(包含相应的相关状态)并随着时间的推移而持久化:

  1. MobileNumberProvided (MobileNumber)
  2. VerificationCodeGenerated (VerificationCode)
  3. MobileNumberValidated (no additional state)
  4. UserDetailsProvided (FullName, Address, …)

这些事件足以让UserRegistration在任何时间重建聚合的当前状态。不需要额外的事件,特别是没有表示注册现已完成的事件。UserRegistration一旦接受UserDetailsProvided事件被处理,由于其内部域逻辑,聚合就知道这个事实。因此,一个UserRegistration实例可以随时响应注册是否已经完成。

此外,每个事件仅包含在重放期间能够重建聚合状态所必需的状态。这通常只是受触发事件的调用影响的状态,即一种“差异”。从事件源的角度来看,在不受调用影响的事件上存储附加状态是没有意义的。因此,即使显式事件UserRegistrationCompleted持续存在,也不会包含任何其他状态。

事件溯源的一些支持者投票表明,来自UserRegistration聚合的事件溯源的这些事件也可以发布到有界上下文内外的其他相关方,因此可以触发进一步的域逻辑或更新其他状态。在我们的示例中,这些将是两个有界的上下文Accounting(用于初始化钱包)和Rental(用于创建注册用户)。

如果要使用来自事件源的事件来完成,则必须每个使用它的有界的上下文必须:

  • 处理这些细粒度事件并从UserRegistration聚合中知道至少部分域逻辑(例如,在用户被认为完全注册之后)。
  • 结合几个事件来获得用户所需的整个状态(例如来自的电话号码MobileNumberProvided和其他详细信息UserDetailsProvided)
  • 忽略对相应有界上下文不感兴趣的事件(例如,VerificationCodeGenerated或MobileNumberValidated确认电话号码)

从我的观点来看,这种方法打破了系统不同部分之间的预期封装,导致有界上下文之间的繁琐通信,从而增加了有界上下文之间的耦合。主要原因是来自事件源的细粒度事件的语义在事件本身和相关信息(“有效载荷”)方面都太低级。

在我看来,如果想改善事情,UserRegistration发布领域事件UserRegistrationCompleted时应该将所有相关信息MobileNumber,FullName以及Address(例如VerificationCode登记已成功完成后)作为有效载荷。该领域事件具有适当的语义,易于由外部有界上下文处理,而不必知道注册过程的任何内部。

在某些情况下,事件源的事件语义肯定可以提供适当的语义,以便外部消费者能够以简单的方式处理(例如MobileNumberProvided,想要了解所有电话​​的假设消费者的事件)已注册的数字)。但即便如此,我还是选择将事件溯源和事件的实现分开,以便它们可以相互独立地进化。这意味着在系统中输入的领域事件中电话号码将有两个表示形式,每个表示具有不同的用途。

事件溯源和CQRS

那么来自事件溯源的事件是否只能在相应的聚合中使用?

从我的角度来看,基本上是的。然而,一个可能且有意义的例外是将这些事件与CQRS中的读取模型结合使用。当然,这也会影响封装,但我的经验表明,CQRS的读取模型通常与聚合相关联,因为它们提供了对该聚合数据的特定视图。因此,人们可能会争辩说,在读取模型中处理细粒度事件所产生的耦合是可以接受的。

结论

我将事件源视为状态持久性的实现策略,例如聚合。不应将此策略暴露在聚合的边界之外。因此,事件源的事件应仅在相应的聚合内部或CQRS的上下文中使用,以构建相关的读取模型。

另一方面,领域事件表示与聚合的持久性策略的类型无关的特定事实或事件,例如,用于集成有界上下文。

事件溯源和领域事件当然可以同时使用,但不应相互影响。这两个概念用于不同的目的,因此不应混合使用。

(banq注:领域事件和事件溯源的事件应该尽量统一,因为如果事件溯源中的事件不是代表业务事件,那么我们就不必关注业务状态了,但是这是不可能的。)

 

                   

3
sinaID99391
2019-01-28 14:54

读过,好晕。。。