关注事件
2006 年 1 月 25 日
这是我在 2000 年代中期进行的进一步的企业应用程序架构开发写作的一部分。不幸的是,太多其他事情吸引了我的注意力,所以我没有时间进一步研究它们,而且在可预见的未来我也没有看到太多时间。因此,这些材料非常草稿形式,我不会进行任何更正或更新,直到我能找到时间再次处理它。
思考企业应用程序的最长久的方法之一是将其视为对外部世界事件做出反应的系统。这是一种在 80 年代后半叶在结构化设计社区中确立的思维方式。您现在在“事件驱动架构”的旗帜下听到它。
这种思维方式集中在将系统的与外部世界的交互视为事件的传输。通过形成一个事件列表来理解输入,该列表描述了与外部世界中的事件相关的每个可能的系统输入。类似地,系统通过向外部系统发出信号来宣布其自身发生的任何重大变化。
像这样看待系统事件是设计系统过程的一部分。一个很好的例子是结构化分析后期形式中使用的事件分区技术。它也可以在实现中烘焙到系统中,通过显式创建事件对象并将它们馈送到某个处理程序进行处理。
像这样将我们的目光集中在事件上,已经引起了人们对集成多个系统的极大兴趣——这是事件驱动架构的驱动力。通过使用事件消息,您可以轻松地将发送方和接收方在身份方面(您广播事件而不关心谁响应它们)和时间方面(事件可以在接收方准备好处理它们时排队和转发)解耦。由于这种松散耦合,此类架构为可扩展性和可修改性提供了很多优势。
然而,在本节中,我对这些组合并不感兴趣——尽管它们很有价值。相反,我对事件焦点对单个应用程序的影响感兴趣。正如我所见,我已经注意到一些有趣的特征,这些特征产生了一些非常有趣的机会。其中许多是相当零散的,因此我很难在脑海中组织它们。但这里有一些机会可以实现一些非常有趣的功能,这些功能通常不是应用程序提供的。
表示事件
到目前为止,我一直在松散地谈论事件,但当我们深入挖掘时,我们需要更仔细地观察事件。本质上,一个领域事件表示外部世界中发生的事情,这对应用程序很重要。它以某种数据结构的形式传输到应用程序,并附带描述事件的数据——我称之为源数据。事件可以来自各种地方——消息系统、用户界面、数据库表上的触发器。
事件具有非常不同的源数据,因为它们可以表示许多不同的东西。但是,事件应该具有的两个重要方面是时间点。我们应该始终考虑两个不同的时间点:事件发生的时刻和系统注意到事件的时刻。(在一个复杂的系统中,甚至可能有多个注意到时间。)这些时间点与我在时间模式中谈论的实际和记录的概念密切相关。
除了源数据之外,事件还可以携带处理数据,这些数据描述了我们对事件所做的事情。区分两者很重要。特别是事件的源数据是不可变的——这是我们所知道发生的事件,我们不能轻易更改它。(这里有一个陷阱,我们可能会收到不正确的源数据,我很快就会谈到这一点。)
由于领域事件是对象,因此它们很容易记录。通常有意义的是记录领域事件并保留它们的记录,如果没有什么别的,它可以作为很好的审计跟踪。
使用事件进行协作
我们习惯于将程序划分为多个协同工作的组件。(我在这里故意使用模糊的“组件”一词,因为在这种情况下,我的意思是很多东西:包括程序中的对象和跨网络通信的多个进程。)使它们协同工作最常见的方式是请求/响应风格。如果一个客户对象想要从一个销售员对象获取一些数据,它会调用销售员对象上的一个方法来请求该数据。
另一种协作方式是事件协作。在这种风格中,您永远不会让一个组件要求另一个组件做任何事情,相反,每个组件在发生任何变化时都会发出一个事件。其他组件监听该事件并根据需要做出反应。众所周知的观察者模式是事件协作的一个例子。
事件协作改变了对象履行其职责的一些假设。特别是它改变了围绕存储状态的职责。使用请求/响应协作,我们努力让只有一个组件存储特定数据,然后其他组件在需要时向该组件请求数据。使用事件协作,每个组件都存储它需要的所有数据并监听该数据的更新事件。使用请求/响应协作,存储数据的组件通常负责更新它,在事件协作中,负责更新某些数据的组件不必存储它,它只需要确保在更新时发出事件。
事件协作在应用程序中使用事件时不是强制性的,此外,在事件协作和请求/响应之间进行选择也不是一个排他性的选择。通常我会看到两种风格的混合,请求/响应通常占主导地位。
事件协作会导致非常松散的耦合,这使得在不修改现有组件的情况下将新组件添加到系统中变得特别容易。事件协作的缺点是很难理解协作。请求/响应协作在某种形式的代码中指定,该代码显示了整个流程,事件协作更加隐式——这使得在发生意外情况时更难调试。
事件溯源
但我们可以比审计跟踪更进一步,进入一些非常有趣的领域。实现这一点的推动因素是,所有对系统的更改都是由事件引起的——我称之为事件溯源的方法。另一种看待这一点的方式是,当我们可以通过处理领域事件的日志完全推导出应用程序的状态时,就会发生事件溯源。
这种情况开辟了一些有趣的机会,有些是功能性的,有些是实现性的。一个直接的实现机会是将应用程序的整个状态保存在内存中,完全放弃持久数据库。如果应用程序崩溃,请重新运行事件——也许是从应用程序状态的定期快照开始。系统可以选择出于性能或简单性原因这样做——从系统中删除持久性逻辑可以极大地减少构建和维护它的工作量。当然,并非每个系统都能走这条路,但对于某些系统来说,这是一个有用的选择。
由于我们的应用程序状态完全由我们处理的事件定义,我们可以构建替代的应用程序状态——我称之为并行模型——通过处理替代的领域事件列表。特别是,这些允许我们回到过去,构建过去某个时间的应用程序状态,以便检查某人为什么做了他们所做的事情,或者比较过去和现在的变化。除了调查过去,它们还允许我们通过探索替代的事件流来考虑尚未发生的事情。如果那场风暴只让芝加哥奥黑尔机场停运了一个小时而不是两个小时,周四会发生什么?如果我们将交付周期从每天一次改为每天两次,我们的运营会有什么不同?
并行模型在企业应用程序中并不常见,但对于企业应用程序的开发人员来说,它们非常熟悉。源代码控制系统是任何开发人员工具包中必不可少的一部分,它们是使用事件溯源按需生成并行模型的系统。这些并行模型可能是历史的,也可能通过分支允许多种现实。因此,源代码控制系统可以提供一个有价值的隐喻来探索并行模型的益处和问题。
通过操纵事件流,我们可以做的另一件有趣的事情是处理不正确信息的后续影响。正如我之前提到的,事件源数据是不可变的。我们无法更改我们已经接收和处理的内容。但如果我们收到的信息是错误的呢?如果我们使用事件溯源,我们可以做的是回到事件发生的点,构建一个新的并行模型来捕获应该发生的事情——本质上是用一个新的追溯事件替换那个不正确的事件。事实上,完全自动化此过程可能出奇地容易,而这通常需要一些复杂且昂贵的体力劳动。
所以事件溯源听起来很棒——Safeways 有吗?不幸的是,确实有一个陷阱,实际上有几个陷阱。最简单的陷阱是编程模型——让系统的每次更新都以可持久化的事件的形式出现很麻烦,特别是对于高度交互式的系统而言。它也不自然,所以需要一些时间来适应。
但事件溯源的真正困难在于连接到没有以事件为中心的外部系统。要重放事件,您需要能够查询外部系统的过去状态。您必须避免向下游系统发送重复的更新。源代码控制系统是并行模型的典范,它们避免了这种情况,因为它们不需要进行这种动态集成。但许多企业应用程序与它们自己的工作一样多地涉及集成,因此这些问题非常突出。
这些问题并非不可克服。根据您进行的替代处理量、与外部系统的集成程度以及这些外部系统本身使用事件和事件协作的程度;它们可能值得解决。即使没有事件溯源更花哨的后果,以这种方式设计系统也能提供出色的审计功能,以及一个基础,允许您稍后朝着更复杂的方向发展。对事件溯源进行改造看起来会非常混乱(我这么说,因为我还没有遇到过以这种方式改造自己的系统)。
另一个需要考虑的事情是,整个系统不必使用事件溯源。事实上,我大多看到事件溯源被用于系统的部分——主要是会计方面。事实上,之前尝试写下这些模式是针对会计的,因为那是我在那里看到的。会计也有助于缓解事件溯源的一些问题,以及需要事件溯源提供的强大审计功能。
处理事件
无论事件是否包含系统的所有更新,它们都提供了一些有趣的替代方案,用于组织处理它们的处理逻辑。
一种特别有趣的风格是使用调度器对象,它会询问事件的源数据以确定哪个实际执行器对象将处理该事件。这允许执行器对象保持简单,而调度器不包含任何业务逻辑,除了找到正确执行器所需的逻辑。
从概念上讲,您可以用多种方式组织这样的调度器,但我看到的一种反复出现的风格是 协议调度器。在这里,调度机制的中心组织特征是管理事件上下文的契约协议。对客户的销售可能受持续合同或隐式协议的约束,该协议规定了与偶尔客户的常见销售政策。这种风格鼓励一系列委托,其中调度器询问事件以找到要发送的协议,而协议对象承载进一步的条件检查,并且该链继续,直到我们到达最终的执行器。
优势在于,执行器和事件匹配的大部分逻辑可以设置在对象间关系中——实际上是作为数据。这使系统具有很高的可配置性。特别是,通过将时间包含在这个对象关系网络中,我们可以处理事件处理中的另一个常见问题——应对业务规则的更新。
事件和命令
在本讨论中,我指的是通过事件封装对应用程序状态的所有更改,我可以轻松地使用“命令”这个词(和模式)来表达所有这些。事件显然共享命令的大多数属性——能够拥有自己的生命周期,被排队和执行,事件反转与命令撤销相同。
使用事件的一个原因是,该术语在该领域中被广泛用于描述这种行为。人们经常听到诸如事件驱动架构和 事件消息之类的术语。
最后,我选择了事件,因为我认为它带有一组微妙但重要的关联。人们认为命令封装了一个请求——使用命令,你告诉系统执行 X。然而,事件只是传达了某件事发生了——使用事件,你让系统知道 Y 发生了。另一个区别是,你认为将事件广播给所有可能感兴趣的人,而将命令只发送给特定的接收者。当涉及到实际行为时,它并不重要。系统对 X 命令的多态反应与对 Y 事件的反应没有区别。然而,我认为命名会影响人们对事件的思考方式以及他们创建的事件类型。
命令 是 [四人帮] 中描述的经典模式。您还应该查看 [hohpe-woolf],了解 命令消息 和 事件消息 之间的对比。
重大修订
2006 年 1 月 25 日:添加了关于事件协作的概述
2005 年 12 月 12 日:在线首稿