如何运用领域驱动设计 - 领域事件

开篇

距离发布上一篇该系列的文章好像已经过了快一个半月了,好吧,我托更了????。一晃就已经到了3月份,在这樱花????盛开的季节,终于得重新连载该系列了。在停更的期间时不时会收到大家关于DDD的留言和问题,一旦我有时间一定会回复大家的问题。在此,衷心感谢大家对本系列文章的支持????。

概述

在实践领域驱动设计(DDD)的过程中,我们往往会遇到多个领域对象相互交互的情况。比如聚合根A在执行某操作之前需要得到聚合根B的某个信号(或某些数据)。如果在单体应用程序中,我们有条件和机会使得两者进行强引用来完成操作,但是这将直接打破领域驱动设计的规范,从而使得项目不可控,再次回到大泥球的开发。

现在,咱们可以选取一种更纯净的方式来解决这类问题,并且还能够更清晰的描述领域对象的活动迹象。这就是咱们今天的主题 ———— “领域事件”。那么到底什么是领域事件呢?引入领域事件会为我们已有的DDD项目带来哪些益处?是否一定要使用领域事件呢?本文将从不同的角度来带大家重新认识一下“领域事件”这个概念,并且给出相应的代码片段(本教程的代码片段都使用的是C#,当然思想是跨越任何编程语言的????)。

什么是领域事件

在原著 《领域驱动设计:软件核心复杂性应对之道》 其实并没有直接提及到关于领域事件的介绍。领域对象是在后期才被作者Evans提出,经过Udi Dahan(Nservicebus作者)和Jimmy Bogard(MetdiaR、AutoMapper作者)等专家后期的不断实践和演变才有了今天的领域事件版本。

此处我摘录了《实现领域驱动设计》书中对领域事件的描述:

领域专家所关心的发生在领域中的一些事件。
将领域中所发生的活动建模成一系列的离散事件。每个事件都用领域对象来表示,领域事件是领域模型的组成部分,表示领域中所发生的事情。

如何使用领域事件

当您一看到“事件”这个词语的时候,您可能会一下联系到 C# 中的事件,那个基于委托的事件。确实,它们之间有着共性,就比如:“当事件发生的时候,与该事件相关联的对象都将受到波及。” 所以,如果您了解C#中的事件,那将帮助您更好的理解“领域事件”。

由此我们可以推导出:在领域驱动设计建模过程中,如果发现有一项动作发生了之后,与之关联的其他领域对象将会受到波及。 那么该动作可能就是“领域事件”。

光从概念上来讲些许有些让人头晕,我们来看看实际的一个例子:“当用户将商品添加到购物车的时候,下方的推荐商品将为他推荐同类型的商品”。这是一个有前后发生关系的典型案例,商品被添加到了购物车就会引发推荐同类商品。所以咱们仔细来感受一下这一个过程,抓一抓里面的关键词。“商品加入购物车” 就会导致 “推荐同类商品”。是不是和咱们上面那一段的描述有些类似了?所以仔细观察之后,我们可以捕获出一个领域对象来,该对象您可能将它命名为(ProductAddedEvent)。

为什么我们要将它命名为过去时呢?这也是印证了开头那句话“动作发生了之后”。当该事件被捕获了之后,就会将事件信息传递给“推荐商品”聚合根,执行相应处理逻辑。

那么事件的来源是哪里呢?“用户点击”,“网页响应” 这些都不是哦!记住,我们要深刻关心领域对象,刚才所说的情况显然与咱们的领域对象一点儿关系也没有。所以我们可以很自然的将目光转向到“购物车”,“购物车”可能就是一个聚合根,它会有一个叫做“添加商品”的行为,当该行为完成之后就会引发一个“商品添加完成”的事件。

经过整理之后我们可能会得到一个这样的流程:

所以您会发现,领域事件一方面充当了描述领域信息的作用,一方面承接了不同聚合根之间的交互。当然事件不一定只有一个,被影响的领域对象也不一定只有一个。就好比“推荐商品”受到了“商品添加完成”事件之后,它自己也能产生一个另外的领域事件传递给下游。

思维的转换

到这里您或许会感到使用领域事件和以往咱们捕获其他对象不太一样,比如捕获值对象、实体等。因为对于领域事件来说,它可能是“隐式”,我们没有直观的感受它的存在。

所以,请仔细的考虑这一点:当您要使用领域事件时,您将认同您的项目需要以事件作为中心。而项目中的各个领域对象都将以产生、发布领域事件完成一系列的交互流程。

这里我摘录了《领域驱动设计模式、原理与实践》中的一段话分享给大家:“领域事件将会在领域专家一起进行的知识提炼环节中揭示出来。揭示领域事件是如此有价值,DDD实践者都拥有创新的知识提炼技术来进行实践以便让其更专注于事件,比如事件风暴。不过,使用这些创新技术会带来新的挑战。既然概念化的模型都是以事件为中心的,那么代码也需要以事件为中心,以便它能够表述概念化模型。这就是领域事件设计模式所带来的价值。”

所以在大多数时候您将感受到项目逐渐具有 EDA(事件驱动架构)的风格。而此时,您可能会联想到DDD中的另外一种模式:事件溯源(EventSource),认为自己必须要采用事件溯源来建立您的ddd项目。其实这并不是一定的,采用领域事件和使用事件溯源是没有直接关系的,虽然领域事件会帮助事件溯源完成的更好。

捕获领域事件

结合上面的介绍,您可能已经对发现领域事件有一点感觉了。当聚合与聚合之间具有交互关系时,我们往往会发现他们之间会存在某个领域事件来引发这系列行为。

如果与领域专家交谈时,发现了这样的关键词汇:“当………………”、“如果A完成之后,那么…………”,“发生…………的时候”。这些词汇可能在隐式的告诉您,该处也许存在着“领域事件”对象。

内部事件 and 外部事件

在使用领域事件之前,我们必须要知道事件其实被划分成了:“内部”和“外部”。就正如它的描述一样,内部的领域事件发生在边界之内,而外部的事件发生在边界之外(比如微服务A产生了一个事件,而微服务B会受到该事件的影响)。

在Microsoft关于ESHOP案例的指导书籍《.NET 微服务 - 体系结构》 中,将其命名为“领域事件和集成事件”:

该图也形象的说明了基于一个边界内的内部事件是如何交互的:

外部的事件往往需要一些基础结构来实现远程服务之间的进程间和分布式通信,比如rabbitMQ,kafka等。本篇文章重点讲解内容为内部的领域事件,关于外部的事件将会在后期《分布式中的领域驱动设计》系列中为大家介绍。

可选 Or 必须

那么是否我的DDD项目就必须使用“领域事件”呢?也许您在网上从来没有见到过这样的问题,因此也没有该问题的确切性答案。关于该问题,我个人觉得答案是“不一定”。

就像上文说的一样,如果您开始使用领域事件,那么就证明您的项目和思维将转换为“以事件作为中心”。领域中大部分的交互都将以事件的方式来呈现。所以与其考虑“我的DDD项目就必须使用“领域事件””这个问题,还不如转换为:“我是否需要用事件作为中心来考虑问题?”。

所以,该问题的答案就取决于您自己了。这也是为什么您会在某些DDD框架或者DDD项目中没有发现“领域事件”的原因之一。

那么,如果不使用事件来建模,聚合与聚合之间是如何进行交互的呢?请看下文↓。

领域事件 VS 领域服务

我利用搜索引擎进行了大量的查找,没有发现任何关于“领域事件” 和 “领域服务”之间的对比内容。但是我认为这两者却有着很多相似的地方。当Evans在初次提出领域驱动的概念时,是没有考虑领域事件的,那么也就意味着我们能够通过原有的领域对象完成领域建模和业务流程。

回到刚才那个问题,聚合与聚合之间只能通过事件完成操作吗?不一定。“领域服务”也承担着领域对象与领域对象转换的功能。

先回顾一下咱们在领域服务章节了解到的部分内容:

当我们发现一个操作无法赋予一个实体或者值对象,且该操作又对业务流程很重要时,我们往往需要使用领域服务
通过A和B,得到一个C。
A需要一个繁琐的内部策略才能得到一个结果B。(ps: A,B,C指的是领域对象中的值对象或者实体)

所以这也意味着,领域服务内部可以对多个领域对象(比如聚合根)进行操作。所以某些DDD框架将领域服务作为完成流程操作的主要工具,允许使用者在领域服务中注入多个仓储,从而对多个聚合根进行操作。

而“领域事件”呢,它通过发布领域事件来达到不同领域对象的交互。

那么到底应该使用“领域服务”还是“领域事件”呢?先回答自己是否需要引入事件模型。如果“是”,那么请优先考虑使用领域事件。

这是很容易让人头晕的两个对象,下面我将用两句话让您感受他们的使用场景:

A:快递在入库时需要进行规格检查,比如是否超重等
该场景,我们除了引入“快递”这一聚合根之外,没有引入其他领域对象。那么此处的“检查”操作,该行为应该交给谁呢?给“快递”?快递自己检查自己?显然不对,所以当某行为不属于一个实体或者值对象时,我们就需要引入一个领域服务了。

B:当快递被投递到营业点时,证明快递已经到达,配送员将打电话给用户进行派送。
该场景中,我们已经发现了有“快递”、“营业点”、“快递员”等领域对象,如果要完成一个“快递到达”的用例,我们会如何操作呢?调用"营业点"的“收纳进快递”,并且接下来是调用“快递员”的“配送快递”。此处涉及到多个聚合根之间的交互,那么是选用领域服务还是领域事件呢?如果您基于事件建模,可以采用领域事件,反之,您可以使用领域服务。

如果您开始尝试DDD项目,我建议您优先采用事件建模的方式。也就是说,考虑采用领域事件。将聚合根与聚合根之间的交互动作通过领域事件来传达,而将领域对象的策略运算交由领域服务完成。更清晰的划分它俩之间的职责。

实践方案

实践方案主要采用了Jimmy Bogard所提出的领域事件实现方案。聚合根中保持领域事件的集合,通过事件分配器将事件分配给对应的处理事件。

因此我们可以先建立几个接口:IDomainEvent(表明该类为领域事件)、IDomainEventHandler(用于拦截处理领域事件)、IEventDispatcher(事件分配器,将领域事件分发给处理程序)。

复制代码

public interface IDomainEvent
{
}public interface IDomainEventHandler<in TDomainEvent>where TDomainEvent : IDomainEvent
{Task HandleAysnc(TDomainEvent domainEvent, CancellationToken cancellationToken = default);
}public interface IEventDispatcher
{Task DispatchAsync<TDomainEvent>(TDomainEvent domainEvent,CancellationToken cancellationToken = default) where TDomainEvent :IDomainEvent;
}

然后还需要给聚合根添加上一些方法,便于它能够保留领域事件在实例中:

复制代码

 public abstract class AggregateRoot<TKey>
{public virtual TKey Id { get; set; }protected List<IDomainEvent> _domainEvents = new List<IDomainEvent>();public virtual void AddDomainEvent(IDomainEvent domainEvent)=> _domainEvents.Add(domainEvent);public virtual void RemoveDomainEvent(IDomainEvent domainEvent)=> _domainEvents.Remove(domainEvent);public List<IDomainEvent> GetDomainEvents()=> _domainEvents;
}

最后,在仓储进行持久化之前,通过事件分发器将保持在聚合根实例上的领域事件分发给对应的事件处理程序:

复制代码

// EF Core DbContext
public class OrderingContext : DbContext
{public async Task<bool> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)){//Get aggregateRootvar aggregateRoots = dbContext.ChangeTracker.Entries().ToList();// Dispatch Domain Events collection.await _eventDispatcher.DispatchAsync(aggregateRoots,cancellationToken);// After this line runs, all the changes (from the Command Handler and Domain// event handlers) performed through the DbContext will be committedvar result = await base.SaveChangesAsync();}
}

由于篇幅有限,上面的实现方案只是给了大家一个思路,所以缺少了一些实现,如果您有需要可以联系我,我提取一个小Demo上传至Github。

关于另外的实现方案,您可以查看微软Eshop教程。

为什么选取领域事件

为什么我会建议您优先考虑使用领域事件呢?为了后期能够更容易的拆解项目为微服务。假如咱们都是将聚合根之间的交互通过领域服务来完成,比如现在有一个领域服务A,它需要帮助聚合根A和聚合根B完成操作:

复制代码

public class DomainServiceA
{DomainServiceA(IRepositoryA repositoryA,IRepositoryB repositoryB);
}

在该领域服务中,以来了聚合根A、B的存储库。现在A和B位于同一个服务中,这可以很好的运行。但是如果有一天,B需要被独立出去,单独成为一个服务怎么办呢?该领域服务不得不进行更改。

而加入我们通过领域事件来进行流转,当聚合B被拆分出去之后,假如B需要A发布的某个事件,那么B只需要在自己的项目中添加一个该事件的类型就可以了,而不需要修改其他逻辑。(也许需要将内部事件转换为外部事件,但是核心业务代码是不会更改的)。

所以构建项目初期,我们在选型时要进行长远的考虑。

总结

本次我们介绍了领域驱动设计中的领域事件。“如果捕获领域事件?”,“DDD是否一定需要领域事件?”相信这些问题,看到这里您心里已经有了自己的答案。

领域事件能够帮助我们更好的描述领域中各个对象之间的状态,就如同本文刚开始所提及到的观点:“如果发现有一项动作发生了之后,与之关联的其他领域对象将会受到波及。” 将这些提取建模为领域事件,将对您的项目带来很好的收益。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/311305.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

滑动窗口最大值-leetcode 239题

给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回滑动窗口中的最大值。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;htt…

hdu1276 士兵队列训练问题-list容器

Problem Description 某部队进行新兵队列训练&#xff0c;将新兵从一开始按顺序依次编号&#xff0c;并排成一行横队&#xff0c;训练的规则如下&#xff1a;从头开始一至二报数&#xff0c;凡报到二的出列&#xff0c;剩下的向小序号方向靠拢&#xff0c;再从头开始进行一至三…

diff git 指定时间,git diff日期?

Im accustomed to running a git comparison that will allow comparison with local git revs like:git diff HEAD HEAD~110 -- some/file/path/file.extIs it possible to use the date instead? And if so, how? I would like to be able insert in place of the "11…

一文读懂 Copyleft 开源许可证

开源组件已改变了我们开发软件的方式。来自开源社区的现成库&#xff08;ready-made libraries&#xff09;使忙碌的开发者们能专注于他们的秘密武器&#xff0c;这些秘密武器或将成为未来令人兴奋的新软件产品。而且不需要付费。下载开源组件不需要你提供信用卡号码&#xff0…

常用决策树集成模型Random Forest、Adaboost、GBDT详解

常用的集成学习策略 在之前的文章我有介绍过常用的基本决策树模型ID3、C4.5、CART算法&#xff0c;其中提到了一个关于基本决策树模型的缺点&#xff0c;那就是决策树模型学习一棵最优的决策树被认为是NP-Complete问题。实际中的决策树是基于启发式的贪心算法建立的&#xff0…

hdu1213 How Many Tables-并查集

Problem Description Today is Ignatius’ birthday. He invites a lot of friends. Now it’s dinner time. Ignatius wants to know how many tables he needs at least. You have to notice that not all the friends know each other, and all the friends do not want to …

redis spring 切面缓存_今日份学习: Spring中使用AOP并实现redis缓存?

笔记在Spring中如何使用AOP?Spring是如何切换JDK动态代理和CGLIB的&#xff1f;spring.aop.proxy-target-classtrue (在下方第二个链接中&#xff0c;原生doc中提到过)Aspect生命切面BeforeAfterAroundRedis广泛使用的内存缓存常见的数据结构:StringListSetHashZSetRedis为什么…

开源网站云查杀方案,搭建自己的云杀毒。

最近公司的一个客户被勒索病毒攻击了&#xff0c;可悲的是&#xff0c;客户的文件附件太多而且大&#xff0c;没有做双机热备的功能。当客户发现病毒后&#xff0c;还第一时间格式化了服务器。那叫一个惨&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;初步分析…

下一个更大元素 leetcode-496

给你两个 没有重复元素 的数组 nums1 和 nums2 &#xff0c;其中nums1 是 nums2 的子集。 请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。 nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在&#xff0c;对应位…

最简单的并查集模板

代码如下&#xff1a; #include <iostream> using namespace std; const int N 1010; int a[N];void init_set() { //初始化for (int i 1; i < N; i)a[i] i; }int find_set(int x) { //查找return x a[x] ? x : find_set(a[x]); }void union_set(int x, int y) …

linux关机方法有哪些?有何区别_Linux下判断是否正常关机的一个简单方法

思想&#xff1a;系统启动的时候生成一个文件&#xff0c;正常关机的时候则删除这个文件&#xff0c;则可以根据系统开机时有没有那个文件来判断是不是非正常关机。步骤&#xff1a;在/etc/rc.d/init.d/下编程脚本。判断是否正常关机&#xff0c;和生成文件的脚本&#xff1a;t…

二叉树的遍历—广度优先(BFS)和深度优先(DFS)python实现

二叉树 二叉树&#xff08;Binary tree&#xff09;是树形结构的一个重要类型。对于二叉树的基础知识这里不做过多介绍&#xff0c;下面我们直接介绍二叉树的遍历方式和如何用python代码去实现二叉树的遍历。 二叉树的遍历&#xff08;重点&#xff09; “前”、“中”、“后…

五分钟了解数据库事务隔离

前言什么是事务隔离呢&#xff1f;们知道&#xff0c;关系型数据基本都支持事务&#xff0c;事务具备四个特性&#xff0c;分别是&#xff1a;原子性&#xff08;Atomicity&#xff09;、一致性&#xff08;Consistency&#xff09;、隔离性&#xff08;Isolation&#xff09;、…

fusionsphere读音_hydroxyapatite

全部Conclusion The absorbable coral hydroxyapatite is a good bone tissue engineering scaffold.结论可吸收性珊瑚羟基磷灰石具有促进骨髓基质细胞表达成骨细胞表型、合成细胞外基质的功能,是良好的细胞载体.互联网Phase changes and structural stability of hydroxyapati…

基于 Redis 实现 CAS 操作

基于 Redis 实现 CAS 操作Intro在 .NET 里并发情况下我们可以使用 Interlocked.CompareExchange 来实现 CAS &#xff08;Compare And Swap&#xff09; 操作&#xff0c;在分布式的情景下很多时候我们都会使用 Redis &#xff0c;最近在改之前做的一个微信小游戏项目&#xff…

数据结构-堆(heap)最大堆、最小堆的相关操作和实战

堆&#xff08;heap&#xff09; 堆的概念&#xff1a; 是完全二叉树&#xff1b;每个节点 > 或 < 孩子节点。 条件二中分别对应&#xff1a;最大堆和最小堆。 最大堆&#xff1a;最大值为堆顶元素&#xff0c;每个节点 > 孩子节点。 最小堆&#xff1a;最小值为堆…

无法载入增效工具_山东省 智能工具箱 智能工具管理 工具管理企业数字化管理...

我们日常工具管理中难免会遇到东西找不到&#xff0c;工具丢失无法落实到人&#xff0c;工具买回来没有及时维护导致生锈等&#xff0c;工具生命周期不细致无法及时送检&#xff0c;导致设备参数不达标等一些细微问题&#xff0c;在工具管理上可能是小问题&#xff0c;但是设备…

Asp.Net Core 中间件应用实战中你不知道的那些事

一、概述这篇文章主要分享Endpoint 终结点路由的中间件的应用场景及实践案例&#xff0c;不讲述其工作原理&#xff0c;如果需要了解工作原理的同学&#xff0c; 可以点击查看以下两篇解读文章&#xff1a;•Asp.Net Core EndPoint 终结点路由工作原理解读•ASP.NET CORE 管道模…

递归算法(一)递归概念与思路

递归的概念 程序调用自身的编程技巧称为递归&#xff08; recursion&#xff09;。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法&#xff0c;它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的…

stm32f103振镜_二维振镜激光扫描教学演示装置及其实现方法

二维振镜激光扫描教学演示装置及其实现方法【专利摘要】本发明公开二维振镜激光扫描教学演示装置及其实现方法&#xff0c;该装置包括二维振镜扫描模块、图像轨迹记录仪和上位机人机界面&#xff1b;上位机人机界面主要用于教学演示图形或文字的绘制以及串口通信的设置&#xf…