【问题标题】:Event Sourcing Refactoring事件溯源重构
【发布时间】:2018-09-23 01:24:03
【问题描述】:

我研究 DDD 已经有一段时间了,偶然发现了 CQRS 和事件溯源 (ES) 等设计模式。这些模式可用于帮助以更少的努力实现 DDD 的某些概念。 在下面示例的架构中,聚合知道如何处理与自身相关的命令和事件。换句话说,事件处理程序和命令处理程序是聚合。

然后,我开始对一个示例域进行建模,以了解实现如何遵循业务逻辑。对于这个问题,这里是我的域(它基于this):

我知道这是一个糟糕的建模示例,但我只是将其用作示例。 因此,使用 ES,在操作结束时,我们会将所有事件(绿色箭头)保存到事件存储中(如果没有异常),每个事件都保存到其给定的事件流中(聚合类型 + 聚合 ID):

到目前为止,一切似乎都是正确的。所以如果我们想重建这个聚合的任何一个实例的内部状态,我们只需要更新它 (new()) 并以正确的顺序应用保存在其各自事件流中的所有事件。

我的问题与模型的变化有关。因为,软件开发是一个我们永远不会停止学习我们的领域的过程,我们总是会提出新的想法。那么,让我们分析一些变化场景:

改变场景一:

假设现在,如果 Reservation Aggregate 检查座位不可用,它应该发送一个事件(Seat not保留),并且此事件应由一个新的聚合来处理,该聚合将存储所有未保留座位的人:

假设旧系统已经正确处理了初始命令(Place order),并将所有事件保存到其各自的事件流中:

  • 当我们想要重建此聚合的任何实例的内部状态时,我们只需对其进行更新 (new()) 并以正确的顺序应用保存在其各自事件流中的所有事件。 (没有改变)。唯一的问题是,旧模型中不存在新的用例。

改变场景2:

假设现在,当付款被接受时,我们在新的聚合(财务聚合)中处理此事件(已接受付款) em>) 并且不再在 Order Aggregate 中。它会向 Order Aggregate 发送一个新事件(Payment Received)。我知道这种情况结构不完善,但可能会发生类似的情况。

假设旧系统已经正确处理了初始命令(Place order),并将所有事件保存到其各自的事件流中:

  • 当我们想要重建此聚合的任何实例的内部状态时,将聚合事件流中的事件应用到自身时会遇到问题:

现在,订单不再知道如何处理Payment Accepted事件。

问题

因此,如示例所示,每当系统更改反映在由不同事件处理程序(聚合)处理的事件中时,就会出现一些主要问题。因为,我们不能再重建内部状态了。 所以,这个问题可以有一些解决方案:

可能的解决方案

当一个事件未被存储在事件流中的聚合处理时,我们可以找到新的处理程序并创建一个新实例并将事件发送给它。但是为了保持内部状态正确,我们需要由 Order Aggregate 处理最后一个事件(Payment Received)强>。所以,我们让它分派事件(和可能的命令):

此解决方案可能存在一些问题。假设有一个新命令 (Place Order) 到达,它必须创建这个订单实例并保存新状态。现在我们会有:

灰色是系统尚未完成模型更改时已在上次调用中保存的事件。 我们可以看到为新聚合创建了一个新事件流 (Finance W)。我们可以看到 Event Streams 是仅追加的,所以 Order Y 事件中的 Payment Accepted 事件流仍然存在。 Finance W 事件流中的第一个 Payment Accepted 事件应该由 Order,但必须找到新的处理程序。 Order's 事件流中的 Yellow payment received 事件是由 Payment Accepted 的新处理程序生成的事件> 当 Order's 事件流中的 Payment Accepted 事件由 Finance 处理时。 所有其他绿色事件都是通过处理新模型中的 Place Order 命令生成的新事件。

解决方案的问题

下次需要重建聚合时,流中会有一个 Payment Accepted 事件(因为它是仅追加的),它会再次调用新的处理程序,但这已经完成并且 Payment接收到的事件已保存到流中。所以,没有必要再经历一次,我们可以忽略这个事件并继续。

问题

那么,我的问题是我们如何处理影响每个事件的处理者的模型更改?在这样的更改之后,我们如何重建聚合的内部状态? 我们是否需要构建一些事件流迁移,将事件从一个流更改为新模式(一个或多个流)?就像我们需要在关系数据库中一样? 我们永远不会被允许删除一个处理程序,所以我们只能添加新的处理程序吗?这会导致系统无法管理……

【问题讨论】:

标签: domain-driven-design cqrs event-sourcing


【解决方案1】:

您几乎没问题,除了一件事:聚合不应处理来自其他聚合的事件。这就像一个非事件源聚合与另一个聚合共享一个表:它们不应该。

在事件驱动的 DDD 中,聚合是系统的构建块,用于接收命令(表达意图的事物)并返回事件(已经发生的事物)。对于每个 Command 类型,必须存在 一个且只有一个处理它的聚合类型。在执行命令之前,聚合被提供所有它自己的之前发出的事件,也就是说,过去由 this 聚合实例发出的每个事件都应用于 this 聚合实例,按时间顺序排列。

因此,如果您想正确建模您的系统,则不允许将事件从一个聚合作为事件发送到另一个聚合(不同类型或实例)。

如果您需要对涉及多个聚合的业务流程进行建模,正确的做法是使用 Saga/流程管理器。这是一个不同的组件。它与聚合相反。 它接收聚合发出的事件并向其他聚合发送命令。

在最简单的情况下,Saga 管理器只需从一个事件中获取属性并使用这些属性创建+填充命令。然后它将命令发送到目标聚合。

在更复杂的情况下,Saga 等待多个事件,当所有事件都被接收到时,它才会创建并发送一个命令。

Saga 还可以对事件进行重复数据删除或重新排序。

在您的情况下,Saga 可能是 Sale,其目的是协调从订购到产品发货的整个销售流程。

总之,您有这个问题是因为您没有正确建模您的系统。如果您的聚合只处理它们的特定命令(而不是其他人的事件),那么即使您必须在新的业务流程出现时创建一个新的 Saga,它也会将相同的命令发送到相同的聚合。

【讨论】:

    【解决方案2】:

    简单回答

    我的问题是我们如何处理影响谁处理每个事件的模型更改?

    处理事件通常是一件容易改变的事情,因为处理部分是短暂的。事件只有一个作者,但它们可以有很多读者。您只需要安排管道将事件通知每个订阅者。

    所以在场景 #1 中,它的 PaymentAggregate 会记下 PaymentAccepted 事件(在它自己的流中),然后您的管道会通知 OrderAggregate PaymentAccepted 事件发生了,然后它会按照自己的逻辑执行下一件事。

    要更改为场景 #2,我们将保留 Payment Aggregate 不变,但我们会安排管道以便它告诉 FinanceAggregate 有关 PaymentAccepted 的信息,并告诉 OrderAggregate 有关 PaymentReceived 的信息。

    你的照片让人很难看出来;我认为您没有仔细跟踪状态的每个更改都存储在更改的聚合流中。不是你的错——微软的形象真的很糟糕。

    换句话说,您的箭头#3“预留座位”不是SeatsReserved 事件,而是Handle(SeatsReserved) 命令

    【讨论】:

      猜你喜欢
      • 2017-04-06
      • 2021-06-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-27
      • 1970-01-01
      相关资源
      最近更新 更多