美文网首页DDD领域驱动设计.NETDDD
从壹开始微服务 [ DDD ] 之十一 ║ 基于源码分析,命令分

从壹开始微服务 [ DDD ] 之十一 ║ 基于源码分析,命令分

作者: SAYLINING | 来源:发表于2018-11-29 17:05 被阅读3次

    缘起

    哈喽小伙伴周三好,老张又来啦,DDD领域驱动设计的第二个D也快说完了,下一个系列我也在考虑之中,是 Id4 还是 Dockers 还没有想好,甚至昨天我还想,下一步是不是可以写一个简单的Angular 入门教程,本来是想来个前后端分离的教学视频的,简单试了试,发现自己的声音不好听,真心不好听那种,就作罢了,我看博客园有一个大神在 Bilibili 上有一个视频,具体地址忘了,有需要的留言,我找找。不过最近年底了比较累了,目前已经写了15万字了(一百天,平均一天1500字),或者看看是不是给自己放一个假吧,自己也找一些书看一看,给自己充充电,希望大家多提一下建议或者帮助吧。

    言归正传,在上一篇文章中《之十 ║领域驱动【实战篇·中】:命令总线Bus分发(一)》,我主要是介绍了,如果通过命令模式来对我们的API层(这里也包括应用层)进行解耦,通过命令分发,可以很好的解决在应用层写大量的业务逻辑,以及多个对象之间混乱的关联的问题。如果对上一篇文章不是很记得了,我这里简单再总结一下,如果你能看懂这些知识点,并心里能大概行程一个轮廓,那可以继续往下看了,如果说看的很陌生,或者想不起来了,那请看上一篇文章吧。上篇文章有以下几个小点:

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

    2、MediatR 是如何实现中介者服务的?常用哪两种方法?(提示:请求/响应)

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

    这些知识点都是在上文中提到的,可能说的有点儿凌乱,不知道是否能看懂,上篇遗留了几个问题,所以我就新开了一篇文章,来重点对上一篇文章进行解释说明,大家可以看看是否和自己想的一样,欢迎来交流。

    当然还是每篇一问,也是本文的提纲:

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

    2、你自己能画一个详细的流程草图么?

    零、今天实现左下角浅紫色的下框部分

    image

    (昨天的故事中,说到了,咱们已经建立了一个基于 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() 方法。

    image

    下边就是整体流程,

    1、应用层的命令请求:

    // 领域命令请求
    Bus.SendCommand(registerCommand);
    

    2、领域命令的包装

    不知道大家还记得 MediatR 有哪两种常用方法,没错,就是**请求/响应 **Request/Response 和 发布 Publish这两种,咱们的命令是用的第一种方法,所以今天就先说说这个 Mediator.Send() 。咱们在中介内存总线InMemoryBus.cs 中,定义了SendCommand方法,是基于IMediator 接口的,今天咱们就把真实的方法拿出来:

    1、把源代码中 Internal 文件夹下的 RequestHandlerWrapper.cs 放到我们的基础设施层的 Christ3D.Infra.Bus 层中

    从这个名字 RequestHandlerWrapper 中我们也能看懂,这个类的作用,就是把我们的请求领域命令,包装成指定的命令处理程序。

    image

    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() ,对抽象类进行实现

    image image

    上图的过程是这样:

    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 类。

    image

    5、匹配具体的命令处理方法

    这个很简单,在第四步之后,紧接着就是自动寻找到了 Task<Unit> Handle(RegisterStudentCommand message, CancellationToken cancellationToken) 方法,整个流程就这么结束了。

    image

    现在这个流程你应该已经很清晰了,或者大概了解了整体过程,还有一个小问题就是,我们如何将错误信息收集的,在之前的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命令处理程序中调用

    image

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

    image

    4、效果浏览

    image

    整体流程就是这样:

    image

    四、结语

    上边的流程想必你已经看懂了,或者说七七八八,但是,至少你现在应该明白了,中介者模式,是如何通过命令总线Bus,把命令发出去,又是为什么在领域层的处理程序里接受到的,最后又是如何执行的,如果还是不懂,请继续看一看,或者结合代码,调试一下。我们可以这样来说,请求以命令的形式包裹在对象中,并传给调用者。调用者(代理)对象查找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令 。

    如果你看到这里了,那你下一节的领域事件,就很得心应手,这里有两个问题遗留下来:

    1、我们记录错误信息,缓存很不好,还需要每次清理,不是基于事务的,那如何替换呢?

    2、MediatR有两个常用方法,一个是请求/响应模式,另一个发布模式如何使用么?

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

    五、GitHub & Gitee

    https://github.com/anjoy8/ChristDDD

    https://gitee.com/laozhangIsPhi/ChristDDD

    --End

    相关文章

      网友评论

        本文标题:从壹开始微服务 [ DDD ] 之十一 ║ 基于源码分析,命令分

        本文链接:https://www.haomeiwen.com/subject/yzvfcqtx.html