日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發(fā)布時間:2023/12/4 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如何运用领域驱动设计 - 领域事件 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

開篇

距離發(fā)布上一篇該系列的文章好像已經(jīng)過了快一個半月了,好吧,我托更了????。一晃就已經(jīng)到了3月份,在這櫻花????盛開的季節(jié),終于得重新連載該系列了。在停更的期間時不時會收到大家關(guān)于DDD的留言和問題,一旦我有時間一定會回復(fù)大家的問題。在此,衷心感謝大家對本系列文章的支持????。

概述

在實踐領(lǐng)域驅(qū)動設(shè)計(DDD)的過程中,我們往往會遇到多個領(lǐng)域?qū)ο笙嗷ソ换サ那闆r。比如聚合根A在執(zhí)行某操作之前需要得到聚合根B的某個信號(或某些數(shù)據(jù))。如果在單體應(yīng)用程序中,我們有條件和機(jī)會使得兩者進(jìn)行強(qiáng)引用來完成操作,但是這將直接打破領(lǐng)域驅(qū)動設(shè)計的規(guī)范,從而使得項目不可控,再次回到大泥球的開發(fā)。

現(xiàn)在,咱們可以選取一種更純凈的方式來解決這類問題,并且還能夠更清晰的描述領(lǐng)域?qū)ο蟮幕顒盂E象。這就是咱們今天的主題 ————?“領(lǐng)域事件”。那么到底什么是領(lǐng)域事件呢?引入領(lǐng)域事件會為我們已有的DDD項目帶來哪些益處?是否一定要使用領(lǐng)域事件呢?本文將從不同的角度來帶大家重新認(rèn)識一下“領(lǐng)域事件”這個概念,并且給出相應(yīng)的代碼片段(本教程的代碼片段都使用的是C#,當(dāng)然思想是跨越任何編程語言的????)。

什么是領(lǐng)域事件

在原著?《領(lǐng)域驅(qū)動設(shè)計:軟件核心復(fù)雜性應(yīng)對之道》?其實并沒有直接提及到關(guān)于領(lǐng)域事件的介紹。領(lǐng)域?qū)ο笫窃诤笃诓疟蛔髡?strong>Evans提出,經(jīng)過Udi Dahan(Nservicebus作者)和Jimmy Bogard(MetdiaR、AutoMapper作者)等專家后期的不斷實踐和演變才有了今天的領(lǐng)域事件版本。

此處我摘錄了《實現(xiàn)領(lǐng)域驅(qū)動設(shè)計》書中對領(lǐng)域事件的描述:

領(lǐng)域?qū)<宜P(guān)心的發(fā)生在領(lǐng)域中的一些事件。
將領(lǐng)域中所發(fā)生的活動建模成一系列的離散事件。每個事件都用領(lǐng)域?qū)ο髞肀硎?#xff0c;領(lǐng)域事件是領(lǐng)域模型的組成部分,表示領(lǐng)域中所發(fā)生的事情。

如何使用領(lǐng)域事件

當(dāng)您一看到“事件”這個詞語的時候,您可能會一下聯(lián)系到 C# 中的事件,那個基于委托的事件。確實,它們之間有著共性,就比如:“當(dāng)事件發(fā)生的時候,與該事件相關(guān)聯(lián)的對象都將受到波及。” 所以,如果您了解C#中的事件,那將幫助您更好的理解“領(lǐng)域事件”。

由此我們可以推導(dǎo)出:在領(lǐng)域驅(qū)動設(shè)計建模過程中,如果發(fā)現(xiàn)有一項動作發(fā)生了之后,與之關(guān)聯(lián)的其他領(lǐng)域?qū)ο髮艿讲啊?/strong>?那么該動作可能就是“領(lǐng)域事件”。

光從概念上來講些許有些讓人頭暈,我們來看看實際的一個例子:“當(dāng)用戶將商品添加到購物車的時候,下方的推薦商品將為他推薦同類型的商品”。這是一個有前后發(fā)生關(guān)系的典型案例,商品被添加到了購物車就會引發(fā)推薦同類商品。所以咱們仔細(xì)來感受一下這一個過程,抓一抓里面的關(guān)鍵詞。“商品加入購物車” 就會導(dǎo)致 “推薦同類商品”。是不是和咱們上面那一段的描述有些類似了?所以仔細(xì)觀察之后,我們可以捕獲出一個領(lǐng)域?qū)ο髞?#xff0c;該對象您可能將它命名為(ProductAddedEvent)。

為什么我們要將它命名為過去時呢?這也是印證了開頭那句話“動作發(fā)生了之后”。當(dāng)該事件被捕獲了之后,就會將事件信息傳遞給“推薦商品”聚合根,執(zhí)行相應(yīng)處理邏輯。

那么事件的來源是哪里呢?“用戶點擊”,“網(wǎng)頁響應(yīng)” 這些都不是哦!記住,我們要深刻關(guān)心領(lǐng)域?qū)ο?#xff0c;剛才所說的情況顯然與咱們的領(lǐng)域?qū)ο笠稽c兒關(guān)系也沒有。所以我們可以很自然的將目光轉(zhuǎn)向到“購物車”,“購物車”可能就是一個聚合根,它會有一個叫做“添加商品”的行為,當(dāng)該行為完成之后就會引發(fā)一個“商品添加完成”的事件。

經(jīng)過整理之后我們可能會得到一個這樣的流程:

所以您會發(fā)現(xiàn),領(lǐng)域事件一方面充當(dāng)了描述領(lǐng)域信息的作用,一方面承接了不同聚合根之間的交互。當(dāng)然事件不一定只有一個,被影響的領(lǐng)域?qū)ο笠膊灰欢ㄖ挥幸粋€。就好比“推薦商品”受到了“商品添加完成”事件之后,它自己也能產(chǎn)生一個另外的領(lǐng)域事件傳遞給下游。

思維的轉(zhuǎn)換

到這里您或許會感到使用領(lǐng)域事件和以往咱們捕獲其他對象不太一樣,比如捕獲值對象、實體等。因為對于領(lǐng)域事件來說,它可能是“隱式”,我們沒有直觀的感受它的存在。

所以,請仔細(xì)的考慮這一點:當(dāng)您要使用領(lǐng)域事件時,您將認(rèn)同您的項目需要以事件作為中心。而項目中的各個領(lǐng)域?qū)ο蠖紝⒁援a(chǎn)生、發(fā)布領(lǐng)域事件完成一系列的交互流程。

這里我摘錄了《領(lǐng)域驅(qū)動設(shè)計模式、原理與實踐》中的一段話分享給大家:“領(lǐng)域事件將會在領(lǐng)域?qū)<乙黄疬M(jìn)行的知識提煉環(huán)節(jié)中揭示出來。揭示領(lǐng)域事件是如此有價值,DDD實踐者都擁有創(chuàng)新的知識提煉技術(shù)來進(jìn)行實踐以便讓其更專注于事件,比如事件風(fēng)暴。不過,使用這些創(chuàng)新技術(shù)會帶來新的挑戰(zhàn)。既然概念化的模型都是以事件為中心的,那么代碼也需要以事件為中心,以便它能夠表述概念化模型。這就是領(lǐng)域事件設(shè)計模式所帶來的價值。”

所以在大多數(shù)時候您將感受到項目逐漸具有 EDA(事件驅(qū)動架構(gòu))的風(fēng)格。而此時,您可能會聯(lián)想到DDD中的另外一種模式:事件溯源(EventSource),認(rèn)為自己必須要采用事件溯源來建立您的ddd項目。其實這并不是一定的,采用領(lǐng)域事件和使用事件溯源是沒有直接關(guān)系的,雖然領(lǐng)域事件會幫助事件溯源完成的更好。

捕獲領(lǐng)域事件

結(jié)合上面的介紹,您可能已經(jīng)對發(fā)現(xiàn)領(lǐng)域事件有一點感覺了。當(dāng)聚合與聚合之間具有交互關(guān)系時,我們往往會發(fā)現(xiàn)他們之間會存在某個領(lǐng)域事件來引發(fā)這系列行為。

如果與領(lǐng)域?qū)<医徽剷r,發(fā)現(xiàn)了這樣的關(guān)鍵詞匯:“當(dāng)………………”、“如果A完成之后,那么…………”,“發(fā)生…………的時候”。這些詞匯可能在隱式的告訴您,該處也許存在著“領(lǐng)域事件”對象。

內(nèi)部事件 and 外部事件

在使用領(lǐng)域事件之前,我們必須要知道事件其實被劃分成了:“內(nèi)部”和“外部”。就正如它的描述一樣,內(nèi)部的領(lǐng)域事件發(fā)生在邊界之內(nèi),而外部的事件發(fā)生在邊界之外(比如微服務(wù)A產(chǎn)生了一個事件,而微服務(wù)B會受到該事件的影響)。

在Microsoft關(guān)于ESHOP案例的指導(dǎo)書籍《.NET 微服務(wù) - 體系結(jié)構(gòu)》?中,將其命名為“領(lǐng)域事件和集成事件”:

該圖也形象的說明了基于一個邊界內(nèi)的內(nèi)部事件是如何交互的:

外部的事件往往需要一些基礎(chǔ)結(jié)構(gòu)來實現(xiàn)遠(yuǎn)程服務(wù)之間的進(jìn)程間和分布式通信,比如rabbitMQ,kafka等。本篇文章重點講解內(nèi)容為內(nèi)部的領(lǐng)域事件,關(guān)于外部的事件將會在后期《分布式中的領(lǐng)域驅(qū)動設(shè)計》系列中為大家介紹。

可選 Or 必須

那么是否我的DDD項目就必須使用“領(lǐng)域事件”呢?也許您在網(wǎng)上從來沒有見到過這樣的問題,因此也沒有該問題的確切性答案。關(guān)于該問題,我個人覺得答案是“不一定”。

就像上文說的一樣,如果您開始使用領(lǐng)域事件,那么就證明您的項目和思維將轉(zhuǎn)換為“以事件作為中心”。領(lǐng)域中大部分的交互都將以事件的方式來呈現(xiàn)。所以與其考慮“我的DDD項目就必須使用“領(lǐng)域事件””這個問題,還不如轉(zhuǎn)換為:“我是否需要用事件作為中心來考慮問題?”。

所以,該問題的答案就取決于您自己了。這也是為什么您會在某些DDD框架或者DDD項目中沒有發(fā)現(xiàn)“領(lǐng)域事件”的原因之一。

那么,如果不使用事件來建模,聚合與聚合之間是如何進(jìn)行交互的呢?請看下文↓。

領(lǐng)域事件 VS 領(lǐng)域服務(wù)

我利用搜索引擎進(jìn)行了大量的查找,沒有發(fā)現(xiàn)任何關(guān)于“領(lǐng)域事件” 和 “領(lǐng)域服務(wù)”之間的對比內(nèi)容。但是我認(rèn)為這兩者卻有著很多相似的地方。當(dāng)Evans在初次提出領(lǐng)域驅(qū)動的概念時,是沒有考慮領(lǐng)域事件的,那么也就意味著我們能夠通過原有的領(lǐng)域?qū)ο笸瓿深I(lǐng)域建模和業(yè)務(wù)流程。

回到剛才那個問題,聚合與聚合之間只能通過事件完成操作嗎?不一定。“領(lǐng)域服務(wù)”也承擔(dān)著領(lǐng)域?qū)ο笈c領(lǐng)域?qū)ο筠D(zhuǎn)換的功能。

先回顧一下咱們在領(lǐng)域服務(wù)章節(jié)了解到的部分內(nèi)容:

當(dāng)我們發(fā)現(xiàn)一個操作無法賦予一個實體或者值對象,且該操作又對業(yè)務(wù)流程很重要時,我們往往需要使用領(lǐng)域服務(wù)
通過A和B,得到一個C。
A需要一個繁瑣的內(nèi)部策略才能得到一個結(jié)果B。(ps: A,B,C指的是領(lǐng)域?qū)ο笾械闹祵ο蠡蛘邔嶓w)

所以這也意味著,領(lǐng)域服務(wù)內(nèi)部可以對多個領(lǐng)域?qū)ο?#xff08;比如聚合根)進(jìn)行操作。所以某些DDD框架將領(lǐng)域服務(wù)作為完成流程操作的主要工具,允許使用者在領(lǐng)域服務(wù)中注入多個倉儲,從而對多個聚合根進(jìn)行操作。

而“領(lǐng)域事件”呢,它通過發(fā)布領(lǐng)域事件來達(dá)到不同領(lǐng)域?qū)ο蟮慕换ァ?/p>

那么到底應(yīng)該使用“領(lǐng)域服務(wù)”還是“領(lǐng)域事件”呢?先回答自己是否需要引入事件模型。如果“是”,那么請優(yōu)先考慮使用領(lǐng)域事件。

這是很容易讓人頭暈的兩個對象,下面我將用兩句話讓您感受他們的使用場景:

A:快遞在入庫時需要進(jìn)行規(guī)格檢查,比如是否超重等
該場景,我們除了引入“快遞”這一聚合根之外,沒有引入其他領(lǐng)域?qū)ο蟆D敲创颂幍摹皺z查”操作,該行為應(yīng)該交給誰呢?給“快遞”?快遞自己檢查自己?顯然不對,所以當(dāng)某行為不屬于一個實體或者值對象時,我們就需要引入一個領(lǐng)域服務(wù)了。

B:當(dāng)快遞被投遞到營業(yè)點時,證明快遞已經(jīng)到達(dá),配送員將打電話給用戶進(jìn)行派送。
該場景中,我們已經(jīng)發(fā)現(xiàn)了有“快遞”、“營業(yè)點”、“快遞員”等領(lǐng)域?qū)ο?#xff0c;如果要完成一個“快遞到達(dá)”的用例,我們會如何操作呢?調(diào)用"營業(yè)點"的“收納進(jìn)快遞”,并且接下來是調(diào)用“快遞員”的“配送快遞”。此處涉及到多個聚合根之間的交互,那么是選用領(lǐng)域服務(wù)還是領(lǐng)域事件呢?如果您基于事件建模,可以采用領(lǐng)域事件,反之,您可以使用領(lǐng)域服務(wù)。

如果您開始嘗試DDD項目,我建議您優(yōu)先采用事件建模的方式。也就是說,考慮采用領(lǐng)域事件。將聚合根與聚合根之間的交互動作通過領(lǐng)域事件來傳達(dá),而將領(lǐng)域?qū)ο蟮牟呗赃\算交由領(lǐng)域服務(wù)完成。更清晰的劃分它倆之間的職責(zé)。

實踐方案

實踐方案主要采用了Jimmy Bogard所提出的領(lǐng)域事件實現(xiàn)方案。聚合根中保持領(lǐng)域事件的集合,通過事件分配器將事件分配給對應(yīng)的處理事件。

因此我們可以先建立幾個接口:IDomainEvent(表明該類為領(lǐng)域事件)、IDomainEventHandler(用于攔截處理領(lǐng)域事件)、IEventDispatcher(事件分配器,將領(lǐng)域事件分發(fā)給處理程序)。

復(fù)制代碼

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; }

然后還需要給聚合根添加上一些方法,便于它能夠保留領(lǐng)域事件在實例中:

復(fù)制代碼

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; }

最后,在倉儲進(jìn)行持久化之前,通過事件分發(fā)器將保持在聚合根實例上的領(lǐng)域事件分發(fā)給對應(yīng)的事件處理程序:

復(fù)制代碼

// 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();} }

由于篇幅有限,上面的實現(xiàn)方案只是給了大家一個思路,所以缺少了一些實現(xiàn),如果您有需要可以聯(lián)系我,我提取一個小Demo上傳至Github。

關(guān)于另外的實現(xiàn)方案,您可以查看微軟Eshop教程。

為什么選取領(lǐng)域事件

為什么我會建議您優(yōu)先考慮使用領(lǐng)域事件呢?為了后期能夠更容易的拆解項目為微服務(wù)。假如咱們都是將聚合根之間的交互通過領(lǐng)域服務(wù)來完成,比如現(xiàn)在有一個領(lǐng)域服務(wù)A,它需要幫助聚合根A和聚合根B完成操作:

復(fù)制代碼

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

在該領(lǐng)域服務(wù)中,以來了聚合根A、B的存儲庫。現(xiàn)在A和B位于同一個服務(wù)中,這可以很好的運行。但是如果有一天,B需要被獨立出去,單獨成為一個服務(wù)怎么辦呢?該領(lǐng)域服務(wù)不得不進(jìn)行更改。

而加入我們通過領(lǐng)域事件來進(jìn)行流轉(zhuǎn),當(dāng)聚合B被拆分出去之后,假如B需要A發(fā)布的某個事件,那么B只需要在自己的項目中添加一個該事件的類型就可以了,而不需要修改其他邏輯。(也許需要將內(nèi)部事件轉(zhuǎn)換為外部事件,但是核心業(yè)務(wù)代碼是不會更改的)。

所以構(gòu)建項目初期,我們在選型時要進(jìn)行長遠(yuǎn)的考慮。

總結(jié)

本次我們介紹了領(lǐng)域驅(qū)動設(shè)計中的領(lǐng)域事件。“如果捕獲領(lǐng)域事件?”,“DDD是否一定需要領(lǐng)域事件?”相信這些問題,看到這里您心里已經(jīng)有了自己的答案。

領(lǐng)域事件能夠幫助我們更好的描述領(lǐng)域中各個對象之間的狀態(tài),就如同本文剛開始所提及到的觀點:“如果發(fā)現(xiàn)有一項動作發(fā)生了之后,與之關(guān)聯(lián)的其他領(lǐng)域?qū)ο髮艿讲啊!?將這些提取建模為領(lǐng)域事件,將對您的項目帶來很好的收益。

總結(jié)

以上是生活随笔為你收集整理的如何运用领域驱动设计 - 领域事件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。