CQRS实践(3): Command执行结果的返回

news/2024/7/1 3:11:34

上篇随笔讨论了CQRS中Command的一种基本实现。

面对UI中的各种命令,Controller会创建相应的Command对象,然后将其交给CommandBus,由CommandBus统一派发到相应的CommandExecutor中去执行,我们的ICommandBus的接口声明如下:

public interface ICommandBus
{
void Send<TCommand>(TCommand cmd) where TCommand : ICommand;
}

当在实际项目中应用CQRS时,我们会发现上面的做法存在一个问题:有时候我们希望Command在执行完后返回一些结果,但上面的Send方法返回void,也就意味着我们没有办法得到执行结果。我们以一个用户注册的例子来说明。

数据库中用户表(User)的定义为User (Id, Email, NickName, Password),括号中的为字段,其中Id为varchar(36)的Guid字符串。

注册用户的RegisterCommand代码如下:

public class RegisterCommand : ICommand
{
public string Email { get; set; }

public string NickName { get; set; }

public string Password { get; set; }

public string ConfirmPassword { get; set; }

public RegisterCommand()
{
}
}

假设在注册后需要将新注册用户的Id存到Session中,那Controller的实现就变得有点纠结:

[HttpPost]
public ActionResult Register(RegisterCommand command)
{
CommandBus.Send(command);

Session["CurrentUserId"] = ???

return Redirect("/");
}

上面的???处要怎么写?我们无法直接得到新注册用户的Id,因为Id只有在Command执行时才会生成。

所以只能在CommandBus.Send(command)的下一行添加一个查询:

[HttpPost]
public ActionResult Register(RegisterCommand command)
{
CommandBus.Send(command);

var newUserId = Query<User>().OrderByDescending(u => u.Id).Select(u => u.Id).First();

Session["CurrentUserId"] = newUserId;

return Redirect("/");
}

这样虽可以勉强解决问题,但比较繁琐,而且也无法保证在Send之后查询之前的这一小片时间里不会有其它新用户产生。于是,我们开始反思Command的设计......

解决方案1

这个方案也正是Sharp Architecture所采用的,即添加一个带两个泛型参数的ICommandExecutor<TCommand, TResult>接口,这样我们就有了两个ICommandExecutor接口:

public interface ICommandExecutor<TCommand>
where TCommand : ICommand
{
void Execute(TCommand cmd);
}

// 这是新添加的带有两个泛型参数的接口
public interface ICommandExecutor<TCommand, TResult>
where TCommand : ICommand
{
// Execute方法返回值变成TResult
TResult Execute(TCommand cmd);
}

为了适应这种变化,我们也需要相应修改ICommandBus的接口:

public interface ICommandBus
{
void Send<TCommand>(TCommand cmd) where TCommand : ICommand;

// 这个Send方法的返回值是TResult
TResult Send<TCommand, TResult>(TCommand cmd) where TCommand : ICommand;
}

看起来不错,现在对于注册用户的例子,我们只要调用第二个Send方法即可:

[HttpPost]
public ActionResult Register(RegisterCommand command)
{
var newUserId = CommandBus.Send<RegisterCommand, string>(command);

Session["CurrentUserId"] = newUserId;

return Redirect("/");
}

但如果我们仔细看看,会发现这是一个非常糟糕的设计!Controller的开发人员怎么知道RegisterCommand执行完会返回结果?怎么知道返回的是string而不是int?Controller中可以写成CommandBus.Send(command),也可以写成CommandBus.Send<RegisterCommand, int>(command),也可以写成CommandBus.Send<RegisterCommand, string>(command),同样是发送RegisterCommand命令,这三种调用全都可以编译通过,但是只有第三个才不会在运行时出现问题。

渐渐的,我们就会变成每调用一次CommandBus.Send()方法就要去查看对应的CommandExecutor是怎么实现的,这就让Command和CommandExecutor相分离的设计变得一点意义都没有。所以我们需要寻求其它的解决方案。

PS: Sharp Architecture中的ICommandHandler对应本文中的ICommandExecutor,ICommandProcessor对应本文中的ICommandBus,但我觉得它的ICommandProcessor的取名也太容易让人误解了,单从名字上看,谁能分清楚ICommandHandler和ICommandProcessor的区别?

解决方案2

其实这个方案非常简单:在Command对象中添加一个ExecutionResult的属性(这个属性要放在具体的Command类中,不要放于ICommand接口中)。如上面的用户注册的例子,我们可以添加一个RegisterCommandResult的类,然后将RegisterCommand改成如下所示:

public class RegisterCommand : ICommand
{
public string Email { get; set; }

public string NickName { get; set; }

public string Password { get; set; }

public string ConfirmPassword { get; set; }

// 亮点在这里
public RegisterCommandResult ExecutionResult { get; set; }

public RegisterCommand()
{
}
}

// 亮点在这里
public class RegisterCommandResult
{
public string GeneratedUserId { get; set; }
}

在调用CommandBus.Send()之前,我们完全不用理会这个ExecutionResult属性,对于Controller的开发人员来说,他只要知道在Command执行完后,ExecutionResult的值就会被赋上,如果没有,那就是CommandExecutor的bug。

而我们的RegisterCommandExecutor就可以改成(User类的构造函数会调用Id = Guid.NewGuid().ToString()对自己的Id进行赋值):

class RegisterCommandExecutor : ICommandExecutor<RegisterCommand>
{
public IRepository<User> _repository;

public RegisterCommandExecutor(IRepository<User> repository)
{
_repository = repository;
}

public void Execute(RegisterCommand cmd)
{
var service = new RegistrationService(_repository);
var user = service.Register(cmd.Email, cmd.NickName, cmd.Password);

// 亮点在这里
cmd.ExecutionResult = new RegisterCommandResult
{
GeneratedUserId = user.Id
};
}
}

然后Controller就很简单了:

[HttpPost]
public ActionResult Register(RegisterCommand command)
{
CommandBus.Send(command);

// 亮点在这里
Session["CurrentUserId"] = command.ExecutionResult.GeneratedUserId;

return Redirect("/");
}

这个方案和第一个方案的关键区别就在于,RegisterCommand中定义的ExecutionResult属性可以让开发人员清楚的知道这个属性会在Command执行完后被赋上合适的值。对于一个Command,如果开发人员在其中找到类似ExecutionResult这样的属性,他就知道这个Command执行完后会返回执行结果,并且结果是以赋值的形式赋给Command中的ExecutionResult属性,若Command中没有发现ExecutionResult这样的属性,那开发人员便知道这个Command执行完不会返回执行结果。

PS: 因为本例中User.Id采用的是Guid字符串,它可以在创建User对象时立刻生成,所以下载中的代码可以跑得还不错,但如果User.Id是使用SQL Server的自增长int类型,那就跑不了了,因为UnitOfWork是在Command执行完后才Commit的,所以,要处理自增Id的情况,我们需要稍微修改相应代码,比如将IUnitOfWork实例作为参数传给ICommandExecutor.Execute()方法,并把IUnitOfWork的提交转交给CommandExecutor负责,然后在对RegisterCommand.ExecutionResult属性赋值前先调用IUnitOfWork.Commit()方法,这样便可以解决问题。

到目前为止,我们所讨论的Command都是同步执行的,如果Command被设计为异步执行,那本文所讨论的内容便可以直接忽略。

如果系统的性能可以满足需求,同步Command无疑是最好的。

代码下载

http://files.cnblogs.com/mouhong-lin/CQRS-3.zip

转载于:https://www.cnblogs.com/mouhong-lin/archive/2012/03/29/cqrs-command-execution-result.html


http://lihuaxi.xjx100.cn/news/252716.html

相关文章

【组队学习】【28期】基于Python的会员数据化运营

基于Python的会员数据化运营 论坛版块&#xff1a; http://datawhale.club/c/team-learning/37-category/37 开源内容&#xff1a; https://github.com/datawhalechina/team-learning-data-mining/tree/master/MemberOperations 学习目标 数据化运营是业务知识与编程技能…

centos安装及网络配置

感谢老师传授&#xff0c;共同学习&#xff01;谢谢&#xff01;仅供自己日后复习之用&#xff01;centos安装关键点&#xff1a;创建分区&#xff1a;/ 系统分区/boot 启动分区SWAP 交换分区&#xff0c;虚拟内存。主要是缓解物理内存不足。虚拟化软件&#xff1a;VMware work…

【组队学习】【28期】基于transformers的自然语言处理(NLP)入门

基于transformers的自然语言处理(NLP)入门 论坛版块&#xff1a; http://datawhale.club/c/team-learning/39-category/39 开源内容&#xff1a; https://github.com/datawhalechina/Learn-NLP-with-Transformers 学习目标 自然语言处理&#xff08;Natural Language Pro…

IIS负载均衡-Application Request Route详解第三篇:使用ARR进行Http请求的负载均衡(上)...

IIS负载均衡-Application Request Route详解第三篇&#xff1a;使用ARR进行Http请求的负载均衡&#xff08;上&#xff09; 在前两篇文章中&#xff0c;我们已经讲述如何配置与安装ARR&#xff0c;从本篇文章开始&#xff0c;我们将重点的来讲述如何在使用ARR进行负载均衡。 本…

MongoDB 学习使用

博客教程&#xff1a; https://jingyan.baidu.com/article/dca1fa6f0428a4f1a440522e.html转载于:https://www.cnblogs.com/harlem/p/10148315.html

基于Guava实现的文件复制

需求&#xff1a;现需要将文件D:\A\B\C\abc.txt进行一下操作 1.在文件夹D:\A\B\C下&#xff0c;没有以abc命名的文件夹则创建 2.将目标文件D:\A\B\C\abc.txt复制到abc下 实现代码&#xff1a; /*** 以目标文件名创建文件夹&#xff0c;并将目标文件复制到该文件夹下** param sr…

【组队学习】【28期】Datawhale组队学习内容介绍

第28期 Datawhale 组队学习活动马上就要开始啦&#xff01; 本次组队学习的内容为&#xff1a; 吃瓜教程——西瓜书南瓜书李宏毅机器学习动手学数据分析集成学习SQL编程语言R语言数据科学基于Python的会员数据化运营数据采集从入门到精通基于transformers的自然语言处理(NLP)入…

CPLD的分频语言

分频器在FPGA/CPLD设计中是不可缺少的一部分&#xff0c;这就包括分频系数是奇数和偶数的&#xff08;我们称为奇分频和偶分频&#xff09;&#xff0c;而对于偶分频来说还有不同的分频方法&#xff0c;下面将给出具体的方法&#xff1a; 1、占空比不为50%的偶分频 占空比&…