哈嘍你們好,老張又見面了,這兩天被各個平臺的「雞湯貼」差點亂了心神,博客園如此,簡書亦如此,還好羣裏小夥伴及時提醒,路還很長,這些小事兒就隨風而去吧,這周本不打算更了,可是被羣裏小夥伴「催稿」了,至少也是對個人一個確定吧,又開始熬夜中,請@初久小夥伴留言,我不知道你的地址,就不放連接了。html
收住,言歸正傳,上次我們說到了領域命令驗證《九 ║從軍事故事中,明白領域命令驗證(上)》,也介紹了其中的兩個角色——領域命令模型和命令驗證,這些都是屬於領域層的概念,固然這裏的內容是 命令 ,查詢就固然不須要這個了,查詢的話,直接從倉儲中獲取值就好了,很簡單。也沒人問我問題,那我就權當你們已經對上篇都看懂了,這裏就再也不贅述。不知道你們是否還記得上篇文章末尾,提到的幾個問題,我這裏再提一下,就是今天的提綱了,若是你今天看完本篇,這幾個問題能回答上來,那恭喜,你就明白了今天所講的問題:git
一、命令模型RegisterStudentCommand 放到 Controller 中真的好麼?//咱們平時都是這麼作的github
二、若是不放到Controller裏調用,咱們若是調用?在 Service裏麼?//也是一個辦法,至少Controller乾淨了,可是 Service 就重了數據庫
三、驗證的結果又如何獲取並在前臺展現呢?//本文會先用一個錯誤的方法來講明問題,下篇會用正確的設計模式
四、如何把領域模型 Student 從應用層 StudentAppService 解耦出去( Register()方法中 )。//本文重點,中介者模式api
好啦,簡單先寫這四個問題吧,這個時候你能夠先不要從 Github 上拉取代碼,先看着目前手中的代碼,而後思考這四個問題,若是要是本身,或者我們之前是怎麼作的,若是你看過之後會有一些新的認識和領悟,請幫忙評論一下,捧我的場嘛,是吧😀。好啦,今天的東西可能有點兒多,請作好大概半個小時的準備,固然這半個小時你須要思考,要是蜻蜓點水,確定是收穫沒有那麼多的,代碼已經更新了,記得看完的時候 pull 一下代碼。緩存
一、本文中可能會涉及比較多的依賴注入,請必定要看清楚,由於這是第二個系列了,有時候小細節就不點明瞭,須要你們有必定的基礎,能夠看我第一個系列。安全
二、這三篇核心內容,都是重點在領域層,請必定要多思考。併發
三、文章不只有代碼,更多的是理解,好比用聯合國的栗子來講明中介者模式,請務必要多思考。app
這個其實很好理解,單單從名字上你們也都能理解它是一個什麼模式,由於本文的重點不是一個講解什麼是23種設計模式的,你們有興趣的能夠好好的買本書,或者找找資料,好好,主要是思想,不須要本身寫一個項目,若是你們有須要,能夠留言,我之後單寫一篇文章,介紹中介者模式。
這裏就摘抄一段定義吧:
中介者模式是一個行爲設計模式,它容許咱們公開一個統一的接口,系統的 不一樣部分 能夠經過該接口進行 通訊,而 不須要 顯示的相互做用;
適用場景:若是一個系統的各個組件之間看起來有太多的直接關係(就好比咱們系統中那麼多模型對象,下邊會解釋),這個時候則須要一箇中心控制點,以便各個組件能夠經過這個中心控制點進行通訊;
該模式促進鬆散耦合的方式是:確保組件的交互是經過這個中心點來進行處理的,而不是經過顯示的引用彼此;
好比系統和各個硬件,系統做爲中介者,各個硬件做爲同事者,當一個同事的狀態發生改變的時候,不須要告訴其餘每一個硬件本身發生了變化,只須要告訴中介者系統,系統會通知每一個硬件某個硬件發生了改變,其餘的硬件會作出相應的變化;
這樣,以前是網狀結構,如今變成了以中介者爲中心的星星結構:
是否是挺像一個容器的,他本身把控着整個流程,和每個對象都有或多或少,或近或遠的聯繫,多個對象之間不用理睬其餘對象發生了什麼,只是負責本身的模塊就好,而後把消息發給中介者,讓中介者再分發給其餘的具體對象,從而實現通信 —— 這個思想就是中介者的核心思想,並且也是DDD領域驅動設計的核心思想之一( 還有一個核心思想是領域設計的思想 ),這裏你可能仍是不那麼直觀,我剛剛花了一個小時,對我們的DDD框架中的中介者模式畫了一個圖,相信會有一些新的認識,在下邊第 3 點會看到,請耐心往下看。
這裏有一個聯合國的栗子,也是經常使用來介紹和解釋中介者模式的栗子:
抽象中介者(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); } } }
最終的結果是:
從這個小栗子中,也許你能看出來,美國和伊拉克之間,對象之間並無任何的交集和聯繫,可是他們之間卻發生了通信,各自獨立,可是又相互通信,這個不就是很好的實現瞭解耦的做用麼!一切都是經過中介者來控制,固然這只是一個小栗子,我們推而廣之:
命令模式、消息通知模型、領域模型等,內部運行完成後,將產生的信息拋向給中介者,而後中介者再根據狀況分發給各個成員(若是又須要的),這樣就實現多個對象的解耦,並且也達到同步的做用,固然還有一些輔助知識:異步、注入、事件等,我們慢慢學習,至少如今中介者模式的思想和原理你應該都懂了。
相信若是你是從個人第一篇文章看下去的,必定會如下幾個模型很熟悉:視圖模型、領域模型、命令模型、驗證(上次說的)、還有沒有說到的通知模型,若是你對這幾個名稱還很朦朧,請如今先在腦子裏仔細想想,否則下邊的可能會亂,若是你一看到名字就能理解都是幹什麼的,都是什麼做用,那好,請看下邊的關係圖。
首先我們看看,若是不使用中介者模式,會是什麼狀態:
這個時候你會說,不!我不信會這麼複雜!是真的麼?咱們的視圖模型確定和命令模型有交互吧,命令模型和領域模型確定也有吧,那命令中有錯誤信息吧,確定要交給通知模型的,說到這裏,你應該會感受可能真的有一些複雜的交互,固然!也可能沒有那麼複雜,咱們平時就是一個實體 model 走天下的,錯誤信息隨便返回給字符串呀,等等諸如此類。
若是你認可了這個結構很複雜,那好!我們看看中介者模式會是什麼樣子的,可能你看着會更復雜,可是會很清晰:
(這但是老張花了一個小時畫的,兄弟給個贊👍吧)
不知道你看到這裏會不會腦子一嗡,不要緊,等這個系列說完了,你就會明白了,今天我們就主要說的是其中一個部分,命令總線 Command Bus、命令處理程序、工做單元的提交 這三塊:
從上邊的大圖中,咱們看到,原本交織在一塊兒的多個模型,本一條虛擬的流程串了起來,這裏邊就包括CQRS讀寫分離思想 和 中介者模型,固然還有人說是發佈-訂閱模型,這個我還在醞釀,之後的文章會說到。雖然對象仍是那麼多,可是清晰了起來,多個對象之間也沒有存在一個很深的聯繫,讓業務之間更加專一自身業務。
若是你如今對中介者模式已經有了必定的意識,也知道了它的做用和意思,那它究竟是如何操做的呢,請耐心往外看,重點來了。
在咱們的核心領域層 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; } }
發佈命令:就好像咱們調用某招聘平臺,發佈了一個招聘命令。
微軟官方eshopOnContainer開源項目中使用到了該工具, mediatR 是一種中介工具,解耦了消息處理器和消息之間耦合的類庫,支持跨平臺 .net Standard和.net framework https://github.com/jbogard/MediatR/wiki 這裏是原文地址。其做者也是Automapper的做者。 功能要是簡述的話就倆方面: request/response 請求響應 //我們就採用這個方式 pub/sub 發佈訂閱
使用方法:經過 .NET CORE 自帶的 IoC 注入
引用 MediatR nuget:install-package MediatR
引用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); // "老張的哲學"
這裏就不講解爲何要使用 MediatR 來實現咱們的中介者模式了,由於我沒有找到其餘的😂,具體的使用方法很簡單,就和咱們的緩存 IMemoryCache 同樣,經過注入,調用該接口便可,若是你仍是不清楚的話,先往下看吧,應該也能看懂。
注意:我這裏把包安裝到了Christ3D.Domain.Core 核心領域層了,由於還記得上邊的那個大圖麼,我說到的,一條貫穿項目的線,因此這個中介處理程序接口在其餘地方也用的到(好比領域層),因此我在覈心領域層,安裝了這個nuget包。注意安裝包後,須要編譯下當前項目。
更新:我放到了基礎設施層了,新建一個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命令基類,再繼承一個抽象類:
這個時候,咱們的中介總線就搞定了。
一、把領域命令模型 從 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 ,沒有實現命令模型、領域模型等的交互通信。
說到這裏,你可能腦子裏有了一個大膽的想法,還記得上邊說的中介者模式麼,就是很好的實現了多個對象之間的通信,還不破壞各自的內部邏輯,使他們只關心本身的業務邏輯,那具體若是使用呢,請往下看。
經過構造函數注入咱們的中介處理接口,這個你們應該都會了吧
//注意這裏是要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); }
最後記得要對服務進行注入,這裏有兩個點
一、ConfigureServices 中添加 MediatR 服務
// Adding MediatR for Domain Events // 領域命令、領域事件等注入 // 引用包 MediatR.Extensions.Microsoft.DependencyInjection services.AddMediatR(typeof(Startup));
二、在咱們的 NativeInjectorBootStrapper.cs 依賴注入文件中,注入咱們的中介總線接口
services.AddScoped<IMediatorHandler, InMemoryBus>();
老張說:這裏的注入,就是指,每當咱們訪問 IMediatorHandler 處理程序的時候,就是實例化 InmemoryBus 對象。
到了這裏,咱們才完成了第一步,命令總線的定義,也就是中介處理接口的定義與使用,那具體是如何進行分發的呢,咱們又是如何進行數據持久化,保存數據的呢?請往下看,咱們先說下工做單元。
博主按:這是一個很豐富的內容,今天就不詳細說明了,留一個坑,爲之後23種設計模式的時候,再詳細說明!
首先了解工做單元(Unit of Work)的意圖:維護受業務影響的對象列表,而且協調變化的寫入和解決併發問題。
能夠用工做單元來實現事務,工做單元就是記錄對象數據變化的對象。只要開始作一些可能對所要記錄的對象的數據有影響的操做,就會建立一個工做單元去記錄這些變化,因此,每當建立、修改、或刪除一個對象的時候,就會通知工做單元。
一、在Christ3D.Domain 領域層的接口文件夾Interfaces種,新建工做單元接口 IUnitOfWork.cs
namespace Christ3D.Domain.Interfaces { /// <summary> /// 工做單元接口 /// </summary> public interface IUnitOfWork : IDisposable { //是否提交成功 bool Commit(); } }
二、在基礎設施層,實現工做單元接口
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(); } } }
在原生依賴注入類 NativeInjectorBootStrapper.cs 中
services.AddScoped<IUnitOfWork, UnitOfWork>();
由於篇幅(太長了有些暈)和時間的問題,今天就暫時先說到這裏,代碼我已經寫好了,而且提交到了Github,你們若是想看的能夠先pull下來,至於爲何這麼用以及它的意義,我們下篇文章再詳細說。其實總體流程和原理,我在上邊也說的很詳細了,若是你能根據聯合國的栗子看懂這個(注意要結合與依賴注入來理解),那你就是完徹底全的理解了,若是下邊的代碼還不是很清楚,不要緊,週末你們先看看,下週我詳細給你們講解下。
我這裏先給你們列舉下三步走,爲下次作準備:
一、添加一個命令處理程序基類 CommandHandler.cs
二、經過緩存Memory來記錄通知信息(錯誤方法)
三、定義學生命令處理程序 StudentCommandHandler.cs
今天真沒想到會寫這麼多,看來仍是夜裏安靜的時候更容易寫東西,思路清晰,沒辦法,我只能把本文拆成兩個文章了。這篇文章我是來來回回的刪了寫,寫了刪,一個下午+一個晚上,大概6個小時,真是很累心的一個過程,不過想一想,哪怕有一個小夥伴能經過文字學到東西,也是極好極開心的,好啦,老張要睡覺了,至於文章的病句,截圖等,明天再調整吧。加油!