從壹開始微服務 [ DDD ] 之十 ║領域驅動【實戰篇·中】:命令總線Bus分發(一)

烽火

哈嘍大家好,老張又見面了,這兩天被各個平臺的“雞湯貼”差點亂了心神,博客園如此,簡書亦如此,還好群里小伙伴及時提醒,路還很長,這些小事兒就隨風而去吧,這周本不打算更了,但是被群里小伙伴“催稿”了,至少也是對我的一個肯定吧,又開始熬夜中,請@初久小伙伴留言,我不知道你的地址,就不放鏈接了。

收住,言歸正傳,上次咱們說到了領域命令驗證《九 ║從軍事故事中,明白領域命令驗證(上)》,也介紹了其中的兩個角色——領域命令模型和命令驗證,這些都是屬于領域層的概念,當然這里的內容是 命令 ,查詢就當然不需要這個了,查詢的話,直接從倉儲中獲取值就行了,很簡單。也沒人問我問題,那我就權當大家已經對上篇都看懂了,這里就不再贅述。不知道大家是否還記得上篇文章末尾,提到的幾個問題,我這里再提一下,就是今天的提綱了,如果你今天看完本篇,這幾個問題能回答上來,那恭喜,你就明白了今天所講的問題:

1、命令模型RegisterStudentCommand 放到 Controller 中真的好么?//我們平時都是這么做的

2、如果不放到Controller里調用,我們如果調用?在 Service里么?//也是一個辦法,至少Controller干凈了,但是 Service 就重了

3、驗證的結果又如何獲取并在前臺展示呢?//本文會先用一個錯誤的方法來說明問題,下篇會用正確的

4、如何把領域模型 Student 從應用層 StudentAppService 解耦出去( Register()方法中 )。//本文重點,中介者模式

好啦,簡單先寫這四個問題吧,這個時候你可以先不要從 Github 上拉取代碼,先看著目前手中的代碼,然后思考這四個問題,如果要是自己,或者咱們以前是怎么做的,如果你看過以后會有一些新的認識和領悟,請幫忙評論一下,捧個人場嘛,是吧??。好啦,今天的東西可能有點兒多,請做好大概半個小時的準備,當然這半個小時你需要思考,要是走馬觀花,肯定是收獲沒有那么多的,代碼已經更新了,記得看完的時候 pull 一下代碼。

讀前必讀

1、本文中可能會涉及比較多的依賴注入,請一定要看清楚,因為這是第二個系列了,有時候小細節就不點明了,需要大家有一定的基礎,可以看我第一個系列。

2、這三篇核心內容,都是重點在領域層,請一定要多思考。

3、文章不僅有代碼,更多的是理解,比如用聯合國的栗子來說明中介者模式,請務必要多思考。

零、今天實現左下角淺紫色的部分

image

一、什么是中介者模式?

1、中介模式的概念

這個其實很好理解,單單從名字上大家也都能理解它是一個什么模式,因為本文的重點不是一個講解什么是23種設計模式的,大家有興趣的可以好好的買本書,或者找找資料,好好,主要是思想,不需要自己寫一個項目,如果大家有需要,可以留言,我以后單寫一篇文章,介紹中介者模式。

這里就摘抄一段定義吧:

中介者模式是一個行為設計模式,它允許我們公開一個統一的接口,系統的 **不同部分 **可以通過該接口進行 通信,而 **不需要 **顯示的相互作用;

適用場景:如果一個系統的各個組件之間看起來有太多的直接關系(就比如我們系統中那么多模型對象,下邊會解釋),這個時候則需要一個中心控制點,以便各個組件可以通過這個中心控制點進行通信;

該模式促進松散耦合的方式是:確保組件的交互是通過這個中心點來進行處理的,而不是通過顯示的引用彼此;

比如系統和各個硬件,系統作為中介者,各個硬件作為同事者,當一個同事的狀態發生改變的時候,不需要告訴其他每個硬件自己發生了變化,只需要告訴中介者系統,系統會通知每個硬件某個硬件發生了改變,其他的硬件會做出相應的變化;

這樣,之前是網狀結構,現在變成了以中介者為中心的星星結構:

image

是不是挺像一個容器的,他自己把控著整個流程,和每一個對象都有或多或少,或近或遠的聯系,多個對象之間不用理睬其他對象發生了什么,只是負責自己的模塊就好,然后把消息發給中介者,讓中介者再分發給其他的具體對象,從而實現通訊 —— 這個思想就是中介者的核心思想,而且也是DDD領域驅動設計的核心思想之一( 還有一個核心思想是領域設計的思想 ),這里你可能還是不那么直觀,我剛剛花了一個小時,對咱們的DDD框架中的中介者模式畫了一個圖,相信會有一些新的認識,在下邊第 3 點會看到,請耐心往下看。

2、中介模式的原理

這里有一個聯合國的栗子,也是常用來介紹和解釋中介者模式的栗子:

抽象中介者(AbstractMediator):定義中介者和各個同事者之間的通信的接口;//比如下文提到的 抽象聯合國機構

抽象同事者(AbstractColleague):定義同事者和中介者通信的接口,實現同事的公共功能;//比如下文中的 抽象國家

中介者(ConcreteMediator):需要了解并且維護每個同事對象,實現抽象方法,負責協調和各個具體的同事的交互關系;//比如下文中的 聯合國安理會

同事者(ConcreteColleague):實現自己的業務,并且實現抽象方法,和中介者進行通信;//比如下文的 美國、英國、伊拉克等國家

注意:其中同事者是多個同事相互影響的才能叫做同事者;

還是希望大家能好好看看,好好想想,如果你還沒有接觸過這個中介者模式,如果了解并使用過,就簡單看一看,要是你能把這個小栗子看懂了,那下邊的內容,就很容易了,甚至是以后的內容就如魚得水了,畢竟DDD領域驅動設計兩個核心就是:CQRS讀寫分離 + 中介者模式 。

image

這個下邊是一個簡單的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);
        }
    }
}

最終的結果是:

image

從這個小栗子中,也許你能看出來,美國和伊拉克之間,對象之間并沒有任何的交集和聯系,但是他們之間卻發生了通訊,各自獨立,但是又相互通訊,這個不就是很好的實現了解耦的作用么!一切都是通過中介者來控制,當然這只是一個小栗子,咱們推而廣之:

命令模式、消息通知模型、領域模型等,內部運行完成后,將產生的信息拋向給中介者,然后中介者再根據情況分發給各個成員(如果又需要的),這樣就實現多個對象的解耦,而且也達到同步的作用,當然還有一些輔助知識:異步、注入、事件等,咱們慢慢學習,至少現在中介者模式的思想和原理你應該都懂了。

3、本項目是如何使用中介者模式的

相信如果你是從我的第一篇文章看下去的,一定會以下幾個模型很熟悉:視圖模型、領域模型、命令模型、驗證(上次說的)、還有沒有說到的通知模型,如果你對這幾個名稱還很朦朧,請現在先在腦子里仔細想一想,不然下邊的可能會亂,如果你一看到名字就能理解都是干什么的,都是什么作用,那好,請看下邊的關系圖。

首先咱們看看,如果不適用中介者模式,會是什么狀態:

image

這個時候你會說,不!我不信會這么復雜!是真的么?我們的視圖模型肯定和命令模型有交互吧,命令模型和領域模型肯定也有吧,那命令中有錯誤信息吧,肯定要交給通知模型的,說到這里,你應該會感覺可能真的有一些復雜的交互,當然!也可能沒有那么復雜,我們平時就是一個實體 model 走天下的,錯誤信息隨便返回給字符串呀,等等諸如此類。

如果你承認了這個結構很復雜,那好!咱們看看中介者模式會是什么樣子的,可能你看著會更復雜,但是會很清晰:

image

(這可是老張花了一個小時畫的,兄弟給個贊??吧)

不知道你看到這里會不會腦子一嗡,沒關系,等這個系列說完了,你就會明白了,今天咱們就主要說的是其中一個部分,**命令總線 Command Bus、命令處理程序、工作單元的提交 **這三塊:

image

從上邊的大圖中,我們看到,本來交織在一起的多個模型,本一條虛擬的流程串了起來,這里邊就包括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包。注意安裝包后,需要編譯下當前項目。

image

新建一個類庫 Christ3D.Infra.Bus

當然你也可以把它和接口 IMediatorHandler 放在一起,不過我個人感覺不是很舒服,因為這個具體的實現過程,不是我們領域設計需要知道的,就好像我們的 EFCore 倉儲,我們就是在領域層,建立了倉儲接口,然后再在基礎設施數據層 Christ3D.Infrastruct.Data 中實現的,所以為了保持一致性,我就新建了這個類庫項目,用來實現我們的中介處理程序接口。

注意下,Bus總線類庫是需要引用 Domain.Core 核心領域層的,所以我們以后在 Domain領域層,直接引用 Bus總線層即可。

image

實現我們的中介處理程序接口

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看看這個方法:

image

可以看到 send 方法的入參,必須是MediarR指定的 IRequest 對象,所以,我們需要給我們的 Command命令基類,再繼承一個抽象類:


image

這個時候,我們的中介總線就搞定了。

4、刪除命令模型在Controller中的使用

1、把領域命令模型 從 controller 中去掉

只需要一個service調用即可

image

這個時候我們文字開頭的第一個問題就出現了,我們先把 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個小時,真是很累心的一個過程,不過想想,哪怕有一個小伙伴能通過文字學到東西,也是極好極開心的,好啦,老張要睡覺了,至于文章的病句,截圖等,明天再調整吧。加油!

五、GitHub & Gitee

https://github.com/anjoy8/ChristDDD

https://gitee.com/laozhangIsPhi/ChristDDD

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容