哈嘍小夥伴週三好,老張又來啦,DDD領域驅動設計的第二個D也快說完了,下一個系列我也在考慮之中,是 Id4 仍是 Dockers 尚未想好,甚至昨天我還想,下一步是否是能夠寫一個簡單的Angular 入門教程,原本是想來個先後端分離的教學視頻的,簡單試了試,發現本身的聲音很差聽,真心很差聽那種,就做罷了,我看博客園有一個大神在 Bilibili 上有一個視頻,具體地址忘了,有須要的留言,我找找。不過最近年末了比較累了,目前已經寫了15萬字了(一百天,平均一天1500字),或者看看是否是給本身放一個假吧,本身也找一些書看一看,給本身充充電,但願你們多提一下建議或者幫助吧。html
言歸正傳,在上一篇文章中《之十 ║領域驅動【實戰篇·中】:命令總線Bus分發(一)》,我主要是介紹了,若是經過命令模式來對咱們的API層(這裏也包括應用層)進行解耦,經過命令分發,能夠很好的解決在應用層寫大量的業務邏輯,以及多個對象之間混亂的關聯的問題。若是對上一篇文章不是很記得了,我這裏簡單再總結一下,若是你能看懂這些知識點,並內心能大概行程一個輪廓,那能夠繼續往下看了,若是說看的很陌生,或者想不起來了,那請看上一篇文章吧。上篇文章有如下幾個小點:git
一、什麼是中介者模式?以及中介者模式的原理?(提示:多對象不依賴,但可通信)github
二、MediatR 是如何實現中介者服務的?經常使用哪兩種方法?(提示:請求/響應)後端
三、工做單元是什麼?做用?(提示:事務)緩存
這些知識點都是在上文中提到的,可能說的有點兒凌亂,不知道是否能看懂,上篇遺留了幾個問題,因此我就新開了一篇文章,來重點對上一篇文章進行解釋說明,你們能夠看看是否和本身想的同樣,歡迎來交流。服務器
固然仍是每篇一問,也是本文的提綱:app
一、咱們是如何把一個Command命令,一步步走到持久化的?前後端分離
二、你本身能畫一個詳細的流程草圖麼?異步
(昨天的故事中,說到了,我們已經創建了一個基於 MediatR 的在緩存中的命令總線,咱們能夠在任何一個地方經過該總線進行命令的分發,而後咱們在應用層 StudentAppService.cs 中,對添加StudentCommand進行了分發,那咱們到底應該如何分發,中介者又是如何調用的呢, 今天咱們就繼續接着昨天的故事往下說... )async
我們先把處理程序作出來,具體是如何執行的,我們下邊會再說明。
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; } } }
這個仍是很簡單的,只是提供了一個工做單元的提交,下邊會增長對領域通知的僞處理。
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(); } } }
在咱們的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領域驅動設計所帶來的不同的快感(第一次是領域、聚合、值對象等相關概念)。固然可能還不是很透徹,至少咱們已經經過第一條總線——命令總線,來實現了複雜多模型直接的通信了,下一篇咱們說領域事件的時候,你會更清晰。那聰明的你必定就會問了:
好吧,你說的這些我懂了,也大概知道了怎麼用,那它們是如何運行的呢?不知道過程,反而沒法理解其做用!沒錯,那接下來,咱們就具體說一說這個命令是如何分發的,請耐心往下看。
這裏說的基於源碼,不是一字一句的講解,那要是我能說出來,我就是做者了😄,我就簡單的說一說,但願你們能看得懂。
既然要研究源碼,這裏就要下載相應的代碼,這裏有兩個方式,
一、能夠在VS 中下載 ReSharper ,能夠查看反編譯的全部代碼,注意會比之前卡一些。
二、直接查看Github ,https://github.com/jbogard/MediatR/tree/master/src/MediatR,如今開源的項目是愈來愈多,既然人家開源了,我們就不能辜負了他們的開源精神,因此下載下來看一看也是很不錯。
原本我想把整個類庫,添加到我們的項目中,發現有兼容問題,想一想仍是算了,就把其中幾個方法摘出來了,好比這個 Mediator.Send() 方法。
下邊就是總體流程,
// 領域命令請求 Bus.SendCommand(registerCommand);
不知道你們還記得 MediatR 有哪兩種經常使用方法,沒錯,就是請求/響應 Request/Response 和 發佈 Publish 這兩種,我們的命令是用的第一種方法,因此今天就先說說這個 Mediator.Send() 。我們在中介內存總線InMemoryBus.cs 中,定義了SendCommand方法,是基於IMediator 接口的,今天我們就把真實的方法拿出來:
一、把源代碼中 Internal 文件夾下的 RequestHandlerWrapper.cs 放到咱們的基礎設施層的 Christ3D.Infra.Bus 層中
從這個名字 RequestHandlerWrapper 中咱們也能看懂,這個類的做用,就是把咱們的請求領域命令,包裝成指定的命令處理程序。
二、修改咱們的內存總線方法
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 呢,請繼續往下看。(你要是問我做者具體是咋封裝的,請看源碼,或者給他發郵件,說不定你還能夠成爲他的開發者之一喲 ~)
咱們獲取到了 handler 之後,就去執行該處理程序
handler.Handle(request, cancellationToken, _serviceFactory);
咱們看到 這個handler 仍是一個抽象類 internal abstract class RequestHandlerWrapper<TResponse> ,接下來,咱們就是經過 .Handle() ,對抽象類進行實現
上圖的過程是這樣:
一、訪問類方法 handler.Handle() ;
二、是一個管道處理程序,要包圍內部處理程序的管道行爲,實現添加其餘行爲並等待下一個委託。
三、就是調用了這個匿名方法;
四、執行GetHandler() 方法;
其實從上邊簡單的看出來,就是實現了請求處理程序從抽象到實現的過程,而後添加管道,並下一步要對該處理程序進行實例化的過程,說白了就是把 RequestHandlerWrapper<RegisterStudentCommand> 給轉換成 IRequestHandler<RegisterStudentCommand> 的過程,而後下一步給 new 了一下。但是這個時候你會問,那實例化,確定得有一個對象吧,這個接口本身確定沒法實例化的,沒錯!若是你能想到這裏,證實你已經接近成功了,請繼續往下看。
在上邊的步驟中,咱們知道了一個命令是如何封裝成了特定的處理程序接口,而後又是在哪裏進行實例化的,可是具體實例化成什麼樣的對象呢,就是在咱們的 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 類。
這個很簡單,在第四步以後,緊接着就是自動尋找到了 Task<Unit> Handle(RegisterStudentCommand message, CancellationToken cancellationToken) 方法,整個流程就這麼結束了。
如今這個流程你應該已經很清晰了,或者大概瞭解了總體過程,還有一個小問題就是,咱們如何將錯誤信息收集的,在以前的Controller 裏寫業務邏輯的時候,用的是 ViewBag,那類庫是確定不能這麼用的,爲了講解效果,我暫時用緩存替換,明天咱們會用領域事件來深刻講解。
這裏僅僅是一個小小的亂入補充,上邊已經把流程調通了,若是你想看看什麼效果,這裏就出現了一個問題,咱們的錯誤通知信息沒有辦法獲取,由於以前咱們用的是ViewBag,這裏無效,固然Session等都無效了,由於咱們是在整個項目的多個類庫之間使用,只能用 Memory 緩存了。
//將領域命令中的驗證錯誤信息收集 //目前用的是緩存方法(之後經過領域通知替換) 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); }
/// <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中,每次把緩存清空。
總體流程就是這樣:
上邊的流程想必你已經看懂了,或者說七七八八,可是,至少你如今應該明白了,中介者模式,是如何經過命令總線Bus,把命令發出去,又是爲何在領域層的處理程序裏接受到的,最後又是如何執行的,若是仍是不懂,請繼續看一看,或者結合代碼,調試一下。咱們能夠這樣來講,請求以命令的形式包裹在對象中,並傳給調用者。調用者(代理)對象查找能夠處理該命令的合適的對象,並把該命令傳給相應的對象,該對象執行命令 。
若是你看到這裏了,那你下一節的領域事件,就很駕輕就熟,這裏有兩個問題遺留下來:
一、咱們記錄錯誤信息,緩存很很差,還須要每次清理,不是基於事務的,那如何替換呢?
二、MediatR有兩個經常使用方法,一個是請求/響應模式,另外一個發佈模式如何使用麼?
若是你很好奇,那就請看下回分解吧~~
https://github.com/anjoy8/ChristDDD
https://gitee.com/laozhangIsPhi/ChristDDD
--End