從壹開始微服務 [ DDD ] 之十 ║領域驅動【實戰篇·中】:命令匯流排Bus分發(一)
烽火
哈嘍大家好,老張又見面了,這兩天被各個平臺的“雞湯貼”差點亂了心神,部落格園如此,簡書亦如此,還好群裡小夥伴及時提醒,路還很長,這些小事兒就隨風而去吧,這周本不打算更了,但是被群裡小夥伴“催稿”了,至少也是對我的一個肯定吧,又開始熬夜中,請@初久小夥伴留言,我不知道你的地址,就不放連結了。
收住,言歸正傳,上次咱們說到了領域命令驗證《 ofollow,noindex" target="_blank">九 ║從軍事故事中,明白領域命令驗證(上) 》,也介紹了其中的兩個角色——領域命令模型和命令驗證,這些都是屬於領域層的概念,當然這裡的內容是 命令 ,查詢就當然不需要這個了,查詢的話,直接從倉儲中獲取值就行了,很簡單。也沒人問我問題,那我就權當大家已經對上篇都看懂了,這裡就不再贅述。不知道大家是否還記得上篇文章末尾,提到的幾個問題,我這裡再提一下,就是今天的提綱了,如果你今天看完本篇,這幾個問題能回答上來,那恭喜,你就明白了今天所講的問題:
1、命令模型RegisterStudentCommand 放到 Controller 中真的好麼?//我們平時都是這麼做的 2、如果不放到Controller裡呼叫,我們如果呼叫?在 Service裡麼?//也是一個辦法,至少Controller乾淨了,但是 Service 就重了 3、驗證的結果又如何獲取並在前臺展示呢?//本文會先用一個錯誤的方法來說明問題,下篇會用正確的 4、如何把領域模型 Student 從應用層 StudentAppService 解耦出去( Register()方法中 )。//本文重點,中介者模式
好啦,簡單先寫這四個問題吧,這個時候你可以先不要從 Github 上拉取程式碼,先看著目前手中的程式碼,然後思考這四個問題,如果要是自己,或者咱們以前是怎麼做的,如果你看過以後會有一些新的認識和領悟,請幫忙評論一下,捧個人場嘛,是吧:grinning:。好啦,今天的東西可能有點兒多,請做好大概半個小時的準備,當然這半個小時你需要思考,要是走馬觀花,肯定是收穫沒有那麼多的,程式碼已經更新了,記得看完的時候 pull 一下程式碼。
讀前必讀
1、本文中可能會涉及比較多的依賴注入,請一定要看清楚,因為這是第二個系列了,有時候小細節就不點明瞭,需要大家有一定的基礎,可以看我第一個系列。
2、這三篇核心內容,都是重點在領域層,請一定要多思考。
3、文章不僅有程式碼,更多的是理解,比如用聯合國的栗子來說明中介者模式,請務必要多思考。
零、今天實現左下角 淺紫色 的部分
一、什麼是中介者模式?
1、中介模式的概念
這個其實很好理解,單單從名字上大家也都能理解它是一個什麼模式,因為本文的重點不是一個講解什麼是23種設計模式的,大家有興趣的可以好好的買本書,或者找找資料,好好,主要是思想,不需要自己寫一個專案,如果大家有需要,可以留言,我以後單寫一篇文章,介紹中介者模式。
這裡就摘抄一段定義吧:
中介者模式是一個行為設計模式,它允許我們公開一個統一的介面,系統的 不同部分 可以通過該介面進行 通訊 ,而 不需要 顯示的相互作用;
適用場景:如果一個系統的各個元件之間看起來 有太多的直接關係 (就比如我們系統中那麼多模型物件,下邊會解釋),這個時候則需要一箇中心控制點,以便各個元件可以通過這個中心控制點進行通訊;
該模式促進鬆散耦合的方式是:確保元件的互動是通過這個中心點來進行處理的,而不是通過顯示的引用彼此;
比如系統和各個硬體,系統作為中介者,各個硬體作為同事者,當一個同事的狀態發生改變的時候,不需要告訴其他每個硬體自己發生了變化,只需要告訴中介者系統,系統會通知每個硬體某個硬體發生了改變,其他的硬體會做出相應的變化;
這樣,之前是網狀結構,現在變成了以中介者為中心的星星結構:
是不是挺像一個容器的,他自己把控著整個流程,和每一個物件都有或多或少,或近或遠的聯絡,多個物件之間不用理睬其他物件發生了什麼,只是負責自己的模組就好,然後把訊息發給中介者,讓中介者再分發給其他的具體物件,從而實現通訊 —— 這個思想就是中介者的核心思想,而且也是DDD領域驅動設計的核心思想之一( 還有一個核心思想是領域設計的思想 ),這裡你可能還是不那麼直觀,我剛剛花了一個小時,對咱們的DDD框架中的中介者模式畫了一個圖,相信會有一些新的認識,在下邊第 3 點會看到,請耐心往下看。
2、中介模式的原理
這裡有一個聯合國的栗子,也是常用來介紹和解釋中介者模式的栗子:
抽象中介者(AbstractMediator):定義中介者和各個同事者之間的通訊的介面; //比如下文提到的 抽象聯合國機構
抽象同事者(AbstractColleague):定義同事者和中介者通訊的介面,實現同事的公共功能; //比如下文中的 抽象國家
中介者(ConcreteMediator):需要了解並且維護每個同事物件,實現抽象方法,負責協調和各個具體的同事的互動關係; //比如下文中的 聯合國安理會
同事者(ConcreteColleague):實現自己的業務,並且實現抽象方法,和中介者進行通訊; //比如下文的 美國、英國、伊拉克等國家
注意: 其中同事者是多個同事相互影響的才能叫做同事者;
還是希望大家能好好看看,好好想想,如果你還沒有接觸過這個中介者模式,如果瞭解並使用過,就簡單看一看,要是你能把這個小栗子看懂了,那下邊的內容,就很容易了,甚至是以後的內容就如魚得水了,畢竟DDD領域驅動設計兩個核心就是: CQRS讀寫分離 + 中介者模式 。
這個下邊是一個簡單的Demo,可以簡單的看一看:
namespace 中介者模式 { class Program { static void Main(string[] args) { //例項化 具體中介者 聯合國安理會 UnitedNationsSecurityCouncil UNSC = new UnitedNationsSecurityCouncil(); //例項化一個美國 USA c1 = new USA(UNSC); //例項化一個里拉開 Iraq c2 = new Iraq(UNSC); //將兩個物件賦值給安理會 //具體的中介者必須知道全部的物件 UNSC.Colleague1 = c1; UNSC.Colleague2 = c2; //美國發表宣告,伊拉克接收到 c1.Declare("不準研製核武器,否則要發動戰爭!"); //伊拉克發表宣告,美國收到資訊 c2.Declare("我們沒有核武器,也不怕侵略。"); Console.Read(); } } /// <summary> /// 聯合國機構抽象類 /// 抽象中介者 /// </summary> abstract class UnitedNations { /// <summary> /// 宣告 /// </summary> /// <param name="message">宣告資訊</param> /// <param name="colleague">宣告國家</param> public abstract void Declare(string message, Country colleague); } /// <summary> /// 聯合國安全理事會,它繼承 聯合國機構抽象類 /// 具體中介者 /// </summary> class UnitedNationsSecurityCouncil : UnitedNations { //美國 具體國家類1 private USA colleague1; //伊拉克 具體國家類2 private Iraq colleague2; public USA Colleague1 { set { colleague1 = value; } } public Iraq Colleague2 { set { colleague2 = value; } } //重寫宣告函式 public override void Declare(string message, Country colleague) { //如果美國發布的宣告,則伊拉克獲取訊息 if (colleague == colleague1) { colleague2.GetMessage(message); } else//反之亦然 { colleague1.GetMessage(message); } } } /// <summary> /// 國家抽象類 /// </summary> abstract class Country { //聯合國機構抽象類 protected UnitedNations mediator; public Country(UnitedNations mediator) { this.mediator = mediator; } } /// <summary> /// 美國 具體國家類 /// </summary> class USA : Country { public USA(UnitedNations mediator) : base(mediator) { } //宣告方法,將宣告內容較給抽象中介者 聯合國 public void Declare(string message) { //通過抽象中介者發表宣告 //引數:資訊+類 mediator.Declare(message, this); } //獲得訊息 public void GetMessage(string message) { Console.WriteLine("美國獲得對方資訊:" + message); } } /// <summary> /// 伊拉克 具體國家類 /// </summary> class Iraq : Country { public Iraq(UnitedNations mediator) : base(mediator) { } //宣告方法,將宣告內容較給抽象中介者 聯合國 public void Declare(string message) { //通過抽象中介者發表宣告 //引數:資訊+類 mediator.Declare(message, this); } //獲得訊息 public void GetMessage(string message) { Console.WriteLine("伊拉克獲得對方資訊:" + message); } } }
最終的結果是:
從這個小栗子中,也許你能看出來,美國和伊拉克之間,物件之間並沒有任何的交集和聯絡,但是他們之間卻發生了通訊,各自獨立,但是又相互通訊,這個不就是很好的實現瞭解耦的作用麼!一切都是通過中介者來控制,當然這只是一個小栗子,咱們推而廣之:
命令模式、訊息通知模型、領域模型等,內部執行完成後,將產生的資訊拋向給中介者,然後中介者再根據情況分發給各個成員(如果又需要的),這樣就實現多個物件的解耦,而且也達到同步的作用,當然還有一些輔助知識:非同步、注入、事件等,咱們慢慢學習,至少現在中介者模式的思想和原理你應該都懂了。
3、本專案是如何使用中介者模式的
相信如果你是從我的第一篇文章看下去的,一定會以下幾個模型很熟悉:檢視模型、領域模型、命令模型、驗證(上次說的)、還有沒有說到的通知模型,如果你對這幾個名稱還很朦朧,請現在先在腦子裡仔細想一想,不然下邊的可能會亂,如果你一看到名字就能理解都是幹什麼的,都是什麼作用,那好,請看下邊的關係圖。
首先咱們看看,如果不適用中介者模式,會是什麼狀態:
這個時候你會說,不!我不信會這麼複雜!是真的麼?我們的檢視模型肯定和命令模型有互動吧,命令模型和領域模型肯定也有吧,那命令中有錯誤資訊吧,肯定要交給通知模型的,說到這裡,你應該會感覺可能真的有一些複雜的互動,當然!也可能沒有那麼複雜,我們平時就是一個實體 model 走天下的,錯誤資訊隨便返回給字串呀,等等諸如此類。
如果你承認了這個結構很複雜,那好!咱們看看中介者模式會是什麼樣子的,可能你看著會更復雜,但是會很清晰:
(這可是老張花了一個小時畫的,兄弟給個贊:+1:吧)
不知道你看到這裡會不會腦子一嗡,沒關係,等這個系列說完了,你就會明白了,今天咱們就主要說的是其中一個部分, 命令匯流排 Command Bus、命令處理程式、工作單元的提交 這三塊:
從上邊的大圖中,我們看到,本來交織在一起的多個模型,本一條虛擬的流程串了起來,這裡邊就包括CQRS讀寫分離思想 和 中介者模型,當然還有人說是釋出-訂閱模型,這個我還在醞釀,以後的文章會說到。雖然物件還是那麼多,但是清晰了起來,多個物件之間也沒有存在一個很深的聯絡,讓業務之間更加專注自身業務。
如果你現在對中介者模式已經有了一定的意識,也知道了它的作用和意思,那它到底是如何操作的呢,請耐心往外看,重點來了。
二、建立命令匯流排 Command Bus
1、建立一箇中介處理程式介面
在我們的核心領域層 Christ3D.Domain.Core 中,新建 Bus 資料夾,然後建立中介處理程式介面 IMediatorHandler.cs
namespace Christ3D.Domain.Core.Bus { /// <summary> /// 中介處理程式介面 /// 可以定義多個處理程式 /// 是非同步的 /// </summary> public interface IMediatorHandler { /// <summary> /// 釋出命令,將我們的命令模型釋出到中介者模組 /// </summary> /// <typeparam name="T"> 泛型 </typeparam> /// <param name="command"> 命令模型,比如RegisterStudentCommand </param> /// <returns></returns> Task SendCommand<T>(T command) where T : Command; } }
釋出命令:就好像我們呼叫某招聘平臺,釋出了一個招聘命令。
2、一個低調的中介者工具 —— MediatR
微軟官方eshopOnContainer開源專案中使用到了該工具, mediatR 是一種中介工具,解耦了訊息處理器和訊息之間耦合的類庫,支援跨平臺 .net Standard和.net framework <a href="https://github.com/jbogard/MediatR/wiki" target="_blank">https://github.com/jbogard/MediatR/wiki</a> 這裡是原文地址。其作者也是Automapper的作者。 功能要是簡述的話就倆方面: request/response 請求響應 //咱們就採用這個方式 pub/sub 釋出訂閱
使用方法:通過 .NET CORE 自帶的 IoC 注入
引用 MediatR nuget:install-package MediatR
引用IOC/">IOC擴充套件 nuget:installpackage MediatR.Extensions.Microsoft.DependencyInjection //擴充套件包
使用方式:
services.AddMediatR(typeof(MyxxxHandler)); //單單注入某一個處理程式
或
services.AddMediatR(typeof(Startup).GetTypeInfo().Assembly); //目的是為了掃描Handler的實現物件並新增到IOC的容器中
//參考示例 //請求響應方式(request/response),三步走: //步驟一:建立一個訊息物件,需要實現IRequest,或IRequest<> 介面,表明該物件是處理器的一個物件 public class Ping : IRequest<string> { } //步驟二:建立一個處理器物件 public class PingHandler : IRequestHandler<Ping, string> { public Task<string> Handle(Ping request, CancellationToken cancellationToken) { return Task.FromResult("老張的哲學"); } } //步驟三:最後,通過mediator傳送一個訊息 var response = await mediator.Send(new Ping()); Debug.WriteLine(response); // "老張的哲學"
3、專案中實現中介處理程式介面
這裡就不講解為什麼要使用 MediatR 來實現我們的中介者模式了,因為我沒有找到其他的:joy:,具體的使用方法很簡單,就和我們的快取 IMemoryCache 一樣,通過注入,呼叫該介面即可,如果你還是不清楚的話,先往下看吧,應該也能看懂。
新增 nuget 包:MediatR
注意:我這裡把包安裝到了Christ3D.Domain.Core 核心領域層了,因為還記得上邊的那個大圖麼,我說到的,一條貫穿專案的線,所以這個中介處理程式介面在其他地方也用的到(比如領域層),所以我在核心領域層,安裝了這個nuget包。 注意安裝包後,需要編譯下當前專案 。
新建一個類庫 Christ3D.Infra.Bus
當然你也可以把它和介面 IMediatorHandler 放在一起,不過我個人感覺不是很舒服,因為這個具體的實現過程,不是我們領域設計需要知道的,就好像我們的 EFCore 倉儲,我們就是在領域層,建立了倉儲介面,然後再在基礎設施資料層 Christ3D.Infrastruct.Data 中實現的,所以為了保持一致性,我就新建了這個類庫專案,用來實現我們的中介處理程式介面。
注意下,Bus匯流排類庫是需要引用 Domain.Core 核心領域層的,所以我們以後在 Domain領域層,直接引用 Bus匯流排層即可。
實現我們的中介處理程式介面
namespace Christ3D.Infra.Bus { /// <summary> /// 一個密封類,實現我們的中介記憶匯流排 /// </summary> public sealed class InMemoryBus : IMediatorHandler { //建構函式注入 private readonly IMediator _mediator; public InMemoryBus(IMediator mediator) { _mediator = mediator; } /// <summary> /// 實現我們在IMediatorHandler中定義的介面 /// 沒有返回值 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="command"></param> /// <returns></returns> public Task SendCommand<T>(T command) where T : Command { return _mediator.Send(command);//這裡要注意下 command 物件 } } }
這個send方法,就是我們的中介者來替代物件,進行命令的分發,這個時候你可以會發現報錯了,我們F12看看這個方法:
可以看到 send 方法的入參,必須是MediarR指定的 IRequest 物件,所以,我們需要給我們的 Command命令基類,再繼承一個抽象類:

這個時候,我們的中介匯流排就搞定了。
4、刪除命令模型在Controller中的使用
1、把領域命令模型 從 controller 中去掉
只需要一個service呼叫即可
這個時候我們文字開頭的第一個問題就出現了,我們先把 Controller 中的命令模型驗證去掉,然後在我們的應用層 Service 中呼叫,這裡先看看文章開頭的第二個問題方法(當然是不對的方法):
public void Register(StudentViewModel StudentViewModel) { RegisterStudentCommand registerStudentCommand = new RegisterStudentCommand(studentViewMod.........ewModel.Phone); //如果命令無效,證明有錯誤 if (!registerStudentCommand.IsValid()) { List<string> errorInfo = new List<string>(); //獲取到錯誤,請思考這個Result從哪裡來的 //..... //對錯誤進行記錄,還需要拋給前臺 ViewBag.ErrorData = errorInfo; } _StudentRepository.Add(_mapper.Map<Student>(StudentViewModel)); _StudentRepository.SaveChanges(); }
且不說這裡邊語法各種有問題(比如不能用 ViewBag ,當然你可能會說用快取),單單從整體設計上就很不舒服,這樣僅僅是從api介面層,挪到了應用服務層,這一塊明明是業務邏輯,業務邏輯就是領域問題,應該放到領域層。
而且還有文章說到的第四個問題,這裡也沒有解決,就是這裡依然有領域模型 Student ,沒有實現命令模型、領域模型等的互動通訊。
說到這裡,你可能腦子裡有了一個大膽的想法,還記得上邊說的中介者模式麼,就是很好的實現了多個物件之間的通訊,還不破壞各自的內部邏輯,使他們只關心自己的業務邏輯,那具體如果使用呢,請往下看。
5、在 StudentAppService 服務中,呼叫中介處理介面
通過建構函式注入我們的中介處理介面,這個大家應該都會了吧
//注意這裡是要IoC依賴注入的,還沒有實現 private readonly IStudentRepository _StudentRepository; //用來進行DTO private readonly IMapper _mapper; //中介者 匯流排 private readonly IMediatorHandler Bus; public StudentAppService( IStudentRepository StudentRepository, IMediatorHandler bus, IMapper mapper ) { _StudentRepository = StudentRepository; _mapper = mapper; Bus = bus; }
然後修改服務方法
public void Register(StudentViewModel StudentViewModel) { //這裡引入領域設計中的寫命令 還沒有實現 //請注意這裡如果是平時的寫法,必須要引入Student領域模型,會造成汙染 //_StudentRepository.Add(_mapper.Map<Student>(StudentViewModel)); //_StudentRepository.SaveChanges(); var registerCommand = _mapper.Map<RegisterStudentCommand>(StudentViewModel); Bus.SendCommand(registerCommand); }
最後記得要對服務進行注入,這裡有兩個點
1、ConfigureServices 中新增 MediatR 服務
// Adding MediatR for Domain Events // 領域命令、領域事件等注入 // 引用包 MediatR.Extensions.Microsoft.DependencyInjection services.AddMediatR(typeof(Startup));
2、在我們的 Christ3D.Infra.IoC 專案中,注入我們的中介匯流排介面
services.AddScoped<IMediatorHandler, InMemoryBus>();
老張說:這裡的注入,就是指,每當我們訪問 IMediatorHandler 處理程式的時候,就是例項化 InmemoryBus 物件。
到了這裡,我們才完成了第一步,命令匯流排的定義,也就是中介處理介面的定義與使用,那具體是如何進行分發的呢,我們又是如何進行資料持久化,儲存資料的呢?請往下看,我們先說下工作單元。
三、工作單元模式 UnitOfWork
博主按 :這是一個很豐富的內容,今天就不詳細說明了,留一個坑,為以後23種設計模式的時候,再詳細說明!
1、為什麼要定義工作單元
首先了解工作單元(Unit of Work)的意圖:維護受業務影響的物件列表,並且協調變化的寫入和解決併發問題。
可以用工作單元來實現事務,工作單元就是記錄物件資料變化的物件。只要開始做一些可能對所要記錄的物件的資料有影響的操作,就會建立一個工作單元去記錄這些變化,所以,每當建立、修改、或刪除一個物件的時候,就會通知工作單元。
2、如何定義UnitOfWork
1、在Christ3D.Domain 領域層的介面資料夾Interfaces種,新建工作單元介面 IUnitOfWork.cs
namespace Christ3D.Domain.Interfaces { /// <summary> /// 工作單元介面 /// </summary> public interface IUnitOfWork : IDisposable { //是否提交成功 bool Commit(); } }
2、在基礎設施層,實現工作單元介面
namespace Christ3D.Infra.Data.UoW { /// <summary> /// 工作單元類 /// </summary> public class UnitOfWork : IUnitOfWork { //資料庫上下文 private readonly StudyContext _context; //建構函式注入 public UnitOfWork(StudyContext context) { _context = context; } //上下文提交 public bool Commit() { return _context.SaveChanges() > 0; } //手動回收 public void Dispose() { _context.Dispose(); } } }
3、記得在IoC層依賴注入
services.AddScoped<IUnitOfWork, UnitOfWork>();
四、命令處理程式 CommandHandlers
因為篇幅(太長了有些暈)和時間的問題,今天就暫時先說到這裡,程式碼我已經寫好了,並且提交到了Github,大家如果想看的可以先pull下來,至於為什麼這麼用以及它的意義,咱們下篇文章再詳細說。其實整體流程和原理,我在上邊也說的很詳細了,如果你能根據聯合國的栗子看懂這個(注意要結合與依賴注入來理解),那你就是完完全全的理解了,如果下邊的程式碼還不是很清楚,沒關係,週末大家先看看,下週我詳細給大家講解下。
我這裡先給大家列舉下三步走,為下次做準備:
1、新增一個命令處理程式基類 CommandHandler.cs
2、通過快取Memory來記錄通知資訊(錯誤方法)
3、定義學生命令處理程式 StudentCommandHandler.cs
五、息鼓
今天真沒想到會寫這麼多,看來還是夜裡安靜的時候更容易寫東西,思路清晰,沒辦法,我只能把本文拆成兩個文章了。這篇文章我是來來回回的刪了寫,寫了刪,一個下午+一個晚上,大概6個小時,真是很累心的一個過程,不過想想,哪怕有一個小夥伴能通過文字學到東西,也是極好極開心的,好啦,老張要睡覺了,至於文章的病句,截圖等,明天再調整吧。加油!