距离发布上一篇该系列的文章好像已通过了快一个半月了,好吧,我托更了😭。一晃就已经到了3月份,在这樱花🌸怒放的季节,终于得从新连载该系列了。在停更的期间时不时会收到你们关于DDD的留言和问题,一旦我有时间必定会回复你们的问题。在此,衷心感谢你们对本系列文章的支持😄。html
在实践领域驱动设计(DDD)的过程当中,咱们每每会遇到多个领域对象相互交互的状况。好比聚合根A在执行某操做以前须要获得聚合根B的某个信号(或某些数据)。若是在单体应用程序中,咱们有条件和机会使得二者进行强引用来完成操做,可是这将直接打破领域驱动设计的规范,从而使得项目不可控,再次回到大泥球的开发。git
如今,我们能够选取一种更纯净的方式来解决这类问题,而且还可以更清晰的描述领域对象的活动迹象。这就是我们今天的主题 ———— “领域事件”。那么到底什么是领域事件呢?引入领域事件会为咱们已有的DDD项目带来哪些益处?是否必定要使用领域事件呢? 本文将从不一样的角度来带你们从新认识一下“领域事件”这个概念,而且给出相应的代码片断(本教程的代码片断都使用的是C#,固然思想是跨越任何编程语言的😀)。github
在原著 《领域驱动设计:软件核心复杂性应对之道》 其实并无直接说起到关于领域事件的介绍。领域对象是在后期才被做者Evans提出,通过Udi Dahan(Nservicebus做者)和Jimmy Bogard(MetdiaR、AutoMapper做者)等专家后期的不断实践和演变才有了今天的领域事件版本。编程
此处我摘录了《实现领域驱动设计》书中对领域事件的描述:设计模式
领域专家所关心的发生在领域中的一些事件。
将领域中所发生的活动建模成一系列的离散事件。每一个事件都用领域对象来表示,领域事件是领域模型的组成部分,表示领域中所发生的事情。架构
当您一看到“事件”这个词语的时候,您可能会一下联系到 C# 中的事件,那个基于委托的事件。 确实,它们之间有着共性,就好比:“当事件发生的时候,与该事件相关联的对象都将受到波及。” 因此,若是您了解C#中的事件,那将帮助您更好的理解“领域事件”。app
由此咱们能够推导出:在领域驱动设计建模过程当中,若是发现有一项动做发生了以后,与之关联的其余领域对象将会受到波及。 那么该动做可能就是“领域事件”。框架
光从概念上来说些许有些让人头晕,咱们来看看实际的一个例子:“当用户将商品添加到购物车的时候,下方的推荐商品将为他推荐同类型的商品”。 这是一个有先后发生关系的典型案例,商品被添加到了购物车就会引起推荐同类商品。 因此我们仔细来感觉一下这一个过程,抓一抓里面的关键词。“商品加入购物车” 就会致使 “推荐同类商品”。是否是和我们上面那一段的描述有些相似了? 因此仔细观察以后,咱们能够捕获出一个领域对象来,该对象您可能将它命名为(ProductAddedEvent)。dom
为何咱们要将它命名为过去时呢? 这也是印证了开头那句话“动做发生了以后”。当该事件被捕获了以后,就会将事件信息传递给“推荐商品”聚合根,执行相应处理逻辑。async
那么事件的来源是哪里呢?“用户点击”,“网页响应” 这些都不是哦! 记住,咱们要深入关心领域对象,刚才所说的状况显然与我们的领域对象一点儿关系也没有。因此咱们能够很天然的将目光转向到“购物车”,“购物车”可能就是一个聚合根,它会有一个叫作“添加商品”的行为,当该行为完成以后就会引起一个“商品添加完成”的事件。
通过整理以后咱们可能会获得一个这样的流程:
因此您会发现,领域事件一方面充当了描述领域信息的做用,一方面承接了不一样聚合根之间的交互。 固然事件不必定只有一个,被影响的领域对象也不必定只有一个。就比如“推荐商品”受到了“商品添加完成”事件以后,它本身也能产生一个另外的领域事件传递给下游。
到这里您或许会感到使用领域事件和以往我们捕获其余对象不太同样,好比捕获值对象、实体等。由于对于领域事件来讲,它多是“隐式”,咱们没有直观的感觉它的存在。
因此,请仔细的考虑这一点:当您要使用领域事件时,您将认同您的项目须要以事件做为中心。 而项目中的各个领域对象都将以产生、发布领域事件完成一系列的交互流程。
这里我摘录了《领域驱动设计模式、原理与实践》中的一段话分享给你们:“领域事件将会在领域专家一块儿进行的知识提炼环节中揭示出来。揭示领域事件是如此有价值,DDD实践者都拥有创新的知识提炼技术来进行实践以便让其更专一于事件,好比事件风暴。不过,使用这些创新技术会带来新的挑战。既然概念化的模型都是以事件为中心的,那么代码也须要以事件为中心,以便它可以表述概念化模型。这就是领域事件设计模式所带来的价值。”
因此在大多数时候您将感觉到项目逐渐具备 EDA(事件驱动架构)的风格。而此时,您可能会联想到DDD中的另一种模式:事件溯源(EventSource),认为本身必需要采用事件溯源来创建您的ddd项目。其实这并非必定的,采用领域事件和使用事件溯源是没有直接关系的,虽然领域事件会帮助事件溯源完成的更好。
结合上面的介绍,您可能已经对发现领域事件有一点感受了。当聚合与聚合之间具备交互关系时,咱们每每会发现他们之间会存在某个领域事件来引起这系列行为。
若是与领域专家交谈时,发现了这样的关键词汇: “当………………”、“若是A完成以后,那么…………”,“发生…………的时候”。 这些词汇可能在隐式的告诉您,该处也许存在着“领域事件”对象。
在使用领域事件以前,咱们必需要知道事件其实被划分红了:“内部”和“外部”。 就正如它的描述同样,内部的领域事件发生在边界以内,而外部的事件发生在边界以外(好比微服务A产生了一个事件,而微服务B会受到该事件的影响)。
在Microsoft关于ESHOP案例的指导书籍《.NET 微服务 - 体系结构》 中,将其命名为“领域事件和集成事件”:
该图也形象的说明了基于一个边界内的内部事件是如何交互的:
外部的事件每每须要一些基础结构来实现远程服务之间的进程间和分布式通讯,好比rabbitMQ,kafka等。本篇文章重点讲解内容为内部的领域事件,关于外部的事件将会在后期《分布式中的领域驱动设计》系列中为你们介绍。
那么是否个人DDD项目就必须使用“领域事件”呢? 也许您在网上历来没有见到过这样的问题,所以也没有该问题的确切性答案。关于该问题,我我的以为答案是“不必定”。
就像上文说的同样,若是您开始使用领域事件,那么就证实您的项目和思惟将转换为“以事件做为中心”。领域中大部分的交互都将以事件的方式来呈现。因此与其考虑“个人DDD项目就必须使用“领域事件””这个问题,还不如转换为:“我是否须要用事件做为中心来考虑问题?”。
因此,该问题的答案就取决于您本身了。这也是为何您会在某些DDD框架或者DDD项目中没有发现“领域事件”的缘由之一。
那么,若是不使用事件来建模,聚合与聚合之间是如何进行交互的呢? 请看下文↓。
我利用搜索引擎进行了大量的查找,没有发现任何关于“领域事件” 和 “领域服务”之间的对比内容。可是我认为这二者却有着不少类似的地方。 当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 aggregateRoot var 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 committed var 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是否必定须要领域事件?”相信这些问题,看到这里您内心已经有了本身的答案。
领域事件可以帮助咱们更好的描述领域中各个对象之间的状态,就如同本文刚开始所说起到的观点:“若是发现有一项动做发生了以后,与之关联的其余领域对象将会受到波及。” 将这些提取建模为领域事件,将对您的项目带来很好的收益。
感受每次讲这个系列就比较严肃,若是您更喜欢轻松一些的内容能够关注个人另一个系列《五分钟的.NET》。
最后,偷偷说一句:创做不易,点个推荐吧.....