烽煙
哈嘍大家周二好呀寺董,咱們又見面了,上周末掐指一算刻剥,距離 圣誕節(jié) 只有 5 周的時間了(如果你還不知道為啥我要提圣誕節(jié)這個時間點遮咖,可以看看我的第二系列開篇《之一 ║ D3模式設計初探 與 我的計劃書》),然后我簡單的思考了下這個DDD領(lǐng)域驅(qū)動設計還剩下的知識點造虏,現(xiàn)在已經(jīng)進入了第二部分御吞,就是領(lǐng)域命令和領(lǐng)域驅(qū)動這一塊麦箍,第三部分包括Identity驗證和.net core api等設計點,大概就是剩了這么多陶珠,預計應該能在圣誕節(jié)前完成挟裂。還有一個就是,之前的八篇文章背率,已經(jīng)比較完整的實現(xiàn)了普通框架的整體搭建话瞧,我也單獨的新建了一個 Git分支—— Framework8 嫩与,如果你不想用領(lǐng)域命令寝姿、領(lǐng)域事件、事件回溯這些東西划滋,僅僅就想要一個空的框架饵筑,一個包括 EFCore+Dtos+Automapper+IoC+Repository 的空框架(就比如我的第一個系列,就是一個普通的框架处坪,請不要再說是這是一個普通三層了根资,拜托??),你就可以直接用這個Framework8 ****分支即可同窘。
言歸正傳玄帕,上次咱們說到了創(chuàng)建新student的時候,提出來一個問題想邦,不知道大家是否還記得裤纹,這里再給大家說明一下,還是每篇一問丧没,希望能好好思考下鹰椒,或者是看看自己是如何設計的:
問題1:平時是如何進行表單驗證的(包括:判空、字符類型有效呕童、業(yè)務驗證:成人不能小于18歲漆际、金額不能小于0等)?
問題2:如果后來驗證變化了改怎么辦夺饲?(比如:手機號要支持全球奸汇,或者座機;亦或者退休年齡從60歲變成65歲往声;)
1擂找、JavaScript前端驗證即可,后端從來不進行驗證烁挟?(問題2:修改js)
2婴洼、后端驗證:直接在Controller中,通過寫很多判斷邏輯撼嗓,比如 If Else等柬采,而且CURD還需要寫很多重復的判斷方法欢唾?(問題2:每一個地方都需要仔細修改,額粉捻。)
3礁遣、后端驗證:寫一個統(tǒng)一的驗證類,或者驗證機制肩刃,比如一個公共類祟霍?甚至更高級的AOP切面驗證?(問題2:好像還是無法滿足每個領(lǐng)域特例)
4盈包、后端驗證:在DTO基礎(chǔ)上沸呐,基于領(lǐng)域命令,通過中介者Bus分發(fā)呢燥?(當然這個就是以后我要寫的)
其實說實話崭添,前三種我都用過,甚至現(xiàn)在偶爾也還是會用叛氨,畢竟很平常的用法呼渣,但是現(xiàn)在我感覺第四種真的很整潔,真正的把整體項目放到了領(lǐng)域中寞埠,一切以領(lǐng)域為核心了 屁置。這里我先把第四種的應用層 Service 方法簡單寫下,你就知道多么簡潔了仁连,具體的會在下面兩篇文章中說到:
/// <summary>
/// StudentAppService 添加新 Student /// </summary>
public void Register(StudentViewModel studentViewModel)
{ //講視圖模型蓝角,轉(zhuǎn)換成命名模型
var registerCommand = _mapper.Map<RegisterStudentCommand>(studentViewModel); //通過Mediator處理程序分發(fā)命令 //執(zhí)行順序:驗證 -> 通知 -> 注冊
Bus.SendCommand(registerCommand);
}
老張說:這兩天我在研究,啃書的時候怖糊,發(fā)現(xiàn)了這個DDD領(lǐng)域驅(qū)動的整體流程帅容,從前臺數(shù)據(jù)傳遞視圖模型 ,到Dto的命令模型伍伤,然后對其校驗的命令驗證模式并徘,最后還有總線分發(fā),然后就是異常通知等等扰魂,就像是一場軍事戰(zhàn)斗中的過程:
這里說的命令是動作的意思麦乞,是用戶發(fā)出的一個請求(從前臺向后端),當然你也可以理解是改領(lǐng)域模型下的命令動作(從內(nèi)到外)劝评,還記得我們說到的讀寫分離CQRS么姐直,就是Command。
每一個個小的戰(zhàn)役(領(lǐng)域模型)蒋畜,都會有自己戰(zhàn)場的一些信息和動作數(shù)據(jù)(視圖模型)声畏,當然這里有正常的消息,也有惡性攻擊或者不當?shù)牟僮鳎恳粋€動作執(zhí)行都是一個前鋒部隊(領(lǐng)域命令模型)插龄,先鋒部隊把這些數(shù)據(jù)打包愿棋,加上時間戳等標識,生成命令標簽均牢,這個時候通過總線指揮官(中介者)糠雨,交給參謀來處理數(shù)據(jù)命令(領(lǐng)域驗證),進行安全甄別徘跪,將正常的甘邀、正確的往下傳遞,傳給司令部(持久化)垮庐,如果是惡性的錯誤信息松邪,則通過通訊兵打包給前線(通知),每次前線執(zhí)行操作突硝,只需要看看是否有通訊兵是否有錯誤異常提醒测摔,如果沒有則證明執(zhí)行成功。
當然還有事件回溯和事件源解恰,我會在以后文章說明,不知道這個栗子是否合理浙于,如果大家看不懂也沒關(guān)系护盈,或者請下邊留言,我們一起討論討論羞酗。
更新
有的小伙伴腐宋,可能看本文或者其他的概念的時候,比較懵懂檀轨,我這里根據(jù)自己的理解胸竞,簡單畫了個草圖,當然等系列結(jié)束的時候参萄,還是有完整的卫枝,這里先來一個簡單的:
零、今天實現(xiàn)棕色的部分
一讹挎、領(lǐng)域命令Commands —— 領(lǐng)域模型的先鋒官
說到這個領(lǐng)域命令校赤,大家肯定不會陌生,或者說應該是在哪里見過筒溃,沒錯马篮!就是我們在上上一篇《七 ║項目第一次實現(xiàn) & CQRS初探》中,說到的讀寫分離 CQRS 中的C —— Commend命令怜奖,這里我簡單說下浑测,為什么叫先鋒官,我們把整個項目比作一個戰(zhàn)場的化歪玲,前端一直和后端進行交互 —— 表單提交迁央,這個時候怎顾,肯定就離不開查詢和命令,查詢這里暫時先不說漱贱,就說一下這個命令槐雾,前端的任何一個動作其實都是一個事件。
大家肯定知道從前臺DTO拿到的實體模型數(shù)據(jù)幅狮,肯定不能直接操作領(lǐng)域模型(當然現(xiàn)在我們是直接這么操作的募强,直接用的是視圖模型和領(lǐng)域模型進行交互操作,這個時候領(lǐng)域模型就起到了一個沖鋒陷陣的作用了崇摄,其實這種設計不符合DDD領(lǐng)域設計的思想擎值,因為領(lǐng)域模型是一切的核心,它應該是一個個司令部逐抑,不能參與到前線鸠儿,他會下發(fā)出一個個的命令模型去執(zhí)行),這個時候我們的命令模型就出現(xiàn)了厕氨,他充當著從前臺到后臺的先鋒官的作用进每,執(zhí)行一個個的命令指令,完成從視圖模型到領(lǐng)域模型的操作和數(shù)據(jù)的過度作用命斧。
然后再通過中介者模式田晚,通過事件總線,通過領(lǐng)域命令一一分發(fā)出去国葬,然后通過驗證贤徒,最后是實現(xiàn)(比如持久化等),然后將中間產(chǎn)生的錯誤信息汇四,或者通知信息接奈,再扔給了前臺,所以說通孽,領(lǐng)域命令就是一個先鋒官序宦,這里你也看到了,他是一個個先鋒官利虫,他的作用是起到引導的作用挨厚,是下達命令的作用,他是不負責具體的邏輯實現(xiàn)的糠惫,具體是為什么呢疫剃,先按下不表歪今。咱們先看看如何定義一個領(lǐng)域命令绊袋。
希望上邊的三段話大家可以幫忙想一想乏德,如果想通了,但是和我寫的不一樣搂漠,請一定要留言芝发!
1霞掺、創(chuàng)建命令抽象基類
在 Christ3D.Domain.Core 領(lǐng)域核心層中秘蛇,新建Commands文件夾,并該文件夾下創(chuàng)建抽象命令基類 Command碉克,這里可能有小伙伴會問凌唬,這個層的作用,我就簡單再說下漏麦,這個層的作用是為了定義核心的領(lǐng)域知識的客税,說人話就是很多基類,比如 Entity 是領(lǐng)域模型的基類撕贞,ValueObject 是值對象的基類更耻,這里的Command 是領(lǐng)域命令的基類,當然捏膨,你也可以把他放到領(lǐng)域?qū)又醒砭靡粋€ Base 文件夾來表示,這小問題就不要爭議了号涯。
namespace Christ3D.Domain.Core.Commands
{ /// <summary>
/// 抽象命令基類 /// </summary>
public abstract class Command
{ //時間戳
public DateTime Timestamp { get; private set; } //驗證結(jié)果目胡,需要引用FluentValidation
public ValidationResult ValidationResult { get; set; } protected Command()
{
Timestamp = DateTime.Now;
} //定義抽象方法,是否有效
public abstract bool IsValid();
}
}
思考:為什么要單單頂一個抽象方法 IsValid()诚隙;
2讶隐、定義 StudentCommand ,領(lǐng)域命令模型
上邊的領(lǐng)域基類建好以后久又,我們就需要給每一個領(lǐng)域模型,建立領(lǐng)域命令了效五,這里有一個小小的繞地消,你這個時候需要靜一靜,想一想畏妖,
1脉执、為什么每一個領(lǐng)域模型都需要一個命令模型。
2戒劫、為什么是一個抽象類半夷。
namespace Christ3D.Domain.Commands
{
/// <summary>
/// 定義一個抽象的 Student 命令模型 /// 繼承 Command /// 這個模型主要作用就是用來創(chuàng)建命令動作的,不是用來實例化存數(shù)據(jù)的迅细,所以是一個抽象類 /// </summary>
public abstract class StudentCommand : Command
{
public Guid Id { get; protected set; }//注意:set 都是 protected 的
public string Name { get; protected set; }
public string Email { get; protected set; }
public DateTime BirthDate { get; protected set; }
public string Phone { get; protected set; }
}
}
希望這個時候你已經(jīng)明白了上邊的兩個問題了巫橄,如果不是很明白,請再好好思考下茵典,如果已經(jīng)明白了湘换,請繼續(xù)往下走。
3、基于命令模型彩倚,創(chuàng)建各種動作指令
上邊的模型創(chuàng)造出來了筹我,咱們需要用它來實現(xiàn)各種動作命令了,比如 URD 操作帆离,肯定是沒有 C 查詢的蔬蕊。這里就重點說一下創(chuàng)建吧,剩下兩個都一樣哥谷。
namespace Christ3D.Domain.Commands
{ /// <summary>
/// 注冊一個添加 Student 命令 /// 基礎(chǔ)抽象學生命令模型 /// </summary>
public class RegisterStudentCommand : StudentCommand
{ // set 受保護岸夯,只能通過構(gòu)造函數(shù)方法賦值
public RegisterStudentCommand(string name, string email, DateTime birthDate, string phone)
{
Name = name;
Email = email;
BirthDate = birthDate;
Phone = phone;
} // 重寫基類中的 是否有效 方法 // 主要是為了引入命令驗證 RegisterStudentCommandValidation。
public override bool IsValid()
{
ValidationResult = new RegisterStudentCommandValidation().Validate(this);//注意:這個就是命令驗證呼巷,我們會在下邊實現(xiàn)它 return ValidationResult.IsValid;
}
}
}
這里你應該就能明白第一步的那個問題了吧:為什么要單單頂一個抽象方法 IsValid()囱修;
不僅僅是驗證當前命令模型是否有效(無效是指:數(shù)據(jù)有錯誤、驗證失敗等等)王悍,只有有效了才可以往下繼續(xù)走(比如持久化等 )破镰,還要獲取驗證失敗的情況下,收錄哪些錯誤信息压储,并返回到前臺鲜漩,這個就是
new RegisterStudentCommandValidation()
的作用。注意這里還沒有實現(xiàn)集惋,我們接下來就會實現(xiàn)它孕似。
添加學生命令寫完了,然后就是更新 UpdateStudentCommand 和 刪除 RemoveStudentCommand 了刮刑,這里就不多說了喉祭。
二、領(lǐng)域驗證Validations —— 領(lǐng)域模型的安保官
這里為啥要說是安保官(就是起的名字雷绢,要是不貼切可以留言)呢泛烙,因為這是從前臺 視圖模型 到 領(lǐng)域模型 的一個屏障,這個就不用解釋了翘紊,因為他就是一個驗證的作用蔽氨,當一個個命令執(zhí)行的時候,需要對數(shù)據(jù)進行處理帆疟,就好像前線先鋒部隊執(zhí)行一個個命令的時候鹉究,需要對一個個事件或者數(shù)據(jù)進行判斷,有些錯誤的踪宠,假的數(shù)據(jù)是不能傳達到領(lǐng)域模型中的自赔,而我們的先鋒官是不會處理這些的,他們只負責一個個命令的執(zhí)行殴蓬,驗證工作就交給了Validations匿级,而且是每一條命令都需要進行驗證蟋滴,這是肯定的。那如何創(chuàng)建基于命令的驗證Validations呢痘绎,請往下看津函。
1、基于StudentCommand 創(chuàng)建抽象驗證基類
在上邊的領(lǐng)域命令中孤页,我們定義一個公共的抽象命令基類尔苦,在驗證中,F(xiàn)luentValidation已經(jīng)為我們定義好了一個抽象基類 AbstractValidator行施,所以我們只需要繼承它就行允坚。
namespace Christ3D.Domain.Validations
{ /// <summary>
/// 定義基于 StudentCommand 的抽象基類 StudentValidation /// 繼承 抽象類 AbstractValidator /// 注意需要引用 FluentValidation /// 注意這里的 T 是命令模型 /// </summary>
/// <typeparam name="T">泛型類</typeparam>
public abstract class StudentValidation<T> : AbstractValidator<T> where T : StudentCommand
{ //受保護方法,驗證Name
protected void ValidateName()
{
//定義規(guī)則蛾号,c 就是當前 StudentCommand 類
RuleFor(c => c.Name)
.NotEmpty().WithMessage("姓名不能為空")//判斷不能為空稠项,如果為空則顯示Message
.Length(2, 10).WithMessage("姓名在2~10個字符之間");//定義 Name 的長度
} //驗證年齡
protected void ValidateBirthDate()
{
RuleFor(c => c.BirthDate)
.NotEmpty()
.Must(HaveMinimumAge)//Must 表示必須滿足某一個條件,參數(shù)是一個bool類型的方法鲜结,更像是一個委托事件
.WithMessage("學生應該14歲以上展运!");
} //驗證郵箱
protected void ValidateEmail()
{
RuleFor(c => c.Email)
.NotEmpty()
.EmailAddress();
} //驗證手機號
protected void ValidatePhone()
{
RuleFor(c => c.Phone)
.NotEmpty()
.Must(HavePhone)
.WithMessage("手機號應該為11位!");
} //驗證Guid
protected void ValidateId()
{
RuleFor(c => c.Id)
.NotEqual(Guid.Empty);
} // 表達式
protected static bool HaveMinimumAge(DateTime birthDate)
{
return birthDate <= DateTime.Now.AddYears(-14);
} // 表達式
protected static bool HavePhone(string phone)
{
return phone.Length == 11;
}
}
}
關(guān)于 FluentValidation 的使用精刷,這里就不多說了拗胜,大家可以自己使用,基本的也就是這么多了怒允,當然大家也可以自己寫一些復雜的運算埂软,這里要說的重點是,大家應該也已經(jīng)發(fā)現(xiàn)了纫事,每一個驗證方法都是獨立的勘畔,互不影響,就算是有一個出現(xiàn)錯誤(當然不是編譯錯誤)丽惶,也不會影響當前整個領(lǐng)域命令咖杂,也就等同于不影響當前事件操作,是不是和以前相比蚊夫,不僅方便而且安全性更高了。
這個時候我們定義了這個抽象的學生驗證基類懦尝,剩下的就是需要針對不同的知纷,每一個領(lǐng)域命令,設計領(lǐng)域驗證了陵霉。
2琅轧、實現(xiàn)各個領(lǐng)域命令模型的驗證操作
這里就簡單說一個添加學生的命令驗證,我們實現(xiàn) StudentValidation<RegisterStudentCommand> 踊挠,并初始化相應的命令乍桂,這里可以看到冲杀,我們可以很自由針對某一個命令,隨心隨意的設計不同的驗證睹酌,而且很好的進行管控权谁,比如以后我們不要對名字控制了,我們只需要去掉這個方法憋沿。亦或者我們以后不僅支持手機號旺芽,還支持座機,這里就可以簡單的增加一個即可辐啄。
namespace Christ3D.Domain.Validations
{ /// <summary>
/// 添加學生命令模型驗證 /// 繼承 StudentValidation 基類 /// </summary>
public class RegisterStudentCommandValidation : StudentValidation<RegisterStudentCommand> { public RegisterStudentCommandValidation()
{
ValidateName();//驗證姓名
ValidateBirthDate();//驗證年齡
ValidateEmail();//驗證郵箱
ValidatePhone();//驗證手機號 //可以自定義增加新的驗證
}
}
}
說到了這里采章,相信你應該也命令了領(lǐng)域驅(qū)動的第一個小部分了,就是我們的每一個操作是如何生成命令并進行驗證的壶辜,那聰明的你一定會問了悯舟,我們?nèi)绾尾僮鬟@些領(lǐng)域命令呢,總得有一個驅(qū)動程序吧砸民,它們自己肯定是不會運行的抵怎,不錯!請繼續(xù)往下看阱洪。
三便贵、運籌命令模型 —— 誰會是指揮官?
上邊也說到了視圖模型轉(zhuǎn)成命令模型冗荸,然后在命令模型中進行驗證承璃,現(xiàn)在問題來了,到底是誰在運籌著這些命令蚌本,說人話就是盔粹,是誰在調(diào)用著這些命令,如果你能看懂我說到程癌,那就恭喜你舷嗡,如果不是很懂,也沒關(guān)系嵌莉,今天咱們先不說這個指揮官进萄,今天先說說,我們平時是怎么玩兒的锐峭。
1中鼠、在 Action 中調(diào)用我們的領(lǐng)域命令
[HttpPost]
[ValidateAntiForgeryToken] public ActionResult Create(StudentViewModel studentViewModel)
{ try {
ViewBag.ErrorData = null; // 視圖模型驗證
if (!ModelState.IsValid) return View(studentViewModel); //添加命令驗證,采用構(gòu)造函數(shù)方法實例
RegisterStudentCommand registerStudentCommand = new RegisterStudentCommand(studentViewModel.Name, studentViewModel.Email, studentViewModel.BirthDate, studentViewModel.Phone); //如果命令無效沿癞,證明有錯誤
if (!registerStudentCommand.IsValid())
{
List<string> errorInfo = new List<string>(); //獲取到錯誤援雇,請思考這個Result從哪里來的
foreach (var error in registerStudentCommand.ValidationResult.Errors)
{
errorInfo.Add(error.ErrorMessage);
} //對錯誤進行記錄,還需要拋給前臺
ViewBag.ErrorData = errorInfo; return View(studentViewModel);
} // 執(zhí)行添加方法
_studentAppService.Register(studentViewModel);
ViewBag.Sucesso = "Student Registered!"; return View(studentViewModel);
} catch (Exception e)
{ return View(e.Message);
}
}
這個很好理解椎扬,就是普通的調(diào)用惫搏,這里有兩個問題具温,可以有助于大家是否真正理解:
1、new RegisterStudentCommand() 為什么是構(gòu)造函數(shù)實例筐赔?
2铣猩、ValidationResult.Errors 錯誤信息是從哪里得到的?
如果這兩個看懂了川陆,給自己一個攢??吧剂习。這個時候,我們就需要把信息拋給前臺了较沪,怎么進行展示呢鳞绕,這里我用的是自定義視圖組件,如果你會可以快速看一遍尸曼,如果沒有用過们何,請仔細看看。
2控轿、自定義局部視圖頁面
添加一個視圖組件類
在 Web 根目錄下新建文件夾 ViewComponents冤竹,然后添加視圖組件類 AlertsViewComponent.cs
namespace Christ3D.UI.Web.ViewComponents
{ public class AlertsViewComponent : ViewComponent
{ /// <summary>
/// Alerts 視圖組件 /// 可以異步,也可以同步茬射,注意方法名稱鹦蠕,同步的時候是Invoke /// 我寫異步是為了為以后做準備 /// </summary>
/// <returns></returns>
public async Task<IViewComponentResult> InvokeAsync()
{ var notificacoes = await Task.Run(() => (List<string>)ViewBag.ErrorData); //遍歷錯誤信息,賦值給 ViewData.ModelState
notificacoes?.ForEach(c => ViewData.ModelState.AddModelError(string.Empty, c)); return View();
}
}
}
每一個視圖組件一個類在抛,固定寫法钟病,這個其實就像mvc的controller。那我們還需要配置 view刚梭,如何配置呢肠阱,請往下看。
設計視圖頁面
這里我是手動創(chuàng)建朴读,不知道有沒有快捷鍵屹徘,有知道的請留言哈
在 Views -> Shared 文件夾下,新建 Components\alerts\Default.cshtml 文件
@if (ViewData.ModelState.ErrorCount > 0)
{ <div class="alert alert-danger">
<button type="button" class="close" data-dismiss="alert">×</button>
<h3 id="msgRetorno">Alert! Something went wrong:</h3>
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
</div> }
@if (!string.IsNullOrEmpty(ViewBag.Sucesso))
{ <div class="alert alert-success">
<button type="button" class="close" data-dismiss="alert">×</button>
<h3 id="msgRetorno">@ViewBag.Sucesso</h3>
</div> }
在主頁面內(nèi)調(diào)用
這里有兩種辦法:
@* 將經(jīng)典驗證摘要替換為自定義視圖組件作為標記助手 *@
@*方式一(可用衅金,但不推薦) @await Component.InvokeAsync("Alerts")*@ <!-- 如果使用這個方法噪伊,請記得在_ViewImports.cshtml 中,導入@addTagHelper "*, Christ3D.UI.Web" -->
<vc:alerts />
我個人推薦使用第二種方法氮唯,注意 alerts酥宴,是我們的視圖名稱。
如果你想了解更多關(guān)于自定義視圖組件的知識您觉,可以查看官網(wǎng)
1、https://docs.microsoft.com/zh-cn/aspnet/core/mvc/views/view-components?view=aspnetcore-2.1
3授滓、瀏覽效果
這個時候琳水,我們已經(jīng)把視圖模型肆糕,命令模型,命令驗證等連接在一起在孝,也實現(xiàn)了我們的目的诚啃,看似很正常,其實問題還有很多:
這個指揮官真的指揮的很好么私沮?
為何contrller中還是會存在業(yè)務邏輯始赎?
等等。仔燕。造垛。
四、鳴金...
眼看時間已經(jīng)很晚晰搀,今天就暫時寫到這里了五辽。
這個時候你一定會發(fā)現(xiàn),這種異常數(shù)據(jù)的寫法真的很不舒服外恕,我們設計DDD領(lǐng)域驅(qū)動設計杆逗,目的就是為了要以領(lǐng)域為核心,把業(yè)務邏輯分離出去鳞疲,這個雖然用到了領(lǐng)域命令罪郊,和命令驗證,咋看分離出去了尚洽,但是調(diào)用的時候悔橄,還是沒有把視圖模型和命令模型穿起來,而且細心的你應該也發(fā)現(xiàn)了翎朱,我們的Service方法中橄维,還是使用的領(lǐng)域模型,這個是不對的拴曲。那我們?nèi)绾尾拍馨岩晥D模型争舞,領(lǐng)域模型,驗證模型和命令模型穿起來呢澈灼,又是如何很好的把獲取錯誤信息從controller撥離出來呢竞川,請聽下回分解~