DDD理论学习系列——案例及目录


1. 引言

A domain event is a full-fledged part of the domain model, a representation of something that happened in the domain. Ignore irrelevant domain activity while making explicit the events that the domain experts want to track or be notified of, or which are associated with state change in the other model objects.

领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联。

针对官方释义,我们可以理出以下几个要点:

  1. 领域事件作为领域模型的重要部分,是领域建模的工具之一。
  2. 用来捕获领域中已经发生的事情。
  3. 并不是领域中所有发生的事情都要建模为领域事件,要忽略无业务价值的事件。
  4. 领域事件是领域专家所关心的(需要跟踪的、希望被通知的、会引起其他模型对象改变状态的)发生在领域中的一些事情。

简而言之,领域事件是用来捕获领域中发生的具有业务价值的一些事情。它的本质就是事件,不要将其复杂化。在DDD中,领域事件作为通用语言的一种,是为了清晰表述领域中产生的事件概念,帮助我们深入理解领域模型。

2. 认识领域事件

当用户在购物车点击结算时,生成待付款订单,若支付成功,则更新订单状态为已支付,扣减库存,并推送捡货通知信息到捡货中心。

在这个用例中,“订单支付成功”就是一个领域事件。

考虑一下,在你没有接触领域事件或EDA(事件驱动架构)之前,你会如何实现这个用例。肯定是简单直接的方法调用,在一个事务中分别去调用状态更新方法、扣减库存方法、发送捡货通知方法。这无可厚非,毕竟之前都是这样干的。

那这样设计有什么问题?

  1. 试想一下,若现在要求支付成功后,需要额外发送一条付款成功通知到微信公众号,我们怎么实现?想必我们需要额外定义发送微信通知的接口并封装参数,然后再添加对方法的调用。这种做法虽然可以解决需求的变更,但很显然不够灵活耦合性强,也违反了OCP。
  2. 将多个操作放在同一个事务中,使用事务一致性可以保证多个操作要么全部成功要么全部失败。在一个事务中处理多个操作,若其中一个操作失败,则全部失败。但是,这在业务上是不允许的。客户成功支付了,却发现订单依旧为待付款,这会导致纠纷的。
  3. 违反了聚合的一大原则:在一个事务中,只对一个聚合进行修改。在这个用例中,很明显我们在一个事务中对订单聚合和库存聚合进行了修改。

那如何解决这些问题?我们可以借助领域事件的力量。

  1. 解耦,可以通过发布订阅模式,发布领域事件,让订阅者自行订阅;
  2. 通过领域事件来达到最终一致性,提高系统的稳定性和性能;
  3. 事件溯源;
  4. 等等。

下面我们就来一一深入。

3.建模领域事件

如何使用领域事件来解耦呢?

当然是封装不变,应对万变。那针对上面的用例,不变的是什么,变的又是什么?不变的是订单支付成功这个事件;变化的是针对这个事件的不同处理手段。

而我们要如何封装呢?

这时我们就要理清事件的本质,事件有因必有果,事件是由事件源和事件处理组合而成的。通过事件源我们来辨别事件的来源,事件处理来表示事件导致的下一步操作。

3.1. 抽象事件源

事件源应该至少包含事件发生的时间和触发事件的对象。我们提取IEventData接口来封装事件源:

/// <summary>
/// 定义事件源接口,所有的事件源都要实现该接口
/// </summary>
public interface IEventData
{
/// <summary>
/// 事件发生的时间
/// </summary>
DateTime EventTime { get; set; } /// <summary>
/// 触发事件的对象
/// </summary>
object EventSource { get; set; }
}

通过实现IEventData我们可以根据自己的需要添加自定义的事件属性。

3.2. 抽象事件处理

针对事件处理,我们提取一个IEventHandler接口:

 /// <summary>
/// 定义事件处理器公共接口,所有的事件处理都要实现该接口
/// </summary>
public interface IEventHandler
{
}

事件处理要与事件源进行绑定,所以我们再来定义一个泛型接口:

 /// <summary>
/// 泛型事件处理器接口
/// </summary>
/// <typeparam name="TEventData"></typeparam>
public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData
{
/// <summary>
/// 事件处理器实现该方法来处理事件
/// </summary>
/// <param name="eventData"></param>
void HandleEvent(TEventData eventData);
}

以上,我们就完成了领域事件的抽象。在代码中我们通过实现一个IEventHandler<T>来表达领域事件的概念。

3.3. 领域事件的发布和订阅

领域事件不是无缘无故产生的,它有一个发布方。同理,它也要有一个订阅方。

那如何和订阅和发布领域事件呢?

领域事件的发布可以使用发布--订阅模式来实现。而比较常见的实现方式就是事件总线

事件总线是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。Event Bus就相当于一个介于Publisher(发布方)和Subscriber(订阅方)中间的桥梁。它隔离了Publlisher和Subscriber之间的直接依赖,接管了所有事件的发布和订阅逻辑,并负责事件的中转。

这里就简要说明一下事件总线的实现的要点:

  1. 事件总线维护一个事件源与事件处理的映射字典;
  2. 通过单例模式,确保事件总线的唯一入口;
  3. 利用反射或依赖注入完成事件源与事件处理的初始化绑定;
  4. 提供统一的事件注册、取消注册和触发接口。

最后,我们看下事件总线的接口定义:

public interface IEventBus
{
void Register < TEventData > (IEventHandler eventHandler); void UnRegister < TEventData > (Type handlerType) where TEventData: IEventData; void Trigger < TEventData > (Type eventHandlerType, TEventData eventData) where TEventData: IEventData;
}

在应用服务和领域服务中,我们都可以直接调用Register方法来完成领域事件的注册,调用Trigger方法来完成领域事件的发布。

而关于事件总线的具体实现,可参考我的这篇博文——事件总线知多少

4. 最终一致性

说到一致性,我们要先搞明白下面几个概念。

事务一致性

事务一致性是是数据库事务的四个特性之一,也就是ACID特性之一:

原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。

一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。

隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。

持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。

我们用一张图来理解一下:



在事务一致性的保证下,上面的图示只会有两个结果:

  1. A和B两个操作都成功了。
  2. A和B两个操作都失败了。

数据一致性

举个简单的例子,假设10个人,每人有100个虚拟币,虚拟币仅能在这10人内流通,不管怎么流通,最终的虚拟币总数都是1000个,这就是数据一致性。

领域一致性

简单理解就是在领域中的操作要满足领域中定义的业务规则。比如你转账,并不是你余额充足就可以转账的,还要求账户的状态为非挂失、锁定状态。

回到我们的案例,当支付成功后,更新订单状态,扣减库存,并发送捡货通知。按照我们以往的做法,为了维护订单和库存的数据一致性,我们将这三个操作放到一个应用服务去做(因为应用服务管理事务),事务的一致性可以保证要么全部成功要么全部失败。但是,试想一下,客户支付成功后,订单依旧为待付款状态,这会引起纠纷。另外,由于库存没有及时扣减,很可能会导致库存超卖。怎么办呢?

将事务拆解,使用领域事件来达到最终一致性。

最终一致性

“最终一致性”是一种设计方法,可以通过将某些操作的执行延迟到稍后的时间来提高应用程序的可扩展性和性能。

对于常见于分布式系统的最终一致性工作流中,客户同样在系统中执行一个命令,但这个系统只为维护事务中的领域一致性运行部分的操作,剩余的操作在允许延后执行。针对上图的结果:

  1. A操作执行成功,B操作将延后执行。
  2. A操作失败,B操作将不会执行。

而针对我们的案例,我们如何使用领域事件来进行事务拆分呢?我们看下下面这张图你就明白了。

分析一下,针对我们案例,我们发现一个用例需要修改多个聚合根的情况,并且不同的聚合根还处于不同的限界上下文中。其中订单和库存均为聚合根,分别属于订单系统和库存系统。我们可以这样做:

  1. 在订单所在的聚合根中更新订单支付状态,并发布“订单成功支付”的领域事件;
  2. 然后库存系统订阅并处理库存扣减逻辑;
  3. 通知系统订阅并处理捡货通知。

通过这种方式,我们即保证了聚合的原则,又保证了数据的最终一致性。

5. 事件存储和事件溯源

关于事件存储(Event Store)和事件溯源(Event Sourcing)是一个比较复杂的概念,我们这里就简单介绍下,不做过多展开,后续再设章节详述。

事件存储,顾名思义,即事件的持久化。那为什么要持久化事件?

  1. 当事件发布失败时,可用于重新发布。
  2. 通过消息中间件去分发事件,提高系统的吞吐量。
  3. 用于事件溯源。

源代码管理工具我们都用过,如Git、TFS、SVN等,通过记录文件每一次的修改记录,以便我们跟踪每一次对源代码的修改,从而我们可以随时回滚到文件的指定修改版本。

事件溯源的本质亦是如此,不过它存储的并非聚合每次变化的结果,而是存储应用在该聚合上的历史领域事件。当需要恢复某个状态时,需要把应用在聚合的领域事件按序“重放”到要恢复状态对应的领域事件为止。

6.总结

经过上面的分析,我们知道引入领域事件的目的主要有两个,一是解耦,二是使用领域事件进行事务的拆分,通过引入事件存储,来实现数据的最终一致性。

最后,对于领域事件,我们可以这样理解:

通过将领域中所发生的活动建模成一系列的离散事件,并将每个事件都用领域对象来表示,来跟踪领域中发生的事情。

也可以简要理解为:领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理

以上,仅是个人理解,DDD水很深,剪不断,理还乱,有问题或见解,欢迎指正交流。

参考资料:

在微服务中使用领域事件

使用聚合、事件溯源和CQRS开发事务型微服务

如何理解数据库事务中的一致性的概念?

Eventual Consistency via Domain Events and Azure Service Bus

DDD理论学习系列(9)-- 领域事件的更多相关文章

  1. DDD理论学习系列(8)-- 应用服务&领域服务

    DDD理论学习系列--案例及目录 1. 引言 单从字面理解,不管是领域服务还是应用服务,都是服务.而什么是服务?从SOA到微服务,它们所描述的服务都是一个宽泛的概念,我们可以理解为服务是行为的抽象.从 ...

  2. DDD理论学习系列(2)-- 领域

    DDD理论学习系列目录 1. 引言 领域一词,主要有以下两个意思: 一国主权所达之地. 学术思想或社会活动的范围. 不管是指国家的主权范围也好还是学术活动范围,都是在讲一个范围,一个界限. 比如我们常 ...

  3. DDD理论学习系列(6)-- 实体

    DDD理论学习系列--案例及目录 1.引言 实体对应的英语单词为Entity.提到实体,你可能立马就想到了代码中定义的实体类.在使用一些ORM框架时,比如Entity Framework,实体作为直接 ...

  4. DDD理论学习系列(10)-- 聚合

    DDD理论学习系列--案例及目录 1.引言 聚合,最初是UML类图中的概念,表示一种强的关联关系,是一种整体与部分的关系,且部分能够离开整体而独立存在,如车和轮胎. 在DDD中,聚合也可以用来表示整体 ...

  5. DDD理论学习系列(13)-- 模块

    DDD理论学习系列--案例及目录 1. 引言 Module,即模块,是指提供特定功能的相对独立的单元.提到模块,你肯定就会想到模块化设计思想,也就是功能的分解和组合.对于简单问题,可以直接构建单一模块 ...

  6. DDD理论学习系列——案例及目录

    目录 DDD理论学习系列(1)-- 通用语言 DDD理论学习系列(2)-- 领域 DDD理论学习系列(3)-- 限界上下文 DDD理论学习系列(4)-- 领域模型 DDD理论学习系列(5)-- 统一建 ...

  7. DDD理论学习系列(4)-- 领域模型

    DDD理论学习系列目录 1.引言 我们还是先来拆词理解,领域模型可以拆为"领域"和"模型"二词. 领域:按照我们之前的文章的理解,DDD中的领域是指软件系统要解 ...

  8. DDD理论学习系列(5)-- 统一建模语言

    DDD理论学习系列--案例及目录 1.引言 上一节讲解了领域模型,领域模型主要是将业务中涉及到的概念以面向对象的思想进行抽象,抽象出实体对象,确定实体所对应的方法和属性,以及实体之间的关系.然后将这些 ...

  9. DDD理论学习系列(7)-- 值对象

    DDD理论学习系列--案例及目录 1.引言 提到值对象,我们可能立马就想到值类型和引用类型.而在C#中,值类型的代表是strut和enum,引用类型的代表是class.interface.delega ...

随机推荐

  1. 基于OWIN+DotNetOpenOAuth实现OAuth2.0

    这几天时间一直在研究怎么实现自己的OAuth2服务器,对于太了解OAuth原理以及想自己从零开始实现的,我建议可以参考<Apress.Pro ASP.NET Web API Security&g ...

  2. jquery和vue对比

    1.jquery介绍:想必大家都用过jquery吧,这个曾经也是现在依然最流行的web前端js库,可是现在无论是国内还是国外他的使用率正在渐渐被其他的js库所代替,随着浏览器厂商对HTML5规范统一遵 ...

  3. 2D游戏开发(2)

    每次给游戏添加新功能时,通常也会引入一些新设置.为了让所有的设置进行统一管理,我们可以配置一个名为 setting的模块,这个模块中包含一个setting的类,用来存储所有的设置. #代码-- #!/ ...

  4. RedHat7上安装MySQL5.7.16

    1.查看系统中是否已将安装MySQL,如果安装了,需要卸载. [root@chenguo etc]# rpm -qa|grep -i mysql 2.创建用户和组 [root@chenguo ~]# ...

  5. 在windows环境下利用virtualenv搭建Python虚拟环境

    安装Python 安装时只有一点需要注意,一定一定要将Python添加到系统环境变量那一项勾选. 安装 virtualenv 加入系统目录之后,命令行(CMD)下就多了一条命令:pip.用pip可以自 ...

  6. Qt之新手打包发布程序

    工具:电脑必备.QT下的windeployqt Qt 官方开发环境使用的动态链接库方式,在发布生成的exe程序时,需要复制一大堆 dll,如果自己去复制dll,很可能丢三落四,导致exe在别的电脑里无 ...

  7. (原创)Maven+Spring+CXF+Tomcat7 简单例子实现webservice

    这个例子需要建三个Maven项目,其中一个为父项目,另外两个为子项目 首先,建立父项目testParent,选择quickstart: 输入项目名称和模块名称,然后创建: 然后建立子项目testInt ...

  8. 化繁为简(三)—探索Mapreduce简要原理与实践

    目录-探索mapreduce 1.Mapreduce的模型简介与特性?Yarn的作用? 2.mapreduce的工作原理是怎样的? 3.配置Yarn与Mapreduce.演示Mapreduce例子程序 ...

  9. 实时监控、直播流、流媒体、视频网站开发方案流媒体服务器搭建及配置详解:使用nginx搭建rtmp直播、rtmp点播、,hls直播服务配置详解

    注意:这里不会讲到nginx流媒体模块如何安装的问题,只研究rtmp,hls直播和录制相关的nginx服务器配置文件的详细用法和说明.可以对照这些命令详解配置nginx -rtmp服务 一.nginx ...

  10. Spring学习(4)---Bean基础

    Bean配置项 Bean的作用域 Bean的生命周期 Bean的自动装配 Resources & ResourceLoader (一) Bean配置项 常用的配置项 Id   (IOC容器中B ...