緣起
哈嘍小伙伴周三好,老張又來啦,DDD領域驅動設計的第二個D也快說完了,下一個系列我也在考慮之中,是 Id4 還是 Dockers 還沒有想好,甚至昨天我還想,下一步是不是可以寫一個簡單的Angular 入門教程,本來是想來個前后端分離的教學視頻的,簡單試了試,發現自己的聲音不好聽,真心不好聽那種,就作罷了,我看博客園有一個大神在 Bilibili 上有一個視頻,具體地址忘了,有需要的留言,我找找。不過最近年底了比較累了,目前已經寫了15萬字了(一百天,平均一天1500字),或者看看是不是給自己放一個假吧,自己也找一些書看一看,給自己充充電,希望大家多提一下建議或者幫助吧。
言歸正傳,在上一篇文章中《之十 ║領域驅動【實戰篇·中】:命令總線Bus分發(一)》,我主要是介紹了,如果通過命令模式來對我們的API層(這里也包括應用層)進行解耦,通過命令分發,可以很好的解決在應用層寫大量的業務邏輯,以及多個對象之間混亂的關聯的問題。如果對上一篇文章不是很記得了,我這里簡單再總結一下,如果你能看懂這些知識點,並心里能大概行程一個輪廓,那可以繼續往下看了,如果說看的很陌生,或者想不起來了,那請看上一篇文章吧。上篇文章有以下幾個小點:
1、什么是中介者模式?以及中介者模式的原理?(提示:多對象不依賴,但可通訊)
2、MediatR 是如何實現中介者服務的?常用哪兩種方法?(提示:請求/響應)
3、工作單元是什么?作用?(提示:事務)
這些知識點都是在上文中提到的,可能說的有點兒凌亂,不知道是否能看懂,上篇遺留了幾個問題,所以我就新開了一篇文章,來重點對上一篇文章進行解釋說明,大家可以看看是否和自己想的一樣,歡迎來交流。
當然還是每篇一問,也是本文的提綱:
1、我們是如何把一個Command命令,一步步走到持久化的?
2、你自己能畫一個詳細的流程草圖么?
零、今天實現左下角淺紫色的下框部分
(昨天的故事中,說到了,咱們已經建立了一個基於 MediatR 的在緩存中的命令總線,我們可以在任何一個地方通過該總線進行命令的分發,然后我們在應用層 StudentAppService.cs 中,對添加StudentCommand進行了分發,那我們到底應該如何分發,中介者又是如何調用的呢, 今天我們就繼續接着昨天的故事往下說... )
一、創建命令處理程序 CommandHandlers
咱們先把處理程序做出來,具體是如何執行的,咱們下邊會再說明。
1、添加一個命令處理程序基類 CommandHandler.cs
namespace Christ3D.Domain.CommandHandlers { /// <summary> /// 領域命令處理程序 /// 用來作為全部處理程序的基類,提供公共方法和接口數據 /// </summary> public class CommandHandler { // 注入工作單元 private readonly IUnitOfWork _uow; // 注入中介處理接口(目前用不到,在領域事件中用來發布事件) private readonly IMediatorHandler _bus; // 注入緩存,用來存儲錯誤信息(目前是錯誤方法,以后用領域通知替換) private IMemoryCache _cache; /// <summary> /// 構造函數注入 /// </summary> /// <param name="uow"></param> /// <param name="bus"></param> /// <param name="cache"></param> public CommandHandler(IUnitOfWork uow, IMediatorHandler bus, IMemoryCache cache) { _uow = uow; _bus = bus; _cache = cache; } //工作單元提交 //如果有錯誤,下一步會在這里添加領域通知 public bool Commit() { if (_uow.Commit()) return true; return false; } } }
這個還是很簡單的,只是提供了一個工作單元的提交,下邊會增加對領域通知的偽處理。
2、定義學生命令處理程序 StudentCommandHandler.cs
namespace Christ3D.Domain.CommandHandlers { /// <summary> /// Student命令處理程序 /// 用來處理該Student下的所有命令 /// 注意必須要繼承接口IRequestHandler<,>,這樣才能實現各個命令的Handle方法 /// </summary> public class StudentCommandHandler : CommandHandler, IRequestHandler<RegisterStudentCommand, Unit>, IRequestHandler<UpdateStudentCommand, Unit>, IRequestHandler<RemoveStudentCommand, Unit> { // 注入倉儲接口 private readonly IStudentRepository _studentRepository; // 注入總線 private readonly IMediatorHandler Bus; private IMemoryCache Cache; /// <summary> /// 構造函數注入 /// </summary> /// <param name="studentRepository"></param> /// <param name="uow"></param> /// <param name="bus"></param> /// <param name="cache"></param> public StudentCommandHandler(IStudentRepository studentRepository, IUnitOfWork uow, IMediatorHandler bus, IMemoryCache cache ) : base(uow, bus, cache) { _studentRepository = studentRepository; Bus = bus; Cache = cache; } // RegisterStudentCommand命令的處理程序 // 整個命令處理程序的核心都在這里 // 不僅包括命令驗證的收集,持久化,還有領域事件和通知的添加 public Task<Unit> Handle(RegisterStudentCommand message, CancellationToken cancellationToken) { // 命令驗證 if (!message.IsValid()) { // 錯誤信息收集 NotifyValidationErrors(message); return Task.FromResult(new Unit()); } // 實例化領域模型,這里才真正的用到了領域模型 // 注意這里是通過構造函數方法實現 var customer = new Student(Guid.NewGuid(), message.Name, message.Email, message.Phone, message.BirthDate); // 判斷郵箱是否存在 // 這些業務邏輯,當然要在領域層中(領域命令處理程序中)進行處理 if (_studentRepository.GetByEmail(customer.Email) != null) { //這里對錯誤信息進行發布,目前采用緩存形式 List<string> errorInfo = new List<string>() { "The customer e-mail has already been taken." }; Cache.Set("ErrorData", errorInfo); return Task.FromResult(new Unit()); } // 持久化 _studentRepository.Add(customer); // 統一提交 if (Commit()) { // 提交成功后,這里需要發布領域事件 // 比如歡迎用戶注冊郵件呀,短信呀等 // waiting.... } return Task.FromResult(new Unit()); } // 同上,UpdateStudentCommand 的處理方法 public Task<Unit> Handle(UpdateStudentCommand message, CancellationToken cancellationToken) { // 省略... } // 同上,RemoveStudentCommand 的處理方法 public Task<Unit> Handle(RemoveStudentCommand message, CancellationToken cancellationToken) { // 省略... } // 手動回收 public void Dispose() { _studentRepository.Dispose(); } } }
3、注入我們的處理程序
在我們的IoC項目中,注入我們的命令處理程序,這個時候,你可能有疑問,為啥是這樣的,下邊我講原理的時候會說明。
// Domain - Commands services.AddScoped<IRequestHandler<RegisterStudentCommand, Unit>, StudentCommandHandler>(); services.AddScoped<IRequestHandler<UpdateStudentCommand, Unit>, StudentCommandHandler>(); services.AddScoped<IRequestHandler<RemoveStudentCommand, Unit>, StudentCommandHandler>();
好啦!這個時候我們已經成功的,順利的,把由中介總線發出的命令,借助中介者 MediatR ,通過一個個處理程序,把我們的所有命令模型,領域模型,驗證模型,當然還有以后的領域事件,和領域通知聯系在一起了,只有上邊兩個類,甚至說只需要一個 StudentCommandHandler.cs 就能搞定,因為另一個 CommandHandler 僅僅是一個基類,完全可以合並在 StudentCommandHandler 類里,是不是感覺很神奇,如果這個時候你沒有感覺到他的好處,請先停下往下看的眼睛,仔細思考一下,如果我們不采用這個方法,我們會是怎么的工作:
在 API 層的controller中,進行參數驗證,然后if else 判斷,
接下來在服務器中寫持久化,然后也要對持久化中的錯誤信息,返回到 API 層;
不僅如此,我們還需要提交成功后,進行發郵件,或者發短信等子業務邏輯(當然這一塊,咱們還沒實現,不過已經挖好了坑,下一節會說到。);
最后,我們可能以后會說,添加成功和刪除成功發的郵件方法不一樣,甚至還有其他;
現在想想,如果這樣的工作,我們的業務邏輯需要寫在哪里?毫無疑問的,當然是在API層和應用層,我們領域層都干了什么?只有簡單的一個領域模型和倉儲接口!那這可真的不是DDD領域驅動設計的第二個D —— 驅動。
但是現在我們采用中介者模式,用命令驅動的方法,情況就不是這樣了,我們在API 層的controller中,只有一行代碼,在應用服務層也只有兩行;
var registerCommand = _mapper.Map<RegisterStudentCommand>(StudentViewModel); Bus.SendCommand(registerCommand);
到這個時候,我們已經從根本上,第二次了解了DDD領域驅動設計所帶來的不一樣的快感(第一次是領域、聚合、值對象等相關概念)。當然可能還不是很透徹,至少我們已經通過第一條總線——命令總線,來實現了復雜多模型直接的通訊了,下一篇我們說領域事件的時候,你會更清晰。那聰明的你一定就會問了:
好吧,你說的這些我懂了,也大概知道了怎么用,那它們是如何運行的呢?不知道過程,反而無法理解其作用!沒錯,那接下來,我們就具體說一說這個命令是如何分發的,請耐心往下看。
二、基於源碼分析命令處理過程
這里說的基於源碼,不是一字一句的講解,那要是我能說出來,我就是作者了😄,我就簡單的說一說,希望大家能看得懂。
0、下載 MediatR 源碼
既然要研究源碼,這里就要下載相應的代碼,這里有兩個方式,
1、可以在VS 中下載 ReSharper ,可以查看反編譯的所有代碼,注意會比以前卡一些。
2、直接查看Github ,https://github.com/jbogard/MediatR/tree/master/src/MediatR,現在開源的項目是越來越多,既然人家開源了,咱們就不能辜負了他們的開源精神,所以下載下來看一看也是很不錯。
本來我想把整個類庫,添加到咱們的項目中,發現有兼容問題,想想還是算了,就把其中幾個方法摘出來了,比如這個 Mediator.Send() 方法。
下邊就是整體流程,
1、應用層的命令請求:
// 領域命令請求 Bus.SendCommand(registerCommand);
2、領域命令的包裝
不知道大家還記得 MediatR 有哪兩種常用方法,沒錯,就是請求/響應 Request/Response 和 發布 Publish 這兩種,咱們的命令是用的第一種方法,所以今天就先說說這個 Mediator.Send() 。咱們在中介內存總線InMemoryBus.cs 中,定義了SendCommand方法,是基於IMediator 接口的,今天咱們就把真實的方法拿出來:
1、把源代碼中 Internal 文件夾下的 RequestHandlerWrapper.cs 放到我們的基礎設施層的 Christ3D.Infra.Bus 層中
從這個名字 RequestHandlerWrapper 中我們也能看懂,這個類的作用,就是把我們的請求領域命令,包裝成指定的命令處理程序。
2、修改我們的內存總線方法
namespace Christ3D.Infra.Bus { /// <summary> /// 一個密封類,實現我們的中介內存總線 /// </summary> public sealed class InMemoryBus : IMediatorHandler { //構造函數注入 private readonly IMediator _mediator; //注入服務工廠 private readonly ServiceFactory _serviceFactory; private static readonly ConcurrentDictionary<Type, object> _requestHandlers = new ConcurrentDictionary<Type, object>(); public InMemoryBus(IMediator mediator, ServiceFactory serviceFactory) { _mediator = mediator; _serviceFactory = serviceFactory; } /// <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);//請注意 入參 的類型 //注意!這個僅僅是用來測試和研究源碼的,請開發的時候不要使用這個 return Send(command);//請注意 入參 的類型 } /// <summary> /// Mdtiator Send方法源碼 /// </summary> /// <typeparam name="TResponse">泛型</typeparam> /// <param name="request">請求命令</param> /// <param name="cancellationToken">用來控制線程Task</param> /// <returns></returns> public Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default) { // 判斷請求是否為空 if (request == null) { throw new ArgumentNullException(nameof(request)); } // 獲取請求命令類型 var requestType = request.GetType(); // 對我們的命令進行封裝 // 請求處理程序包裝器 var handler = (RequestHandlerWrapper<TResponse>)_requestHandlers.GetOrAdd(requestType, t => Activator.CreateInstance(typeof(RequestHandlerWrapperImpl<,>).MakeGenericType(requestType, typeof(TResponse))));
//↑↑↑↑↑↑↑ 這以上是第二步 ↑↑↑↑↑↑↑↑↑↑
//↓↓↓↓↓↓↓ 第三步開始 ↓↓↓↓↓↓↓↓↓
// 執行封裝好的處理程序 // 說白了就是執行我們的命令 return handler.Handle(request, cancellationToken, _serviceFactory); } } }
上邊的方法的第二步中,我們獲取到了 handler ,這個時候,我們已經把 RegisterStudentCommand 命令,包裝成了 RequestHandlerWrapper<RegisterStudentCommand> ,那如何成功的定位到 StudentCommandHandler.cs 呢,請繼續往下看。(你要是問我作者具體是咋封裝的,請看源碼,或者給他發郵件,說不定你還可以成為他的開發者之一喲 ~)
3、服務工廠調用指定的處理程序
我們獲取到了 handler 以后,就去執行該處理程序
handler.Handle(request, cancellationToken, _serviceFactory);
我們看到 這個handler 還是一個抽象類 internal abstract class RequestHandlerWrapper<TResponse> ,接下來,我們就是通過 .Handle() ,對抽象類進行實現
上圖的過程是這樣:
1、訪問類方法 handler.Handle() ;
2、是一個管道處理程序,要包圍內部處理程序的管道行為,實現添加其他行為並等待下一個委托。
3、就是調用了這個匿名方法;
4、執行GetHandler() 方法;
其實從上邊簡單的看出來,就是實現了請求處理程序從抽象到實現的過程,然后添加管道,並下一步要對該處理程序進行實例化的過程,說白了就是把 RequestHandlerWrapper<RegisterStudentCommand> 給轉換成 IRequestHandler<RegisterStudentCommand> 的過程,然后下一步給 new 了一下。可是這個時候你會問,那實例化,肯定得有一個對象吧,這個接口自己肯定無法實例化的,沒錯!如果你能想到這里,證明你已經接近成功了,請繼續往下看。
4、通過注入,實例化我們的處理程序
在上邊的步驟中,我們知道了一個命令是如何封裝成了特定的處理程序接口,然后又是在哪里進行實例化的,但是具體實例化成什么樣的對象呢,就是在我們的 IoC 中:
// Domain - Commands // 將命令模型和命令處理程序匹配 services.AddScoped<IRequestHandler<RegisterStudentCommand, Unit>, StudentCommandHandler>(); services.AddScoped<IRequestHandler<UpdateStudentCommand, Unit>, StudentCommandHandler>(); services.AddScoped<IRequestHandler<RemoveStudentCommand, Unit>, StudentCommandHandler>();
如果你對依賴注入很了解的話,你一眼就能明白這個的意義是什么:
依賴注入 services.AddScoped<A, B>();意思就是,當我們在使用或者實例化接口對象 A 的時候,會在容器中自動匹配,並尋找與之對應的類對象 B。說到這里你應該也就明白了,在第三步中,我們通過 GetInstance,對我們包裝后的命令處理程序進行實例化的時候,自動尋找到了 StudentCommandHandler.cs 類。
5、匹配具體的命令處理方法
這個很簡單,在第四步之后,緊接着就是自動尋找到了 Task<Unit> Handle(RegisterStudentCommand message, CancellationToken cancellationToken) 方法,整個流程就這么結束了。
現在這個流程你應該已經很清晰了,或者大概了解了整體過程,還有一個小問題就是,我們如何將錯誤信息收集的,在之前的Controller 里寫業務邏輯的時候,用的是 ViewBag,那類庫是肯定不能這么用的,為了講解效果,我暫時用緩存替換,明天我們會用領域事件來深入講解。
三、用緩存來記錄錯誤通知
這里僅僅是一個小小的亂入補充,上邊已經把流程調通了,如果你想看看什么效果,這里就出現了一個問題,我們的錯誤通知信息沒有辦法獲取,因為之前我們用的是ViewBag,這里無效,當然Session等都無效了,因為我們是在整個項目的多個類庫之間使用,只能用 Memory 緩存了。
1、命令處理程序基類CommandHandler 中,添加公共方法
//將領域命令中的驗證錯誤信息收集 //目前用的是緩存方法(以后通過領域通知替換) protected void NotifyValidationErrors(Command message) { List<string> errorInfo = new List<string>(); foreach (var error in message.ValidationResult.Errors) { errorInfo.Add(error.ErrorMessage); } //將錯誤信息收集 _cache.Set("ErrorData", errorInfo); }
2、在Student命令處理程序中調用
3、自定義視圖模型中加載
/// <summary> /// Alerts 視圖組件 /// 可以異步,也可以同步,注意方法名稱,同步的時候是Invoke /// 我寫異步是為了為以后做准備 /// </summary> /// <returns></returns> public async Task<IViewComponentResult> InvokeAsync() { // 獲取到緩存中的錯誤信息 var errorData = _cache.Get("ErrorData"); var notificacoes = await Task.Run(() => (List<string>)errorData); // 遍歷添加到ViewData.ModelState 中 notificacoes?.ForEach(c => ViewData.ModelState.AddModelError(string.Empty, c)); return View(); }
這都是很簡單,就不多說了,下一講的領域事件,再好好說吧。
這個時候記得要在API的controller中,每次把緩存清空。
4、效果瀏覽
整體流程就是這樣:
四、結語
上邊的流程想必你已經看懂了,或者說七七八八,但是,至少你現在應該明白了,中介者模式,是如何通過命令總線Bus,把命令發出去,又是為什么在領域層的處理程序里接受到的,最后又是如何執行的,如果還是不懂,請繼續看一看,或者結合代碼,調試一下。我們可以這樣來說,請求以命令的形式包裹在對象中,並傳給調用者。調用者(代理)對象查找可以處理該命令的合適的對象,並把該命令傳給相應的對象,該對象執行命令 。
如果你看到這里了,那你下一節的領域事件,就很得心應手,這里有兩個問題遺留下來:
1、我們記錄錯誤信息,緩存很不好,還需要每次清理,不是基於事務的,那如何替換呢?
2、MediatR有兩個常用方法,一個是請求/響應模式,另一個發布模式如何使用么?
如果你很好奇,那就請看下回分解吧~~
五、GitHub & Gitee
https://github.com/anjoy8/ChristDDD
https://gitee.com/laozhangIsPhi/ChristDDD
--End