使用域事件更新读取模型不会重复业务逻辑吗?

Doesn't updating read models using domain events duplicate business logic?

我目前正在考虑领域事件的粒度。据我所知,从命令到域事件的 1:1 关系开始是一个很好的模式,这样域事件就可以表达用户所做的事情。

在我的示例中,有一个旅程(火车或公共汽车)由一系列站点组成。这些停靠点中的每一个都有一个分配的时间戳。现在,用户可以取消此旅程,这会导致从旅程的停靠点中删除(简化)所有时间戳。

我要做的是发出一个 "JourneyCancelled" 事件。

现在如果有一个读取模型提供旅程的停靠点列表 - 读取模型将如何处理此事件?它可能还会为旅程的每一站重置时间戳,这意味着它在那里复制逻辑(以及处理此事件的所有其他读取模型)。

我对行程取消这一事实很感兴趣,但我也对发生的细节很感兴趣。

我在域事件方面做错了什么?

罗尼

Now the user may cancel this journey which results in all the timestamps being removed (simplified) from the journey's events.

事件应该是不可变的,您永远不应根据后续事件从中删除数据。

Now if there's a read model providing a list of stops for the journeys - how would the read model handle this event? It would probably also reset the timestamps for every single stop of the journey

这个读取模型有什么用?你想回答什么问题?企业是否需要一个显示所有 predicted 行程停靠点的读取模型,还是 JourneyCompleted 事件更合适? (或更细化,LegCompleted 事件)。

它是否需要知道随后取消了哪些行程,但保持预测停靠点?

一种方法是将事件进一步拆分,如上文所述:

  • JourneyStarted(可能包括所有预测的停靠点)
  • LegCompleted(停止 name/id、时间戳等)
  • LegCompleted
  • JourneyCompleted

  • JourneyStarted
  • LegCompleted
  • JourneyCancelled

等...

我不清楚您在读端或写端试图做什么。读取模型通常 数据聚合,旨在回答以下问题:

  • "how many cancellations were there in the last hour?"
  • "what are the most common reasons for cancellations?"
  • "is there a correlation between the length of journey and the % of cancellations?"
  • 等...

域的写入端用于执行业务规则和不变量。

虽然您将 command->event 作为起点是正确的,但到目前为止,这不是教条。如果您查看任何 event-sourcing 框架,它们总是假定域模型上的一个事务可能 returns 一个事件列表。

话虽如此,一个命令可能会导致产生许多事件。这些事件中的每一个都可以投影到读取模型上的单个原子操作。

在您的情况下,CancelJourney 命令将产生一个 JourneyCancelled 加上许多 StopTimestampCleared 事件。因此,您的业务逻辑将在聚合方法中,您的预测将非常愚蠢,不会包含该逻辑。

重要的是记录导致聚合状态更改的任何事件。此外,尽量使 Apply 方法尽可能小。在这种情况下,您会看到 JourneyCancelled 与其他的不同。另外,非常重要的是,请记住 Apply 方法会随时间变化,但您需要做好准备,每次读取聚合时,它们都会再次播放。

请记住这一点,您现在在停靠点中进行了隐式状态更改,但您确实需要使其更加可见。通过为他们举办自己的活动,您肯定会改进您的模型。

As far as I understood it is a good pattern to start with a 1:1 relation from commands to domain events so that a domain event expresses something the user did.

重新学习——1:1很常见,因为许多命令只修改单个实体并且不需要任何补偿操作,但它并不通用。当用户提出更改时,您通常需要多个事件来保持不变性。

你也要小心"something the user did"。如果您的消息描述的是用户(在现实世界中)所做的事情,那么该消息就是事件,而不是命令。命令是用户要求模型做某事(重要的试金石 - 模型可以说不吗?)。

In my example there is a journey (of a train or a bus) which consists of a sequence of stops. Each of these stops has a timestamp assigned when it is going to happen.

还要记住 "aggregates" 几乎总是 信息资源 -- 不是旅行者,而是旅行者档案。不是旅程,而是预订或行程。

在这种情况下,"when it is going to happen" 暗示 Schedule 或 Itinerary 作为一个概念,应该在您的模型中明确(尚不清楚它是聚合的根还是其中的子实体)。

您还需要考虑停靠点(我喜欢 Legs,来自我在旅行软件中的工作)是值还是具有当前状态的实体。

Now the user may cancel this journey which results in all the timestamps being removed (simplified) from the journey's stops.

提示:征求建模建议时,避免简化;好的建模需要理解约束。

完全不清楚为什么取消旅程应该从时间表中删除时间戳。

尝试猜测你想要的类比;在航空旅行中,一次旅行由航段组成。如果我们取消行程,我们还想取消航段的机票支付、取消座位分配等等。

因此选择座位分配作为具体示例;取消行程意味着释放座位分配这一事实是不变的,因此应该有一个明确的事件宣布座位分配已被取消(截至时间戳等)。

因此读取的跟踪座位分配的模型应该监听座位分配事件,并适当地更新座位图(显式),而不是监听 JourneyCancelled 并推断座位的变化(隐式)。

换句话说,我们通过使事件流更明确来避免重复业务逻辑的陷阱。请注意我们免费获得的额外解耦:因为读取模型可以侦听特定于它关心的实体的事件,我们可以重新设计写入模型中的聚合,而不必担心对读取模型的影响。