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


緣起

哈嘍小伙伴周三好,老張又來啦,DDD領域驅動設計的第二個D也快說完了,下一個系列我也在考慮之中,是 Id4 還是 Dockers 還沒有想好,甚至昨天我還想,下一步是不是可以寫一個簡單的Angular 入門教程,本來是想來個前后端分離的教學視頻的,簡單試了試,發現自己的聲音不好聽,真心不好聽那種,就作罷了,我看博客園有一個大神在 Bilibili 上有一個視頻,具體地址忘了,有需要的留言,我找找。不過最近年底了比較累了,目前已經寫了15萬字了(一百天,平均一天1500字),或者看看是不是給自己放一個假吧,自己也找一些書看一看,給自己充充電,希望大家多提一下建議或者幫助吧。

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

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

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

3、工作單元是什么?作用?(提示:事務)

 

這些知識點都是在上文中提到的,可能說的有點兒凌亂,不知道是否能看懂,上篇遺留了幾個問題,所以我就新開了一篇文章,來重點對上一篇文章進行解釋說明,大家可以看看是否和自己想的一樣,歡迎來交流。

當然還是每篇一問,也是本文的提綱:

1、我們是如何把一個Command命令,一步步走到持久化的?

2、你自己能畫一個詳細的流程草圖么?

 

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

 

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

 

一、創建命令處理程序 CommandHandlers

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

1、添加一個命令處理程序基類 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;
        }
    }
}

這個還是很簡單的,只是提供了一個工作單元的提交,下邊會增加對領域通知的偽處理。

 

2、定義學生命令處理程序 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();
        }
    }
}

 

3、注入我們的處理程序

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

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

 

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

這里說的基於源碼,不是一字一句的講解,那要是我能說出來,我就是作者了😄,我就簡單的說一說,希望大家能看得懂。

0、下載 MediatR 源碼

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

1、可以在VS 中下載 ReSharper ,可以查看反編譯的所有代碼,注意會比以前卡一些。

2、直接查看Github ,https://github.com/jbogard/MediatR/tree/master/src/MediatR,現在開源的項目是越來越多,既然人家開源了,咱們就不能辜負了他們的開源精神,所以下載下來看一看也是很不錯。

本來我想把整個類庫,添加到咱們的項目中,發現有兼容問題,想想還是算了,就把其中幾個方法摘出來了,比如這個 Mediator.Send() 方法。

 

 

下邊就是整體流程,

1、應用層的命令請求:

// 領域命令請求
Bus.SendCommand(registerCommand);

 

2、領域命令的包裝

不知道大家還記得 MediatR 有哪兩種常用方法,沒錯,就是請求/響應 Request/Response 和 發布 Publish 這兩種,咱們的命令是用的第一種方法,所以今天就先說說這個 Mediator.Send() 。咱們在中介內存總線InMemoryBus.cs 中,定義了SendCommand方法,是基於IMediator 接口的,今天咱們就把真實的方法拿出來:


1、把源代碼中 Internal 文件夾下的 RequestHandlerWrapper.cs 放到我們的基礎設施層的 Christ3D.Infra.Bus 層中

從這個名字 RequestHandlerWrapper 中我們也能看懂,這個類的作用,就是把我們的請求領域命令,包裝成指定的命令處理程序。

 

2、修改我們的內存總線方法

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 呢,請繼續往下看。(你要是問我作者具體是咋封裝的,請看源碼,或者給他發郵件,說不定你還可以成為他的開發者之一喲 ~)

 

3、服務工廠調用指定的處理程序

 我們獲取到了 handler 以后,就去執行該處理程序

handler.Handle(request, cancellationToken, _serviceFactory);

 

我們看到 這個handler 還是一個抽象類 internal abstract class RequestHandlerWrapper<TResponse> ,接下來,我們就是通過 .Handle() ,對抽象類進行實現

上圖的過程是這樣:

1、訪問類方法  handler.Handle() ;

2、是一個管道處理程序,要包圍內部處理程序的管道行為,實現添加其他行為並等待下一個委托。

3、就是調用了這個匿名方法;

4、執行GetHandler() 方法;

 

其實從上邊簡單的看出來,就是實現了請求處理程序從抽象到實現的過程,然后添加管道,並下一步要對該處理程序進行實例化的過程,說白了就是把 RequestHandlerWrapper<RegisterStudentCommand> 給轉換成 IRequestHandler<RegisterStudentCommand>  的過程,然后下一步給 new 了一下。可是這個時候你會問,那實例化,肯定得有一個對象吧,這個接口自己肯定無法實例化的,沒錯!如果你能想到這里,證明你已經接近成功了,請繼續往下看。

 

4、通過注入,實例化我們的處理程序

在上邊的步驟中,我們知道了一個命令是如何封裝成了特定的處理程序接口,然后又是在哪里進行實例化的,但是具體實例化成什么樣的對象呢,就是在我們的 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 類。

 

 

5、匹配具體的命令處理方法

這個很簡單,在第四步之后,緊接着就是自動尋找到了 Task<Unit> Handle(RegisterStudentCommand message, CancellationToken cancellationToken) 方法,整個流程就這么結束了。

 

現在這個流程你應該已經很清晰了,或者大概了解了整體過程,還有一個小問題就是,我們如何將錯誤信息收集的,在之前的Controller 里寫業務邏輯的時候,用的是 ViewBag,那類庫是肯定不能這么用的,為了講解效果,我暫時用緩存替換,明天我們會用領域事件來深入講解。

 

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

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

1、命令處理程序基類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);
}

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

 

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

/// <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,把命令發出去,又是為什么在領域層的處理程序里接受到的,最后又是如何執行的,如果還是不懂,請繼續看一看,或者結合代碼,調試一下。我們可以這樣來說,請求以命令的形式包裹在對象中,並傳給調用者。調用者(代理)對象查找可以處理該命令的合適的對象,並把該命令傳給相應的對象,該對象執行命令 。

如果你看到這里了,那你下一節的領域事件,就很得心應手,這里有兩個問題遺留下來:

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

2、MediatR有兩個常用方法,一個是請求/響應模式,另一個發布模式如何使用么?

如果你很好奇,那就請看下回分解吧~~ 

 

五、GitHub & Gitee

https://github.com/anjoy8/ChristDDD

https://gitee.com/laozhangIsPhi/ChristDDD 

 

--End

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM