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

开篇

距离发布上一篇该系列的文章好像已经过了快一个半月了,好吧,我托更了????。一晃就已经到了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…

一文读懂 Copyleft 开源许可证

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

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

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

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

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

下一个更大元素 leetcode-496

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

二叉树的遍历—广度优先(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;、…

数据结构-堆(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;它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的…

洛谷 P1706 P1036 -小试牛刀

题目1&#xff1a; 题目描述 输出自然数 1 到 n 所有不重复的排列,即 n 的全排列&#xff0c;要求所产生的任一数字序列中不允许出现重复的数字。 输入格式 一个整数 n。 输出格式 由 1∼n1∼n1∼n 组成的所有不重复的数字序列&#xff0c;每行一个序列。 每个数字保留 5 个…

理解ASP.NET Core中的中间件

中间件是ASP.NET Core的一个重要特点&#xff0c;ASP.NET Core应用程序之所以能够灵活地处理各种各样的请求&#xff0c;完成都是由于中间件&#xff0c;那么它究竟是怎么一回事呢&#xff1f;一、理解中间件ASP.NET Core的一个主要特点是中间件&#xff08;Middleware&#xf…

XGBoost-原理推导(上)

XGBoost简介 XGBoost&#xff08;eXtreme Gradient Boosting&#xff09;是华盛顿大学博士陈天奇创造的一个梯度提升&#xff08;Gradient Boosting&#xff09;的开源框架。至今可以算是各种数据比赛中的大杀器&#xff0c;被大家广泛地运用。 之前的文章我已经介绍了GBDT&a…

redis深度历险_Redis的数据结构(内存具体怎么优化的)

上一篇我们讲解了Redis中SDS的组成以及优势&#xff0c;这一篇我们讨论下Redis中的Hash数据类型是怎么构成的呢&#xff1f;Java中存在HashMap和HashTable的数据类型。而Hash的数据结构可以近似于HashTable&#xff0c;依据数组链表的形式构成。在Redis中&#xff0c;Hash在元素…

.NET Core开发实战(第19课:日志作用域:解决不同请求之间的日志干扰)--学习笔记...

19 | 日志作用域&#xff1a;解决不同请求之间的日志干扰开始之前先看一下上一节的代码// 配置的框架 var configBuilder new ConfigurationBuilder(); configBuilder.AddCommandLine(args); configBuilder.AddJsonFile("appsettings.json", optional: false, reloa…

递归算法(二)-分治法

分治法 分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题&#xff0c;这些子问题相互独立且与原问题性质相同。求出子问题的解&#xff0c;就可得到原问题的解。即一种分目标完成程序算法&#xff0c;简单问题可用二分法完成。 分治法解题的一般步骤&#…

自适应滤波器在matlab仿真的程序_电气信息类专业课程之matlab系统仿真 第五章 BPSK通信系统(3)...

继续讲解&#xff01;上一篇文章提出了那么多问题&#xff0c;不知道大家是否能回答上来啊&#xff1f;如果回答不了&#xff0c;有没有去问问度娘呢&#xff1f;程序写完了&#xff0c;回想了一下辅导2017届学生毕业设计的过程&#xff0c;那是真累。我要求他们在现有程序基础…

阿捷外传之Git代码统计:DotNetCore + PowerBI 实现Git仓库日志分析

前言2020年3月初春&#xff0c;虽然春节已经过去一个多月&#xff0c;大街上还未恢复往年的热闹。由于春节前夕突然降临的冠状病毒&#xff0c;导致很多员工无法回到城市复工。春节之后&#xff0c;阿捷所在的公司考虑到复工带来的风险&#xff0c;通知所有员工以远程的方式在家…

递归算法(三)- 回溯法Backtracking

回溯法 回溯法Backtracking&#xff08;找所有的可能&#xff09;递归&#xff1a; 类似枚举&#xff0c;一层一层向下递归&#xff0c;尝试搜索答案。找到答案&#xff1a; > 返回答案&#xff0c;并尝试别的可能未找到答案&#xff1a; > 返回上一层递归&#xff0c;…