领域驱动设计DDD入门6:领域事件

发表于2019-10-25,长度3656, 50个单词, 10分钟读完
Flag Counter

第6节课我们讲战术工具之领域事件。战术工具在战略集成中通常也占据一席之地。这节课我先讲领域事件的设计、实现和使用,再讲一下事件溯源。


因果一致性

前面已经讨论过域事件如何在有界上下文之间的集成中发挥作用。还有当发布有界上下文也是订阅有界上下文时,它们如何对发布有界上下文产生影响。基本上,发布边界上下文具有发布域事件的聚合。域事件最终进入消息传递机制,然后消息机制将该域事件发布给感兴趣的各方或订阅有界上下文;订阅有界上下文接收该域事件,然后更新一个或多个聚合。请记住,有因就有果。因是导致域事件发生然后又可能触发另一个域事件。我们必须确保在看到结果之前先发现原始原因,因为原因必须在结果之前发生。举个例子:这是一个带有讨论和讨论中帖子的论坛。小孙在讨论中发布了一条消息说:“我丢了钱包!”小李阅读了她的信息并说:“那太糟糕了”。然后小孙又发布了一条新消息说:“不用担心,我找到了我的钱包!”最后,小李回复新帖子说:“太好了!” 现在,请考虑一下分布式系统。假设这些帖子都从北京发出,然后上海的用户在自己机器上阅读它们,如果因果不正常怎么办?例如,如果小孙的帖子(第一个说“我丢了钱包”)很快就到达了上海的服务器,但是由于某种原因,小李对小孙的原始信息的回复有所延迟,小孙先收到的是:“太好了!”回应“我丢了钱包”。这才真的糟糕呢。怎么办?必须确保在收到第二条消息时,小李说“这太糟糕了”回应小孙的第一条消息:“我丢了钱包”。在第二条消息之后,必须确保接下来看到第三条消息。最后必须确保在第三条消息之后可以看到第四条消息,小李回答说:“太好了。”这是因果一致性。我们确保在对每个原因的响应之前先了解原因。因此,如果将每一个帖子都视为域事件,域事件一,域事件二,域事件三和域事件四,那么我们必须确保每个事件都是按发生顺序接收和处理的。这是因果一致性,它使系统健壮、定义明确且正确。

事件建模

那么如何设计或建模域事件并实现它们呢?首先,我建议为所有域事件创建一个接口。例如该接口被命名为DomainEvent,并且至少应具有OccurredOn时间戳,并且它有一个公开的getter。所有域事件都可能实现此接口。这里有一些域事件的示例:ProductCreated、SprintScheduled、ReleaseScheduled、BacklogItemPlanned、BacklogItemCommitted。注意这些域事件的命名:一个名词,跟一个动词过去式。因此ProductCreated表示产品是已经创建的。这样表达的是我们已经捕获了一个事实,即我们的域模型中发生了一些事情。 但是,域事件中存在哪些字段或属性?对于ProductCreated,这个域事件的字段或属性应该是什么?域事件的因是什么?实际原因是在命令CreateProduct中表达的,这个命令要求向域模型创建产品。做为响应,产品聚合会创建产品,并且发布名为ProductCreated的域事件。因此,该命令自然具有其参数,例如tenantId,这是该服务的订阅租户,productId是产品聚合的ID,唯一的全局标识,此外还有名称和产品说明。因此,CreateProduct命令将使用ProductCreated事件的属性,我们实际上要做的是从事件的命令中提供完全相同的属性,并且做为果,CreateProduct命令中有一个ProductCreated事件,它也有一个tenantId,一个productId ,名称和说明。这说明了原因起作用方式:该产品是根据这些值创建的我先前介绍的每个域事件都具有特定且适当的属性。

例如,我们刚刚看的ProductCreated。 SprintScheduled具有tenantId,sprintId,productId(因为产品是sprint的父聚合),名称,描述,startsOn和endsOn。ReleaseScheduled具有tenantId,releaseId,productId,名称,描述和targetDate。 BacklogItemPlanned具有tenantId,backlogItemId,productId,sprintId以及一个故事和摘要。 BacklogItemCommitted具有tenantId,backlogItemId和sprintId。 CommitBacklogItemToSprint是一个命令。该命令对名为BacklogItem的聚合有影响,BacklogItem将被修改以提交给sprint。当将BacklogItem提交给sprint时,我们将产生一个事实,即BacklogItemCommitted域事件,该事件具有tenantId,backlogItemId和要提交到的sprint的sprintId。 CommitBacklogItemToSprint产生影响后,将触发域事件发生。然后将域事件以及BacklogItem的新状态保存到数据库的事件表中。 因此,在单个事务中,BacklogItem被提交到数据库,并且BacklogItemCommitted事件也被提交到数据库。它们要么一起保存,要么一起回滚。以保证只要将BacklogItem提交给sprint,我们就绝对拥有将BacklogItem提交的事实同时保存在数据库中的新状态。将BacklogItemCommitted域事件保存到数据库的特定事件表中之后,可以从该表中读取并通过消息机制进行发布。订阅有界上下文,甚至我们自己的敏捷项目管理上下文,就可以订阅它。从前面的课程中我们知道它确实订阅了,并且此BacklogItemCommitted域事件最终对sprint产生了影响。 域事件是事实。但是,它们可能不是命令引起的。到目前为止,我们一直在研究命令如何对聚合产生影响,并且这种影响会导致域事件,但是有时会有非命令源。例如,一个事件可能是基于时间的,如果这些时间范围具有重要的业务意义,可能是一天,一周或一年的结束。例如,当会计年度结束时,我们可能想要一个域事件,该事件表明该会计年度结束。我们也可能有一个股市关闭领域事件。例如,每天下午3点股市关闭。可能不一定是3点,但这是市场关闭的事实。

事件溯源

到目前为止,我们还没有谈论将事件用于聚合的持久状态。这是事件溯源所提供的。如果我们的待办聚合是可事件溯源的聚合,我们会保存每一个发生在聚合上的重要事件。

例如,如果从图的右侧查看事件序列,会看到“BacklogItemPlanned”是待办聚合中的第一个事件。第二个事件是“BacklogItemStoryDe​​fined”,第三事件是响应给左侧“CommitBacklogItemtoSprint”命令的事件。当“CommitBacklogItemToSprint”命令施在待办上是它会导致域事件“BacklogItemCommited”。这些域事件中的每一个发生时,它们都将作为事件流保存到我们数据库中的事件日志中。如果后面要恢复待办聚合的状态,我们可以按它们最初发生的顺序读回每个域事件,并解释它们或将这些域事件应用于待办项目的状态。

  • 第一步:“CommitBacklogItemtoSprint”是对待办聚合执行的命令。
  • 第二步:待办事项聚合发出待办事项被提交的域事件。
  • 第三步:在“BacklogItemCommitted”域事件被依序保存在事件日志数据库表中。
  • 第四步,待办聚合需要从其持久状态重建。

我们从事件日志中按事件发生的先后顺序读取事件:“BacklogItemPlanned”,“ BacklogItemStoryDe​​fined”和“ BacklogItemCommited”,然后将这些事件重新应用于待办聚合,以恢复其状态。这是事件源背后的基本思想。

当将“BacklogItemCommited”域事件写入事件存储时,请注意附加上流信息。假设我们的待办具有标识“BacklogItem123”,这将成为待办关联的流ID。当我们更新事件日志表时,我们将附加“BacklogItem123”作为流ID。流版本是一个从1开始的序列号,标明事件实际发生的顺序。然后,将域事件也写入事件类型对应的列中,也就是将“BacklogItemCommited”事件写入事件类型中,事件内容就是序列化的域事件。因此,事件类型命名了域事件的类型,事件内容是域事件状态的内容。现在,如果要重建待办聚合,我们将读取流ID为“BacklogItem123”的整个流,并读取“BacklogItem123”存在的所有版本。

在这种情况下,它是流版本1、2和3,其事件类型为“BacklogItemPlanned”,然后是“BacklogItemStoryDe​​fined”,然后是“ BacklogItemCommmited”。加上事件内容,将使用这些内容来重新构建待办聚合的状态。

Written on October 25, 2019
分类: dev, 标签: ddd
如果你喜欢,请赞赏! davelet