【DDD 设计】域事件:简单可靠的解决方案

Chinese, Simplified

今天,我想写一个简单可靠的方法来实现域事件。


域事件:在提交之前调度


我相信是Udi Dahan首先介绍了域驱动设计特有的域事件概念。这个想法很简单:如果您想指出对您的域有重要意义的事件,请明确地引发此事件,并让域模型中的其他类订阅并对其做出反应。

所以基本上做到以下几点。创建域事件:

然后引入一个静态类,允许订阅事件并提升它们:

现在,当您拥有此基础架构时,您可以开始使用它。这是域事件生成器的外观:

这是消费者的一个例子。它收集所有已提交订单的统计信息并将其保存到数据库:

在提交之前,我将这种经典方法称为域事件调度。这是因为它允许您在提交业务事务之前引发事件并做出反应。

这种方法存在一些问题。它有时很有用,但是当处理域事件的副作用跨越多个事务时,它就会失败。

让我解释一下我的意思。例如,假设您要将另一个使用者添加到上述域事件中。此消费者会向提交订单的用户发送通知电子邮件:

现在,发送电子邮件的行为又为表格带来了一笔交易。在此之前,域事件的生产者和它的使用者都生成了存储在同一数据库中的副作用。生产者的副作用是Order类实例,消费者的副作用是更改了stats信息。您可以创建一个总体数据库事务,以便如果系统由于某种原因无法保持订单,则订单统计信息的更改也不会保留。

添加OrderNotification类后,情况就不再如此。发送电子邮件在其自己的事务边界内工作,您无法将其与数据库绑定。通过在确保数据库事务完成之前发送通知电子邮件,您将打开应用程序以查找潜在的不一致问题。现在可以在不保留订单的情况下发送电子邮件。例如,如果数据库在保存该订单时出现故障,则可能发生这种情况。

有些人认为,当然,这种方法在这种情况下不起作用,但是当没有涉及外部系统时,你仍然能够使用它。换句话说,如果所有使用者都在相同的数据库事务范围内运行,例如OrderStats。

这是一个不好的论点。如果事件的所有使用者都驻留在同一数据库事务中,则域事件几乎不会增加值。如果所有协作方都在同一个数据库上运行,那么最好使域逻辑流明确,并避免处理域事件。域事件以间接的形式带来了显着的复杂性开销。如果你能够避免这种开销,我主张你这样做。

因此,如果OrderStats是OrderSubmitted事件的唯一消费者,您可以将上面的示例重写为以下内容:

并完全摆脱域事件基础设施。此程序流程可以更好地了解请求期间发生的情况。这里明确概述了所有步骤。

这就是我不建议使用“提交前发送”方法的原因。如果消费者产生超出数据库事务范围的副作用,则不能使用它。对于所有其他类型的副作用,最好不要使用域事件。

当谈到域事件的主题时,一般规则就是:当你需要产生超出数据库范围的副作用时,只使用它们进行应用程序间通信。对于内部应用程序通信,让您的程序流程直观明确。使用常规技术,例如返回操作的结果并将其传递给下一个方法。

Jimmy Bogard在几年前写了另一篇关于域名事件的文章。 Udi和Jimmy的方法之间的区别在于后者建议将包含提升域事件的两个步骤分开:创建和调度。它使事件更易于测试,但实质上,它是相同的“提交前调度”方法:在提交数据库事务之前调度域事件。

域事件:在调度之前提交


几年前,我也在我的领域驱动设计实践培训课程中写过关于域事件的文章。由于缺乏更好的名称,我称我的实施“更好”。但是现在我想在发送之前重命名它。

正如您已经从名称中猜到的那样,此方法涉及提交数据库事务,并且仅在调度域事件之后。基本思想类似于Jimmy Bogard在他的博客文章中描述的内容:您不应该立即发送事件,而是需要跟踪它们,直到您准备好提交数据库事务。这里的主要区别是您在提交事务之后而不是之前调度这些事件。

以下是该活动的制作人的样子:

请注意,Order不再适用于静态DomainEvents类来分派这些事件。相反,它将它们保存到内部集合中。顺便说一句,这样就有机会将事件合并为一个事件或取消之前的事件。以前的方法是不可能的,因为事件是立即发送的。

为了调度事件,我使用了NHibernate的事件监听器。特别是,提交数据库事务后触发的事件(从此处获取):

这可确保仅在数据持久化后才执行调度。请注意上面代码中的OnPostUpdateCollection侦听器。它允许您分派事件,即使实体本身尚未更改。例如,当您向订单添加一行但保持订单本身不变时。花了我很长时间才弄清楚如何处理这些用例?

“调度前提交”方法比“提交前调度”方法有了很大改进。现在,您可以在域事件使用者中产生任何副作用,无论是发送电子邮件,在总线上发送消息,调用第三方API等等。当数据库事务失败时,您将不再遇到这种情况但确认电子邮件已发送给客户。

域事件:简单可靠的解决方案


然而,即使有了这种改进,该解决方案仍有两个缺点:

  1. 您需要一个ORM来实现它。而不仅仅是任何ORM,而是支持更新后,删除等事件的ORM。 NHibernate很棒,但也许你不能出于某种原因使用它。
  2. 你仍然可能遇到不一致。它们没有你在提交之前遇到的那些严重,但它们仍然可能发生。如果您的电子邮件网格无法接受通知电子邮件,您将收到提交的订单但没有确认电子邮件。您无法使域事件的分派与提交数据库事务100%一致。

我说这种不一致并不严重,因为它很容易减轻它们。在外部,可能有故障的呼叫之上的可靠队列有很大帮助。它显着降低了遇到不一致的可能性(但当然不能完全消除它,因为可靠的队列也可能会崩溃)。

那么简单可靠的解决方案是什么,没有所有这些缺点?

它与域对象一起持久化域事件。您需要做的是向数据库添加一个表:


域事件:简单可靠的解决方案

并将事件像常规域对象一样持久化。然后,让一个单独的工作人员逐个选择这些事件并处理它们。在大多数情况下,处理将涉及在总线上推送消息,然后可以将消息传递给适当的订户。但是你也可以提出自己的pub-sub机制。

这种方法的好处是,您现在可以在域事件基础架构中100%确定。持久域事件允许您实现这些事件的生产者和使用者之间的完全一致性。生产者只有在它们自己被持久化时才能生成事件(上例中的Order类)。并且消费者有机会实施重试机制,以便没有域事件漏洞。它还有助于消费者居住在一个单独的过程中。

但是,这种方法有一个缺点。与前一个相比,它需要更多的努力来实现。它简单可靠,但并不容易。您需要引入附加表,并且还需要为此目的开发后台作业。

我通常采用“调度前提交”的方法。我的所有活动消费者通常都会在公交车上发消息,这是一个非常可靠的操作。我在制作中没有遇到任何问题。但手动方法也很好,特别是考虑到我上面描述的所有好处。

摘要

 

  1. 仅将域事件用于应用程序间通信。对于内部应用程序通信,请改用显式程序流程。
  2. 提交之前的调度是在数据库事务完成之前调度事件的时间。避免这种类型的域事件,因为您将无法将其用于应用程序间通信。
  3. 调度之前的提交是在数据库事务完成后调度事件时。在大多数情况下,这种方法很好。与此实现不一致的可能性仍然很小。
  4. 如果您需要100%一致性且无法使用ORM,请将域事件与域对象一起保留。这种方法需要更多的工作。

 

讨论:请加入知识星球【首席架构师圈】

本文地址
https://architect.pub/domain-events-simple-and-reliable-solution
SEO Title
Domain events: simple and reliable solution