烽火
哈嘍大家好,老張又見面了,這兩天被各個平臺的“雞湯貼”差點亂了心神,博客園如此,簡書亦如此,還好群里小伙伴及時提醒,路還很長,這些小事兒就隨風而去吧,這周本不打算更了,但是被群里小伙伴“催稿”了,至少也是對我的一個肯定吧,又開始熬夜中,請@初久小伙伴留言,我不知道你的地址,就不放鏈接了。
收住,言歸正傳,上次咱們說到了領域命令驗證《九 ║從軍事故事中,明白領域命令驗證(上)》,也介紹了其中的兩個角色——領域命令模型和命令驗證,這些都是屬于領域層的概念,當然這里的內容是 命令 ,查詢就當然不需要這個了,查詢的話,直接從倉儲中獲取值就行了,很簡單。也沒人問我問題,那我就權當大家已經對上篇都看懂了,這里就不再贅述。不知道大家是否還記得上篇文章末尾,提到的幾個問題,我這里再提一下,就是今天的提綱了,如果你今天看完本篇,這幾個問題能回答上來,那恭喜,你就明白了今天所講的問題:
1、命令模型RegisterStudentCommand 放到 Controller 中真的好么?//我們平時都是這么做的
2、如果不放到Controller里調用,我們如果調用?在 Service里么?//也是一個辦法,至少Controller干凈了,但是 Service 就重了
3、驗證的結果又如何獲取并在前臺展示呢?//本文會先用一個錯誤的方法來說明問題,下篇會用正確的
4、如何把領域模型 Student 從應用層 StudentAppService 解耦出去( Register()方法中 )。//本文重點,中介者模式
好啦,簡單先寫這四個問題吧,這個時候你可以先不要從 Github 上拉取代碼,先看著目前手中的代碼,然后思考這四個問題,如果要是自己,或者咱們以前是怎么做的,如果你看過以后會有一些新的認識和領悟,請幫忙評論一下,捧個人場嘛,是吧??。好啦,今天的東西可能有點兒多,請做好大概半個小時的準備,當然這半個小時你需要思考,要是走馬觀花,肯定是收獲沒有那么多的,代碼已經更新了,記得看完的時候 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 走天下的,錯誤信息隨便返回給字符串呀,等等諸如此類。
如果你承認了這個結構很復雜,那好!咱們看看中介者模式會是什么樣子的,可能你看著會更復雜,但是會很清晰:
(這可是老張花了一個小時畫的,兄弟給個贊??吧)
不知道你看到這里會不會腦子一嗡,沒關系,等這個系列說完了,你就會明白了,今天咱們就主要說的是其中一個部分,**命令總線 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
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); // "老張的哲學"
3、項目中實現中介處理程序接口
這里就不講解為什么要使用 MediatR 來實現我們的中介者模式了,因為我沒有找到其他的??,具體的使用方法很簡單,就和我們的緩存 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個小時,真是很累心的一個過程,不過想想,哪怕有一個小伙伴能通過文字學到東西,也是極好極開心的,好啦,老張要睡覺了,至于文章的病句,截圖等,明天再調整吧。加油!