事件驱动系统中不同类型的事件 - frankdejonge


事件驱动系统有各种形状和大小。明显的共同点是;他们都使用事件来传达信息。这些事件有多种形式和大小,确定事件中的内容会对系统设计产生巨大影响。
在这篇文章中,我想讨论三种不同类型的事件。我希望澄清这些类型将使您能够更好地讨论事件驱动的架构和集成。
三种事件原型
当我与其他开发人员讨论事件时,我区分了三种类型的事件。每种类型都有其独特的特征、优势和劣势。这些类型的事件都不一定比另一种更好,但在特定情况下,特定类型可能更适合。
这些类型的事件是:

  1. 领域事件
  2. 触发或信号事件
  3. RESTful 或“胖”事件

让我们逐一介绍它们,看看它们是什么,以及它们何时有用。
 
领域事件
对于任何对领域驱动设计感兴趣的人来说,这将是最熟悉的事件类型。领域事件是历史的记录,捕捉重要时刻的意图和任何相关上下文。领域事件关注“领域”,这意味着它们关注与业务相关的事物。因为他们记录历史,所以他们用过去时表达。
下图是事件之间的因果与相关性关系:

领域事件以明确表达意图的方式命名。建议使用人类语言来命名这些事件,尽量避免使用“哔哔”语言。而不是命名事件OrderStateChanged或OrderEvent,使用类似的东西OrderWasShipped。
与其他事件类型不同,域事件非常适合捕获意图。因为领域事件只捕捉重要时刻的相关上下文,所以它们也非常适合捕捉变化。这让活动消费者对正在发生的事情有了深刻的了解。事件源系统更进一步,使领域事件成为软件模型的基石。
领域事件特别适合用于创建读取模型。在读取用例要求与有助于做出决策的数据模型非常不同的情况下。在创建读取模型和分析数据模型中,以更改和意图为中心的表示非常适合聚合。
class OrderWasShipped
{
    public function __construct(
        private OrderId $orderId,
        private MomentOfSchipment $shippedAt,
        private ShipmentAddresss $shipmentAddress,
    );
   
    public function orderId(): OrderId
    {
        return $this->orderId;
    }
   
    public function shippedAt(): MomentOfSchipment
    {
        return $this->skippedAt;
    }
   
    public function shipmentAddress(): ShipmentAddresss
    {
        return $this->shipmentAddress;
    }
}

  • 优点

领域事件非常适合捕捉意图和“变化”。它们允许您构建强大的读取模型,其扩展性比原始数据模型上的复杂查询要好得多。领域意图对于创建分析模型非常强大,它可以提供对业务中正在发生的事情的深刻见解。
  • 缺点

域事件暴露了域内部发生的事情。如果消费者依赖此信息,他们就会与之耦合。如果使用领域事件来创建决策模型,那么这些事件的耦合会对开发速度造成压力。耦合增加了改变的成本,所以知道你暴露了什么以及暴露给谁总是好的。作为默认做法,将每个域事件视为“私有”,仅用于内部使用。只有通过故意曝光,消费者才能访问事件,类似于使用 API 而非直接访问数据库的方式。
  
触发或信号事件
触发或信号事件是最小的事件。此事件通常仅包含一个引用聚合或实体的 ID,也可能包含时间戳。正如名称触发器所暗示的,这些事件用于触发消费方的反应。触发器最常用于通知其他业务流程发生变化。在您存储敏感数据的情况下(看看您,GDPR),使用触发器可以帮助防止将事件基础设施暴露于具有挑战性的要求。
class OrderWasShipped
{
    public function __construct(
        private OrderId $orderId
    );
   
    public function orderId(): OrderId
    {
        return $this->orderId;
    }
}

  • 优点

当域事件可能包含敏感数据时,触发器很有用。在这些情况下,生产者发出一个信号并期望消费者使用安全的 API 来获取相应的 ID。触发器不容易引起信息级耦合,仅仅是因为它们不包含任何内容。
  • 缺点

由于触发器不包含任何信息,因此消费者总是依赖于 API。当许多消费者消费许多事件时,这可能会给您的系统带来一些意想不到的负载。信息的缺失也限制了汇总数据的能力。
由于事件是异步处理的,因此从 API 检索的数据可能处于消费者期望的不同状态。消费者必须始终检查从 API 检索的资源是否是他们所期望的,并准备好处理资源可能处于的任何可能状态。例如,如果订单已发货,但商家立即取消发货,则消费者可能会检索到与事件名称所暗示的状态不匹配的货运资源。当发生事件处理延迟时,这可能会产生意想不到的结果。
 
RESTful 或“胖”事件
最后一个原型是“胖”事件。我个人更喜欢术语 RESTful 事件,因为它更好地描述了有效负载中的内容。这种类型的事件包含您将从 RESTful API 检索到的完整资源表示。这是一个很好的整合活动,对外部消费者最有用。
与触发器相比,RESTful 事件阻止消费者往返 API。如果将其与域事件进行比较,它可以防止消费者不得不组合多个事件来获得完整的画面。
class OrderWasShipped
{
    public function __construct(
        private OrderId $orderId,
        private OrderLines $orderLines,
        private DiscountCodes $discountCodes,
        private OrderAmount $orderAmount,
        private MomentOfSchipment $shippedAt,
        private ShipmentAddresss $shipmentAddress,
    );
   
    public function orderId(): OrderId
    {
        return $this->orderId;
    }
   
    public function orderLines(): OrderLines
    {
        return $this->orderLines;
    }
   
    public function discountCodes(): DiscountCodes
    {
        return $this->discountCodes;
    }
   
    public function orderAmount(): OrderAmount
    {
        return $this->orderAmount;
    }
   
    public function shippedAt(): MomentOfSchipment
    {
        return $this->skippedAt;
    }
   
    public function shipmentAddress(): ShipmentAddresss
    {
        return $this->shipmentAddress;
    }
}

  • 优点

RESTful 事件非常适合将状态推送给消费者。在一个事件中,消费者对资源了如指掌。对于每个资源,只需将最后一个事件备份到最新状态,这对于灾难恢复非常有用。如果另一个服务依赖于您的服务的状态,则使用 RESTful 事件是一种将状态推送到那里的好方法。在最终一致性可以接受的情况下,这样做将消除对服务的直接依赖。
  • 缺点

根据我的经验,RESTful 事件仅作为“外部”消费者的集成工具有用。它们对内部建模没有用处。它们很大,更匿名,传达的意图较少,因此不太适合内部建模。RESTful 事件通常需要您构建一个防损坏层来将其他类型的事件转换为 RESTful 事件,这是“额外”的工作。
 
就事件进行有意义的讨论
在技​​术讨论中,很容易跳到解决方案上。只需添加该字段,只需将此内部事件公开给外部消费者,即可解决问题。我希望通过确定几种类型的事件,您可以将这些信息带入您正在进行的讨论中。尝试确定计划中的事件类型,它们具有哪些特征,以及这些特征如何影响您应用它们的情况。暴露领域事件?注意信息级耦合。因为消费者需要它们而将越来越多的信息添加到事件中?也许切换到 RESTful 事件。最后,请记住,如果在正确的环境中应用不同的沟通方式,效果最好。您应该意识到这一点并做出正确的选择。