從壹開始微服務 [ DDD ] 之十一 ║ 基於源碼分析,命令分發的過程(二)

緣起

哈嘍小夥伴週三好,老張又來啦,DDD領域驅動設計的第二個D也快說完了,下一個系列我也在考慮之中,是 Id4 仍是 Dockers 尚未想好,甚至昨天我還想,下一步是否是能夠寫一個簡單的Angular 入門教程,原本是想來個先後端分離的教學視頻的,簡單試了試,發現本身的聲音很差聽,真心很差聽那種,就做罷了,我看博客園有一個大神在 Bilibili 上有一個視頻,具體地址忘了,有須要的留言,我找找。不過最近年末了比較累了,目前已經寫了15萬字了(一百天,平均一天1500字),或者看看是否是給本身放一個假吧,本身也找一些書看一看,給本身充充電,但願你們多提一下建議或者幫助吧。html

言歸正傳,在上一篇文章中《之十 ║領域驅動【實戰篇·中】:命令總線Bus分發(一)》,我主要是介紹了,若是經過命令模式來對咱們的API層(這裏也包括應用層)進行解耦,經過命令分發,能夠很好的解決在應用層寫大量的業務邏輯,以及多個對象之間混亂的關聯的問題。若是對上一篇文章不是很記得了,我這裏簡單再總結一下,若是你能看懂這些知識點,並內心能大概行程一個輪廓,那能夠繼續往下看了,若是說看的很陌生,或者想不起來了,那請看上一篇文章吧。上篇文章有如下幾個小點:git

一、什麼是中介者模式?以及中介者模式的原理?(提示:多對象不依賴,但可通信)github

二、MediatR 是如何實現中介者服務的?經常使用哪兩種方法?(提示:請求/響應)後端

三、工做單元是什麼?做用?(提示:事務)緩存

 

這些知識點都是在上文中提到的,可能說的有點兒凌亂,不知道是否能看懂,上篇遺留了幾個問題,因此我就新開了一篇文章,來重點對上一篇文章進行解釋說明,你們能夠看看是否和本身想的同樣,歡迎來交流。服務器

固然仍是每篇一問,也是本文的提綱:app

一、咱們是如何把一個Command命令,一步步走到持久化的?前後端分離

二、你本身能畫一個詳細的流程草圖麼?異步

 

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

 

(昨天的故事中,說到了,我們已經創建了一個基於 MediatR 的在緩存中的命令總線,咱們能夠在任何一個地方經過該總線進行命令的分發,而後咱們在應用層 StudentAppService.cs 中,對添加StudentCommand進行了分發,那咱們到底應該如何分發,中介者又是如何調用的呢, 今天咱們就繼續接着昨天的故事往下說... )async

 

1、建立命令處理程序 CommandHandlers

我們先把處理程序作出來,具體是如何執行的,我們下邊會再說明。

一、添加一個命令處理程序基類 CommandHandler.cs

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;
        }
    }
}

這個仍是很簡單的,只是提供了一個工做單元的提交,下邊會增長對領域通知的僞處理。

 

二、定義學生命令處理程序 StudentCommandHandler.cs 

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領域驅動設計所帶來的不同的快感(第一次是領域、聚合、值對象等相關概念)。固然可能還不是很透徹,至少咱們已經經過第一條總線——命令總線,來實現了複雜多模型直接的通信了,下一篇咱們說領域事件的時候,你會更清晰。那聰明的你必定就會問了:

好吧,你說的這些我懂了,也大概知道了怎麼用,那它們是如何運行的呢?不知道過程,反而沒法理解其做用!沒錯,那接下來,咱們就具體說一說這個命令是如何分發的,請耐心往下看。

 

2、基於源碼分析命令處理過程

這裏說的基於源碼,不是一字一句的講解,那要是我能說出來,我就是做者了😄,我就簡單的說一說,但願你們能看得懂。

0、下載 MediatR 源碼

既然要研究源碼,這裏就要下載相應的代碼,這裏有兩個方式,

一、能夠在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,那類庫是確定不能這麼用的,爲了講解效果,我暫時用緩存替換,明天咱們會用領域事件來深刻講解。

 

3、用緩存來記錄錯誤通知

這裏僅僅是一個小小的亂入補充,上邊已經把流程調通了,若是你想看看什麼效果,這裏就出現了一個問題,咱們的錯誤通知信息沒有辦法獲取,由於以前咱們用的是ViewBag,這裏無效,固然Session等都無效了,由於咱們是在整個項目的多個類庫之間使用,只能用 Memory 緩存了。

一、命令處理程序基類CommandHandler 中,添加公共方法

//將領域命令中的驗證錯誤信息收集
//目前用的是緩存方法(之後經過領域通知替換)
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);
}

二、在Student命令處理程序中調用

 

三、自定義視圖模型中加載

/// <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中,每次把緩存清空。

 

四、效果瀏覽

 

總體流程就是這樣:

 

 

4、結語

 上邊的流程想必你已經看懂了,或者說七七八八,可是,至少你如今應該明白了,中介者模式,是如何經過命令總線Bus,把命令發出去,又是爲何在領域層的處理程序裏接受到的,最後又是如何執行的,若是仍是不懂,請繼續看一看,或者結合代碼,調試一下。咱們能夠這樣來講,請求以命令的形式包裹在對象中,並傳給調用者。調用者(代理)對象查找能夠處理該命令的合適的對象,並把該命令傳給相應的對象,該對象執行命令 。

若是你看到這裏了,那你下一節的領域事件,就很駕輕就熟,這裏有兩個問題遺留下來:

一、咱們記錄錯誤信息,緩存很很差,還須要每次清理,不是基於事務的,那如何替換呢?

二、MediatR有兩個經常使用方法,一個是請求/響應模式,另外一個發佈模式如何使用麼?

若是你很好奇,那就請看下回分解吧~~ 

 

5、GitHub & Gitee

https://github.com/anjoy8/ChristDDD

https://gitee.com/laozhangIsPhi/ChristDDD 

 

--End

相關文章
相關標籤/搜索