分布式系统中解耦的模式:胖事件 - mathiasverraes

19-05-14 banq
                   

将冗余信息添加到领域事件(增加颗粒度),这样可以降低使用者的复杂性。

问题

消费者对来自生产者的一种事件类型感兴趣,对其作出反应或向用户报告信息,这是就需要对生产者的事件设计有完整性保证。该事件仅包含已更改的属性,并且不包含消费者感兴趣的其他资源状态。因此,消费者也必须收听其他许多事件,每个事件都包含一些状态更改和资源。消费者做的大部分工作是基于事件还原构建生产者发出此事件时的状态,并进行查找以将事物相互关联,消费者因此只是为了完成一项小任务就会有很多复杂性。

解决

一个胖fat事件是:不仅包含已更改的属性的事件,也有一些在该事件发出的时间内尚未改变的属性。这些是专门为减轻消费者必须做的工作而选择的:它不必持久来自其他事件类型的中间状态,因为它需要做的核心工作就是一个(或几个)胖事件。消费者现在只需要介绍更少的事件类型,也能更好地与生产者的变化隔离。

案例

消费者想要显示逾期发票的列表:它监听InvoiceBecameOverdue {invoiceId}事件,此事件仅包含一个invoiceId,但用户也需要查看客户的姓名(可能也有以及其他信息,为了让此示例简单)。所以消费者会倾听CustomerWasInvoiced {invoiceId, customerId, amount, lineItems...}事件。现在,消费者知道发票属于哪个客户,但要获得名称,它需要监听CustomerHasSignedUp {customerId, customerName, ...}和CustomerWasRenamed {customerId, newCustomerName}两个,消费者需要查找表:

class OverdueInvoices implements Projector
    when(CustomerHasSignedUp e)
        INSERT INTO CustomerNames (e.customerId, e.customerName)
    when(CustomerWasRenamed e)
        UPDATE CustomerNames (e.customerId, e.customerName)
    when(CustomerWasInvoiced e)
        INSERT INTO Invoices (e.invoiceId, e.customerId)
    when(InvoiceBecameOverdue e)
        INSERT INTO OverdueInvoices (
            SELECT invoiceId, customerName FROM Invoices i 
            LEFT JOIN CustomerNames cn ON cn.customerId = i.customerId
            WHERE invoiceId = e.invoiceId
        )
    when(WhichInvoicesAreOverdue q) 
        SELECT * FROM OverdueInvoices

在新设计中,我们首先调整生产者:InvoiceBecameOverdue事件现在也将携带属性customerName了。这样消费者查询表变得非常简单:

class OverdueInvoices implements Projector
    when(InvoiceBecameOverdue e)
        INSERT INTO OverdueInvoices (e.invoiceId, e.customerName)
    when(WhichInvoicesAreOverdue q) 
        SELECT * FROM OverdueInvoices

反模式

如果应用不正确,所有模式都会成为反模式,但Fat Events肯定会有一定的风险。

应始终谨慎地调整生产者的API以满足消费者的需求。由于许多消费者都有自己的特定需求,因此很快就会变得一团糟。事件变得巨大,随着消费者的来来往往,尚不清楚属性是否仍然相关。

改变生产者变得更难。这正是完整性保证模式应该避免的那种情况。一个有用的启发式方法是考虑消费者的数量及其所有权。如果一个团队同时拥有生产者和消费者,那么Fat Events的风险要小于大量团队每个人都希望消息中有不同的冗余属性的风险。

最终的一致性和不变性

在许多情况下,存在消费者的状态与生产者的状态不一致的风险。选择上面的发票示例来说明这一点。在我们使用第二个设计中胖事件InvoiceBecameOverdue,假设在发票过期后对客户重命名了。现在,消费者已经列出了一些过期的发票,其中包含一些过时的客户名称。在原始设计中,此问题不存在,因为如果客户名称在发票过期之前或之后发生变化并不重要,则状态始终更新。

这种不一致可能不是问题。例如,如果只有极少数客户确实改变了他们的名字,并且这些发票只能在短时间内保持不付款,那么实际不一致的可能性就会变得很小。在极少数情况下,用户可能会在一个地方看到旧名称,在另一个地方看到新名称,并轻松计算出它是同一张发票。与往常一样,它有助于仔细考虑域,用户,他们的期望和行为。

一个非常强大的启发式方法是不可变的值总是可以安全地复制。如果在生产者中,值在初始设置后永远不会更改,则不存在消费者中的副本变得不一致的风险。在我们的示例中,如果我们不允许客户更改其名称,则带有该名称值对象的胖事件是简化消费者的良好解决方案。

身份标识

不可变性的特殊情况是标识符(值对象的反面是实体)。在设计良好的系统中,ID永远不会改变。因此,假设客户可以改变他们的名字,但所有的ID是恒定的,InvoiceBecameOverdue事件可以被设计为仅包含invoiceId和customerId和值对象,但没有customerName。消费者现在需要再次查询客户名称,但不必查找单独的Invoices表。

由于复制ID的安全性,根据经验,我建议始终在事件中包含冗余ID。成本极低,简化了消费者。在处理事件描述的实体之间的多个关系时,它变得尤为重要。

比如:

CustomerHasSignedUp {customerId, customerName}
OrderWasPlaced {orderId, customerId}
InvoiceWasMadeForOrder {invoiceId, orderId}
PaymentWasReceived {paymentId, invoiceId}

在此示例中,对付款感兴趣的消费者需要三个查找表来将客户名称与付款相关联。对于胖事件,每个事件都有冗余但不可变的ID,并且只需要一个查找表:

CustomerHasSignedUp {customerId, customerName}
OrderWasPlaced {orderId, customerId}
InvoiceWasMadeForOrder {invoiceId, orderId, customerId}
PaymentWasReceived {paymentId, invoiceId, orderId, customerId}

分离的有效载荷payload

如果你担心在事件中放置冗余属性,一个很好的技巧是将有效负载分成实际的核心状态变化和“胖”两个。这使得其他开发人员明白“胖”属性不是核心事件的一部分。

InvoiceBecameOverdue { core: {invoiceId}, fat: {customerName} }

概要事件

概要事件的关系:

  • 一个概要事件通常发生在业务流程的结束,而所有事件可以胖事件。
  • 一个概要事件通常是(但不一定是)一个新的事件类型,而胖事件是现有的事件类型的附加属性。
  • 一个概要事件通常包含起因于业务流程中的资源或人工制品的完整表示,而胖事件通常只包含几个特定的消费者关心的附加属性。摘要事件更有可能对一系列不同的消费者有用。