驾一叶之扁舟 举匏樽以相属
寄蜉蝣于天地,渺沧海之一粟。哀吾生之须臾,羡长江之无穷。
挟飞仙以遨游,抱明月而长终。知不可乎骤得,托遗响于悲风。

从壹开始微服务 [ 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

 

posted @ 2018-11-28 12:53  老张的哲学  阅读(6126)  评论(16)    收藏  举报
作者:老张的哲学
好好学习,天天向上
返回顶部小火箭
好友榜:
如果愿意,把你的博客地址放这里
jianshu.com/u/老张
SqlSugar codeisbug.com
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy