从壹开始微服务 [ DDD ] 之十 ║领域驱动【实战篇·中】:命令总线Bus分发(一)

烽火

哈喽大家好,老张又见面了,这两天被各个平台的“鸡汤贴”差点乱了心神,博客园如此,简书亦如此,还好群里小伙伴及时提醒,路还很长,这些小事儿就随风而去吧,这周本不打算更了,但是被群里小伙伴“催稿”了,至少也是对我的一个肯定吧,又开始熬夜中,请@初久小伙伴留言,我不知道你的地址,就不放链接了。

收住,言归正传,上次咱们说到了领域命令验证《九 ║从军事故事中,明白领域命令验证(上)》,也介绍了其中的两个角色——领域命令模型和命令验证,这些都是属于领域层的概念,当然这里的内容是 命令 ,查询就当然不需要这个了,查询的话,直接从仓储中获取值就行了,很简单。也没人问我问题,那我就权当大家已经对上篇都看懂了,这里就不再赘述。不知道大家是否还记得上篇文章末尾,提到的几个问题,我这里再提一下,就是今天的提纲了,如果你今天看完本篇,这几个问题能回答上来,那恭喜,你就明白了今天所讲的问题:

1、命令模型RegisterStudentCommand 放到 Controller 中真的好么?//我们平时都是这么做的

2、如果不放到Controller里调用,我们如果调用?在 Service里么?//也是一个办法,至少Controller干净了,但是 Service 就重了

3、验证的结果又如何获取并在前台展示呢?//本文会先用一个错误的方法来说明问题,下篇会用正确的

4、如何把领域模型 Student 从应用层 StudentAppService 解耦出去( Register()方法中 )。//本文重点,中介者模式

 好啦,简单先写这四个问题吧,这个时候你可以先不要从 Github 上拉取代码,先看着目前手中的代码,然后思考这四个问题,如果要是自己,或者咱们以前是怎么做的,如果你看过以后会有一些新的认识和领悟,请帮忙评论一下,捧个人场嘛,是吧😀。好啦,今天的东西可能有点儿多,请做好大概半个小时的准备,当然这半个小时你需要思考,要是走马观花,肯定是收获没有那么多的,代码已经更新了,记得看完的时候 pull 一下代码。

 

读前必读

1、本文中可能会涉及比较多的依赖注入,请一定要看清楚,因为这是第二个系列了,有时候小细节就不点明了,需要大家有一定的基础,可以看我第一个系列。

2、这三篇核心内容,都是重点在领域层,请一定要多思考。

3、文章不仅有代码,更多的是理解,比如用联合国的栗子来说明中介者模式,请务必要多思考。

 

 

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

 

 一、什么是中介者模式?

1、中介模式的概念

 这个其实很好理解,单单从名字上大家也都能理解它是一个什么模式,因为本文的重点不是一个讲解什么是23种设计模式的,大家有兴趣的可以好好的买本书,或者找找资料,好好,主要是思想,不需要自己写一个项目,如果大家有需要,可以留言,我以后单写一篇文章,介绍中介者模式。

这里就摘抄一段定义吧:

中介者模式是一个行为设计模式,它允许我们公开一个统一的接口,系统的 不同部分 可以通过该接口进行 通信,而 不需要 显示的相互作用;

适用场景:如果一个系统的各个组件之间看起来有太多的直接关系(就比如我们系统中那么多模型对象,下边会解释),这个时候则需要一个中心控制点,以便各个组件可以通过这个中心控制点进行通信;

该模式促进松散耦合的方式是:确保组件的交互是通过这个中心点来进行处理的,而不是通过显示的引用彼此;

比如系统和各个硬件,系统作为中介者,各个硬件作为同事者,当一个同事的状态发生改变的时候,不需要告诉其他每个硬件自己发生了变化,只需要告诉中介者系统,系统会通知每个硬件某个硬件发生了改变,其他的硬件会做出相应的变化; 

这样,之前是网状结构,现在变成了以中介者为中心的星星结构:

 

是不是挺像一个容器的,他自己把控着整个流程,和每一个对象都有或多或少,或近或远的联系,多个对象之间不用理睬其他对象发生了什么,只是负责自己的模块就好,然后把消息发给中介者,让中介者再分发给其他的具体对象,从而实现通讯 —— 这个思想就是中介者的核心思想,而且也是DDD领域驱动设计的核心思想之一( 还有一个核心思想是领域设计的思想 ),这里你可能还是不那么直观,我刚刚花了一个小时,对咱们的DDD框架中的中介者模式画了一个图,相信会有一些新的认识,在下边第 3 点会看到,请耐心往下看。

 

2、中介模式的原理

这里有一个联合国的栗子,也是常用来介绍和解释中介者模式的栗子:

 

抽象中介者(AbstractMediator):定义中介者和各个同事者之间的通信的接口;//比如下文提到的 抽象联合国机构

抽象同事者(AbstractColleague):定义同事者和中介者通信的接口,实现同事的公共功能;//比如下文中的 抽象国家

中介者(ConcreteMediator):需要了解并且维护每个同事对象,实现抽象方法,负责协调和各个具体的同事的交互关系;//比如下文中的 联合国安理会

同事者(ConcreteColleague):实现自己的业务,并且实现抽象方法,和中介者进行通信;//比如下文的 美国、英国、伊拉克等国家

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

还是希望大家能好好看看,好好想想,如果你还没有接触过这个中介者模式,如果了解并使用过,就简单看一看,要是你能把这个小栗子看懂了,那下边的内容,就很容易了,甚至是以后的内容就如鱼得水了,毕竟DDD领域驱动设计两个核心就是:CQRS读写分离 + 中介者模式 。

这个下边是一个简单的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);
        }
    }
}

 

 最终的结果是:

从这个小栗子中,也许你能看出来,美国和伊拉克之间,对象之间并没有任何的交集和联系,但是他们之间却发生了通讯,各自独立,但是又相互通讯,这个不就是很好的实现了解耦的作用么!一切都是通过中介者来控制,当然这只是一个小栗子,咱们推而广之:

命令模式、消息通知模型、领域模型等,内部运行完成后,将产生的信息抛向给中介者,然后中介者再根据情况分发给各个成员(如果又需要的),这样就实现多个对象的解耦,而且也达到同步的作用,当然还有一些辅助知识:异步、注入、事件等,咱们慢慢学习,至少现在中介者模式的思想和原理你应该都懂了。

 

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

 相信如果你是从我的第一篇文章看下去的,一定会以下几个模型很熟悉:视图模型、领域模型、命令模型、验证(上次说的)、还有没有说到的通知模型,如果你对这几个名称还很朦胧,请现在先在脑子里仔细想一想,不然下边的可能会乱,如果你一看到名字就能理解都是干什么的,都是什么作用,那好,请看下边的关系图。

首先咱们看看,如果不适用中介者模式,会是什么状态:

 

这个时候你会说,不!我不信会这么复杂!是真的么?我们的视图模型肯定和命令模型有交互吧,命令模型和领域模型肯定也有吧,那命令中有错误信息吧,肯定要交给通知模型的,说到这里,你应该会感觉可能真的有一些复杂的交互,当然!也可能没有那么复杂,我们平时就是一个实体 model 走天下的,错误信息随便返回给字符串呀,等等诸如此类。

 

如果你承认了这个结构很复杂,那好!咱们看看中介者模式会是什么样子的,可能你看着会更复杂,但是会很清晰:

 

 

(这可是老张花了一个小时画的,兄弟给个赞👍吧)

不知道你看到这里会不会脑子一嗡,没关系,等这个系列说完了,你就会明白了,今天咱们就主要说的是其中一个部分,命令总线 Command Bus、命令处理程序、工作单元的提交 这三块:

 

 从上边的大图中,我们看到,本来交织在一起的多个模型,本一条虚拟的流程串了起来,这里边就包括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包。注意安装包后,需要编译下当前项目

 

新建一个类库 Christ3D.Infra.Bus 

当然你也可以把它和接口 IMediatorHandler 放在一起,不过我个人感觉不是很舒服,因为这个具体的实现过程,不是我们领域设计需要知道的,就好像我们的 EFCore 仓储,我们就是在领域层,建立了仓储接口,然后再在基础设施数据层 Christ3D.Infrastruct.Data 中实现的,所以为了保持一致性,我就新建了这个类库项目,用来实现我们的中介处理程序接口。

注意下,Bus总线类库是需要引用 Domain.Core 核心领域层的,所以我们以后在 Domain领域层,直接引用 Bus总线层即可。

 

实现我们的中介处理程序接口

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看看这个方法:

 可以看到 send 方法的入参,必须是MediarR指定的 IRequest 对象,所以,我们需要给我们的 Command命令基类,再继承一个抽象类:

这个时候,我们的中介总线就搞定了。

 

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

1、把领域命令模型 从 controller 中去掉

只需要一个service调用即可

 

 这个时候我们文字开头的第一个问题就出现了,我们先把 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