哈喽你们好,老张又见面了,这两天被各个平台的“鸡汤贴”差点乱了心神,博客园如此,简书亦如此,还好群里小伙伴及时提醒,路还很长,这些小事儿就随风而去吧,这周本不打算更了,可是被群里小伙伴“催稿”了,至少也是对个人一个确定吧,又开始熬夜中,请@初久小伙伴留言,我不知道你的地址,就不放连接了。html
收住,言归正传,上次我们说到了领域命令验证《九 ║从军事故事中,明白领域命令验证(上)》,也介绍了其中的两个角色——领域命令模型和命令验证,这些都是属于领域层的概念,固然这里的内容是 命令 ,查询就固然不须要这个了,查询的话,直接从仓储中获取值就好了,很简单。也没人问我问题,那我就权当你们已经对上篇都看懂了,这里就再也不赘述。不知道你们是否还记得上篇文章末尾,提到的几个问题,我这里再提一下,就是今天的提纲了,若是你今天看完本篇,这几个问题能回答上来,那恭喜,你就明白了今天所讲的问题:git
一、命令模型RegisterStudentCommand 放到 Controller 中真的好么?//咱们平时都是这么作的github
二、若是不放到Controller里调用,咱们若是调用?在 Service里么?//也是一个办法,至少Controller干净了,可是 Service 就重了数据库
三、验证的结果又如何获取并在前台展现呢?//本文会先用一个错误的方法来讲明问题,下篇会用正确的设计模式
四、如何把领域模型 Student 从应用层 StudentAppService 解耦出去( Register()方法中 )。//本文重点,中介者模式api
好啦,简单先写这四个问题吧,这个时候你能够先不要从 Github 上拉取代码,先看着目前手中的代码,而后思考这四个问题,若是要是本身,或者我们之前是怎么作的,若是你看过之后会有一些新的认识和领悟,请帮忙评论一下,捧我的场嘛,是吧😀。好啦,今天的东西可能有点儿多,请作好大概半个小时的准备,固然这半个小时你须要思考,要是蜻蜓点水,确定是收获没有那么多的,代码已经更新了,记得看完的时候 pull 一下代码。缓存
一、本文中可能会涉及比较多的依赖注入,请必定要看清楚,由于这是第二个系列了,有时候小细节就不点明了,须要你们有必定的基础,能够看我第一个系列。安全
二、这三篇核心内容,都是重点在领域层,请必定要多思考。并发
三、文章不只有代码,更多的是理解,好比用联合国的栗子来讲明中介者模式,请务必要多思考。app
这个其实很好理解,单单从名字上你们也都能理解它是一个什么模式,由于本文的重点不是一个讲解什么是23种设计模式的,你们有兴趣的能够好好的买本书,或者找找资料,好好,主要是思想,不须要本身写一个项目,若是你们有须要,能够留言,我之后单写一篇文章,介绍中介者模式。
这里就摘抄一段定义吧:
中介者模式是一个行为设计模式,它容许咱们公开一个统一的接口,系统的 不一样部分 能够经过该接口进行 通讯,而 不须要 显示的相互做用;
适用场景:若是一个系统的各个组件之间看起来有太多的直接关系(就好比咱们系统中那么多模型对象,下边会解释),这个时候则须要一个中心控制点,以便各个组件能够经过这个中心控制点进行通讯;
该模式促进松散耦合的方式是:确保组件的交互是经过这个中心点来进行处理的,而不是经过显示的引用彼此;
好比系统和各个硬件,系统做为中介者,各个硬件做为同事者,当一个同事的状态发生改变的时候,不须要告诉其余每一个硬件本身发生了变化,只须要告诉中介者系统,系统会通知每一个硬件某个硬件发生了改变,其余的硬件会作出相应的变化;
这样,以前是网状结构,如今变成了以中介者为中心的星星结构:
是否是挺像一个容器的,他本身把控着整个流程,和每个对象都有或多或少,或近或远的联系,多个对象之间不用理睬其余对象发生了什么,只是负责本身的模块就好,而后把消息发给中介者,让中介者再分发给其余的具体对象,从而实现通信 —— 这个思想就是中介者的核心思想,并且也是DDD领域驱动设计的核心思想之一( 还有一个核心思想是领域设计的思想 ),这里你可能仍是不那么直观,我刚刚花了一个小时,对我们的DDD框架中的中介者模式画了一个图,相信会有一些新的认识,在下边第 3 点会看到,请耐心往下看。
这里有一个联合国的栗子,也是经常使用来介绍和解释中介者模式的栗子:
抽象中介者(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); } } }
最终的结果是:
从这个小栗子中,也许你能看出来,美国和伊拉克之间,对象之间并无任何的交集和联系,可是他们之间却发生了通信,各自独立,可是又相互通信,这个不就是很好的实现了解耦的做用么!一切都是经过中介者来控制,固然这只是一个小栗子,我们推而广之:
命令模式、消息通知模型、领域模型等,内部运行完成后,将产生的信息抛向给中介者,而后中介者再根据状况分发给各个成员(若是又须要的),这样就实现多个对象的解耦,并且也达到同步的做用,固然还有一些辅助知识:异步、注入、事件等,我们慢慢学习,至少如今中介者模式的思想和原理你应该都懂了。
相信若是你是从个人第一篇文章看下去的,必定会如下几个模型很熟悉:视图模型、领域模型、命令模型、验证(上次说的)、还有没有说到的通知模型,若是你对这几个名称还很朦胧,请如今先在脑子里仔细想想,否则下边的可能会乱,若是你一看到名字就能理解都是干什么的,都是什么做用,那好,请看下边的关系图。
首先我们看看,若是不使用中介者模式,会是什么状态:
这个时候你会说,不!我不信会这么复杂!是真的么?咱们的视图模型确定和命令模型有交互吧,命令模型和领域模型确定也有吧,那命令中有错误信息吧,确定要交给通知模型的,说到这里,你应该会感受可能真的有一些复杂的交互,固然!也可能没有那么复杂,咱们平时就是一个实体 model 走天下的,错误信息随便返回给字符串呀,等等诸如此类。
若是你认可了这个结构很复杂,那好!我们看看中介者模式会是什么样子的,可能你看着会更复杂,可是会很清晰:
(这但是老张花了一个小时画的,兄弟给个赞👍吧)
不知道你看到这里会不会脑子一嗡,不要紧,等这个系列说完了,你就会明白了,今天我们就主要说的是其中一个部分,命令总线 Command Bus、命令处理程序、工做单元的提交 这三块:
从上边的大图中,咱们看到,原本交织在一块儿的多个模型,本一条虚拟的流程串了起来,这里边就包括CQRS读写分离思想 和 中介者模型,固然还有人说是发布-订阅模型,这个我还在酝酿,之后的文章会说到。虽然对象仍是那么多,可是清晰了起来,多个对象之间也没有存在一个很深的联系,让业务之间更加专一自身业务。
若是你如今对中介者模式已经有了必定的意识,也知道了它的做用和意思,那它究竟是如何操做的呢,请耐心往外看,重点来了。
在咱们的核心领域层 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; } }
发布命令:就好像咱们调用某招聘平台,发布了一个招聘命令。
微软官方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); // "老张的哲学"
这里就不讲解为何要使用 MediatR 来实现咱们的中介者模式了,由于我没有找到其余的😂,具体的使用方法很简单,就和咱们的缓存 IMemoryCache 同样,经过注入,调用该接口便可,若是你仍是不清楚的话,先往下看吧,应该也能看懂。
注意:我这里把包安装到了Christ3D.Domain.Core 核心领域层了,由于还记得上边的那个大图么,我说到的,一条贯穿项目的线,因此这个中介处理程序接口在其余地方也用的到(好比领域层),因此我在核心领域层,安装了这个nuget包。注意安装包后,须要编译下当前项目。
更新:我放到了基础设施层了,新建一个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命令基类,再继承一个抽象类:
这个时候,咱们的中介总线就搞定了。
一、把领域命令模型 从 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 ,没有实现命令模型、领域模型等的交互通信。
说到这里,你可能脑子里有了一个大胆的想法,还记得上边说的中介者模式么,就是很好的实现了多个对象之间的通信,还不破坏各自的内部逻辑,使他们只关心本身的业务逻辑,那具体若是使用呢,请往下看。
经过构造函数注入咱们的中介处理接口,这个你们应该都会了吧
//注意这里是要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); }
最后记得要对服务进行注入,这里有两个点
一、ConfigureServices 中添加 MediatR 服务
// Adding MediatR for Domain Events // 领域命令、领域事件等注入 // 引用包 MediatR.Extensions.Microsoft.DependencyInjection services.AddMediatR(typeof(Startup));
二、在咱们的 NativeInjectorBootStrapper.cs 依赖注入文件中,注入咱们的中介总线接口
services.AddScoped<IMediatorHandler, InMemoryBus>();
老张说:这里的注入,就是指,每当咱们访问 IMediatorHandler 处理程序的时候,就是实例化 InmemoryBus 对象。
到了这里,咱们才完成了第一步,命令总线的定义,也就是中介处理接口的定义与使用,那具体是如何进行分发的呢,咱们又是如何进行数据持久化,保存数据的呢?请往下看,咱们先说下工做单元。
博主按:这是一个很丰富的内容,今天就不详细说明了,留一个坑,为之后23种设计模式的时候,再详细说明!
首先了解工做单元(Unit of Work)的意图:维护受业务影响的对象列表,而且协调变化的写入和解决并发问题。
能够用工做单元来实现事务,工做单元就是记录对象数据变化的对象。只要开始作一些可能对所要记录的对象的数据有影响的操做,就会建立一个工做单元去记录这些变化,因此,每当建立、修改、或删除一个对象的时候,就会通知工做单元。
一、在Christ3D.Domain 领域层的接口文件夹Interfaces种,新建工做单元接口 IUnitOfWork.cs
namespace Christ3D.Domain.Interfaces { /// <summary> /// 工做单元接口 /// </summary> public interface IUnitOfWork : IDisposable { //是否提交成功 bool Commit(); } }
二、在基础设施层,实现工做单元接口
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(); } } }
在原生依赖注入类 NativeInjectorBootStrapper.cs 中
services.AddScoped<IUnitOfWork, UnitOfWork>();
由于篇幅(太长了有些晕)和时间的问题,今天就暂时先说到这里,代码我已经写好了,而且提交到了Github,你们若是想看的能够先pull下来,至于为何这么用以及它的意义,我们下篇文章再详细说。其实总体流程和原理,我在上边也说的很详细了,若是你能根据联合国的栗子看懂这个(注意要结合与依赖注入来理解),那你就是完彻底全的理解了,若是下边的代码还不是很清楚,不要紧,周末你们先看看,下周我详细给你们讲解下。
我这里先给你们列举下三步走,为下次作准备:
一、添加一个命令处理程序基类 CommandHandler.cs
二、经过缓存Memory来记录通知信息(错误方法)
三、定义学生命令处理程序 StudentCommandHandler.cs
今天真没想到会写这么多,看来仍是夜里安静的时候更容易写东西,思路清晰,没办法,我只能把本文拆成两个文章了。这篇文章我是来来回回的删了写,写了删,一个下午+一个晚上,大概6个小时,真是很累心的一个过程,不过想一想,哪怕有一个小伙伴能经过文字学到东西,也是极好极开心的,好啦,老张要睡觉了,至于文章的病句,截图等,明天再调整吧。加油!