第8章 领域事件
- 第1章 DDD入门
- 第2章 领域、子域和限界上下文
- 第3章 上下文映射图
- 第4章 架构
- 第5章 实体
- 第6章 值对象
- 第7章 领域服务
- 第8章 领域事件
- 第9章 模块
- 第10章 聚合
- 第11章 工厂
- 第12章 资源库
- 第13章 集成限界上下文
- 第14章 应用程序
本章学习路线图
- 学习什么是领域事件,什么时候并且为什么要使用领域事件
- 学习如何将领域事件建模成对象,何时应该为领域事件创建唯一的身份标识
- 学习一个轻量级的发布-订阅模式
- 学习哪些组件用于发布事件,哪些组件用于订阅事件
- 学习为什么我们需要一个事件存储、如何实现事件存储、如何使用使用事件存储
- 学习SooSOvation团队是如何通过不同的方式将领域事件发布给自治系统的
使用**领域事件(Domain Event)**来捕获发生在领域中的一些事情。
何时/为什么使用领域事件
当前对领域事件的定义:
- 领域专家所关心的发生在领域中的一些事情。
- 将领域中所发生的活动建模成一些列的离散事件。每个事件都用领域对象来表示,领域事件是领域模型的组成部分,表示领域中所发生的事情。
考虑以下领域专家的关键词汇来确定哪些事件比较重要:
- 当……
- 如果发生……
- 当……的时候
- 发生……时
根据组织文化,通过此类事件用于来确定更多的事件。
有时在跨团队沟通时会发现先前领域专家未意识到的一些需求,一般这种情况是由于领域事件需要发布到外部系统中,比如发布到另一个限界上下文中。
当团队成员对领域事件达成一致之后,领域事件便是通用语言的正式组成部分了。
当领域事件达到目的地后——无论是本地系统还是外部系统——我们通常将领域事件用于维护事件的一致性。这样可以消除两阶段提交(全局事务),还可以支持聚合(10)。
聚合的其中一个原则是:在单个事务中,只允许对一个聚合实例进行修改,由此产生的其他改变必须在单独的事务中完成。
如图所示展示了领域事件的产生、存储、分发和使用。领域事件即可以由本地限界上下文所消费,也可以由外部限界上下文消费。
对于系统中发生的每一件事情,我们都可以用事件的形式予以捕获,然后将事件发布给订阅方处理,这样能达到简化系统的目的。原来批量集中处理的过程可以分散成许多粒度较小的单元。
建模领域事件
在建模领域事件时,我们应该根据限界上下文中的通用语言来命名事件及其属性。如果事件由聚合上的命令操作产生,那么我们通常根据该操作方法的名字来命名领域事件。在聚合事件发布时,请注意我们应该使事件的名字反应过去发生的事情,即该事件并不是当前发生的,而是先前发生的。需要结合书中示例代码。
创建具有聚合特征的领域事件
领域事件也有可能直接由客户方发出的请求产生,此时领域事件可以建模成一个聚合,并且可以拥有自己的资源库。但是由于领域事件表示过去发生的事情因此资源库是不能对事件进行删除的。
此时领域事件依然应该设计成不变的,并拥有唯一标识来保证事件的唯一性。
客户放可以通过调用**领域服务(7)**来创建事件,然后将其添加到资源库中,再通过消息基础设施进行发布。
身份标识
当领域事件被建模成了聚合;或者我们需要对不同的事件进行比较,但是事件的属性又不足以区分事件时,我们便需要一个唯一标识。当我们需要将领域事件发布到外部限界上下文中时,为事件创建唯一标识也是有必要的。
从领域模型中发布领域事件
我们应该避免将领域模型暴露给任何类型的消息中间件。这些消息中间件只存在于基础设施层中。一种简单高效的发布领域事件的方法便是使用观察者模式,这种方法可以在领域模型和外部组件之间进行解耦。
具体的发布/订阅代码示例说明详见书中对应章节。
向远程限界上下文发布领域事件
首先可以使用消息机制。存在多种消息组件,通常被称为消息中间件,如开源的ActiveMQ、RabbitMQ、Akka等。
在不同的限界上下文之间采用这些消息系统时,我们必须保证最终一致性,在一个模型中的改变肯呢个需要很长一段时间才能反应到另一模型中。
消息设施的一致性
对于最终一致性,我们至少需要在两种存储之间保持最终一致性:领域模型所使用的持久化存储和消息设施所使用的持久化存储。如果两者没有同步,有可能导致模型处于不正确的状态。
有三种基本方式保证两者之间的一致性:
- 领域模型和消息设施共享持久化存储(比如:数据源)。
- 领域模型的持久化存储和消息持久化存储由全局的XA事务(两段提交)所控制。
- 在领域模型的持久化存储中,创建一个特殊的存储区域(比如一张数据表),改区域用于存储领域事件,这便是一个事件存储。
书中选择了方法3,每种情况都有优缺点,需要根据项目实际情况进行选择。
自治服务和系统
这里的自治服务表示一个设计良好的业务服务,我们可以将其看成一个系统或者应用程序。在整个企业范围之内,这些自治服务相互独立的完成各自的服务。
容许时延
我们必须知道多长的时间延迟是可以接受的,多长是可能导致问题的。
事件存储
对于单个限界上下文的所有领域事件来说,为它们维护一个事件存储是有好处的。对于存储由每个模型的命令方法所产生的离散领域事件,可能的方法如下:
- 将事件存储作为一个消息队列来使用,该消息队列的作用是将所有的领域事件通过消息基础设施发布出去。
- 将相同的事件存储用基于REST的事件通知。
- 检查由模型的命令方法所产生的所有结果的历史记录。
- 使用事件存储中的数据来进行业务预测和分析。
- 当从资源库中获取一个聚合实例时,使用事件来重建该聚合实例。
- 撤销对聚合的操作。
书中下文给出SaaSOvation团队的具体代码示例,通过面向切面的方式在每个应用层的执行流中插入对订阅方的注册功能。
转发存储事件的架构风格
本节讨论两种转发事件的架构风格。一个是基于REST资源的方式,一种是基于消息中间件的方式。
以REST资源的方式发布通知
在那些具有基本发布-订阅功能的系统环境中,才用REST风格的事件通知是最合适的。以下是REST风格事件通知的优缺点:
- 一个事件通知可以拥有任意多的消费方。此时用的是“拉”的方式。
- 许多发送方同时为一个或多个消费方服务,此时事件的接收顺序是重要的。对于消息队列来说,“拉”就不是一个好的方法。
书以日志事件为为示例,通过日志ID结合URLhttp://iam/notification/1,58;rel=self
的方式提供1~58号日志事件的拉取方式,并且通过REST的缓存配置提供缓存能力。
通过消息中间件发布事件通知
在使用消息中间件的时候,我们可以省略之前需要在REST风格中的细节处理问题。一般情况下,消息队列是通过“推送”的方式来发送事件通知消息的。
每一个订阅系统都需要自己负责处理所接收到的消息,并保证其自身模型中的领域行为的到了正确的调用。对于消息系统来说,我们只是确保对消息的投递。