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

开篇

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

最后,偷偷说一句:创作不易,点个推荐吧.....

原文地址:https://www.cnblogs.com/uoyo/p/12421553.html

时间: 2024-08-25 19:28:54

如何运用领域驱动设计 - 领域事件的相关文章

如何运用领域驱动设计 - 领域服务

原文:如何运用领域驱动设计 - 领域服务 目录 概述 什么是领域服务 从实际场景下手 更贴近现实 领域服务VS应用服务 扩展上面的需求 最常见的认证授权是领域服务吗 使用领域服务 不要过多的使用领域服务 不要将过多的行为都给了领域服务 总结 小彩蛋 概述 本文将介绍领域驱动设计(DDD)战术模式中另一个非常重要的概念 - 领域服务.在前面两篇博文中,我们已经学习到了什么是值对象和实体,并且能够比较清晰的定位它们自身的行为.但是在某些时候,你会发现某一些业务行为好像不容易落到单个实体或者值对象身上

六 领域驱动设计-领域对象的生命周期

目录 领域驱动设计-领域对象的生命周期 AGGREGATE FACTORY 领域驱动设计-领域对象的生命周期 每个对象都有生命周期,如图6-1所示.对象自创建后,可能会经历各种不同的状态,直至最终消亡--要么存档,要么删除.当然,很多对象是简单的临时对象,仅通过调用构造函数来创建,用来做一些计算,而后由垃圾收集器回收.这类对象没必要搞得那么复杂.但有些对象具有更长的生命周期,其中一部分时间不是在活动内存中度过的.它们与其他对象具有复杂的相互依赖性.它们会经历一些状态变化,在变化时要遵守一些固定规

学习:DDD领域驱动设计

DDD:Domain-driven Design(领域 - 驱动 -> 设计) ->领域驱动领域模型设计 ->领域模型驱动代码实现 摘自网络(汤雪华的博客) <概念总结> 领域就是问题域,有边界,领域中有很多问题: 任何一个系统要解决的那个大问题都对应一个领域: 通过建立领域模型来解决领域中的核心问题,模型驱动的思想: 领域建模的目标针对我们在领域中所关心的问题,即只针对核心关注点,而不是整个领域中的所有问题: 领域模型在设计时应考虑一定的抽象性.通用性,以及复用价值: 通过

领域驱动设计和Spring

原文 http://static.olivergierke.de/lectures/ddd-and-spring/ 1.介绍这篇文章是的介绍一下领域驱动设计的基础构件.概念和Java的web应用(主要是基于Spring框架)之间的关系和区别.这篇文章的第二部分讲了怎么把实体.聚合根.仓储映射到使用Spring框架的Java应用中2.领域驱动设计Eric Evans的<领域驱动设计>无疑是软件设计领域最重要的几本书之一.这本书主要集中在软件开发中如何处理领域和软件的映射关系— 开始强调领域通用语

领域驱动设计 ——一种将概念模型化的方式

原文发布于:http://www.gufeng.tech/ 1.引子 2004年Eric Evans 发表了一本书:<Domain-Driven Design: Tackling Complexity in the Heart of Software>(中文名:<领域驱动设计:软件核心复杂性应对之道>),在这本书中作者提出了领域驱动设计(DDD)的概念,到现在已经10多年的时间了. 1.1 面向对象与面向对象语言 面向对象思想已经存在相当长的历史了(相对于软件的历史),我而们使用的

领域驱动设计的必要性和模型标准——《领域驱动设计-精简版》

一.领域驱动设计 领域驱动设计早在30年前就已经为人所知,一些设计人员开始开始领域建模,领域通用语言的思维构造,以便能够在领域专家和开发专家形成高效的沟通,Eric Evans将这种思维(思潮)定义为Domain-Driven Desigin(领域驱动设计,简称DDD).DDD在B/S还不这么流行的年代,主要应用在软件公司,因为很多都是C端,但是现在各个互联网公司将很多业务尝试模块化.量级上规模化,业务上多样化,不再是CRUD这么简单,因此,领域驱动设计在互联网开发中也起到了一个很好的引领作用.

领域驱动设计学习笔记(一 事件总线)

何为领域驱动设计? 2004年著名建模专家Eric Evans发表了他最具影响力的书籍:<Domain-Driven Design: Tackling Complexity in the Heart of Software>(中文译名:领域驱动设计:软件核心复杂性应对之道),书中提出了领域驱动设计(简称 DDD)的概念. 领域驱动设计事实上是针对OOAD的一个扩展和延伸,DDD基于面向对象分析与设计技术,对技术架构进行了分层规划,同时对每个类进行了策略和类型的划分. 领域模型是领域驱动的核心.

DDD领域驱动设计基本理论知识总结

领域驱动设计之领域模型 加一个导航,关于如何设计聚合的详细思考,见这篇文章. 2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity in the Heart of Software (领域驱动设计),简称Evans DDD.领域驱动设计分为两个阶段: 以一种领域专家.设计人员.开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现领域概念,然后将这些概念设计成一个领域模型:由领域模型驱动软件设计,用代码来实现该领域模型:

(转载)浅谈我对DDD领域驱动设计的理解

原文地址:http://www.cnblogs.com/netfocus/p/5548025.html 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品.所以,自然而然就想到要做一个普通电商系统,用于实现在线销售自己企业产品的目的. 再比如,我是一家互联网公司,公司有很多系统对外提供服务,面向很多客户端设备.但是最近由于各种原因,导致服务经常出故