• [转] (CQRS)命令和查询责任分离架构模式(二) 之 Command的实现


    CQRS

    概述

    继续引用上篇文章中的图片(来源于Udi Dahan博客),UI中的写入操作都将被封装为一个命令中,发送给Domain Model来处理。

    我们遵循Domain Driven Design的设计思想,因此所有的业务逻辑都只在Domain Model中处理,Command中将不会带有业务逻辑。Command中的代码无非是通过Repository获取某些个聚合根(Aggregate Root),然后将操作委托给相应的领域对象或领域服务来处理,仅此而已。

    实现

    实现上,我们会涉及三个东西:

    (1) Command对象

    Command对象的作用是用来封装命令数据,所以这类对象以属性为主,少量简单方法,但注意这些方法中不能包含业务逻辑。

    举个用户注册的例子,用户注册是一个命令,所以我们需要一个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 Gender Gender { get; set; }

    public RegisterCommand()
    {
    }
    }
    复制代码


    这个类的每个属性基本上都对应着注册表单中的一个输入(为了方便起见,上面的每个属性都是public set,但若属性不多不影响编码,最好把属性都改成private set,然后将属性的值通过构造函数传入)。当用户点击“注册”按钮时,Controller(假设使用MVC作为表现层模式)中会创建一个RegisterCommand的实例,设置相应的值,然后调用CommandBus.Send(registerCommand),然后根据执行的情况显示相应的信息给用户。(CommandBus后面会讲到)

    (2) CommandExecutor

    CommandExecutor的作用是执行一个命令,对于注册的例子,我们会有一个RegisterCommandExecutor的类,它只有一个Execute方法,接受RegisterCommand参数:

    复制代码
    public class RegisterCommandExecutor : ICommandExecutor<RegisterCommand>
    {
    private IRepository<User> _repository;

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

    public void Execute(RegisterCommand cmd)
    {
    if (String.IsNullOrEmpty(cmd.Email))
    throw new InvalidOperationException("Email is required.");

    if (cmd.Password != cmd.ConfirmPassword)
    throw new InvalidOperationException("Password not match.");

    // other "Command parameter" validations

    var service = new RegistrationService(_repository);
    service.Register(cmd.Email, cmd.NickName, cmd.Password, cmd.Gender);
    }
    }
    复制代码

    在Execute方法中,我们需要先验证Command的正确性,但需要注意的是,这里的验证只是验证RegisterCommand中的数据是否合法,并非验证业务逻辑。例如,这里会验证邮箱是否为空且格式是否正确,但邮箱格式正确并不意味着就可以注册,因为系统可能要求18岁以上的成年人才能注册,而这属于业务逻辑,RegistrationService将会负责确保所有的业务规则不被破坏,RegistrationService属于Domain Service,存在于Domain Model中。

    可以看到,CommandExecutor中主要有两部分工作,一是验证传入的Command对象是否合法,二是调用领域模型完成操作。上一篇文章中提到的Command是一个概念层次的Command,它不单指(1)中的Command,而是包含了(1)和(2)等。

    PS: 记得三四年前纠结于“三层架构”的时候,最搞不懂的应该算是“业务逻辑”了,现在似乎有点领悟。“业务逻辑”中关键的词是“业务”,这也是它和其它逻辑如应用逻辑区分开来的关键因素,如果一个逻辑带有“业务价值”,那它就算“业务”逻辑,否则就不算。比如下订单时,如果客户的退款次数超过100,那就不允许下单,这是业务逻辑;而"注册时两次输入的密码必须一致"则不算业务逻辑。但我仍有个问题,要求Email必须唯一算不算业务逻辑呢?我个人倾向于认为它是业务逻辑。那邮箱格式必须正确(即中间必须有@符号等等)算业务逻辑吗?个人倾向于认为是不算,如果不算业务逻辑,领域模型中需要对其进行验证吗?个人倾向于不用在领域模型中验证,这些逻辑应该在CommandExecutor中进行验证。不知道大家的看法如何?

    (3) Command Bus

    用于执行Command的是CommandExecutor,但CommandExecutor却并不用来在UI层调用,UI层中只会用到Command对象和即将提到的Command Bus。Command Bus的作用是将一个Command派发给相应的CommandExecutor去执行。在开发UI层时,我们不需要关心Command会被哪个Executor执行了,而只要知道,上帝赐予了我们一个CommandBus,我们只要创建好Command对象,扔给它,神奇的CommandBus就会帮我们把它执行完。这样一来,对于UI层的开发来说,所涉及的概念很简单,涉及的类也少,大部分的工作都是得到表单中的输入,封装成Command对象,扔给CommandBus。

    下面是注册的例子的Controller:

    复制代码
    public class AccountController : Controller 
    {
    [HttpPost]
    public ActionResult Register(RegisterCommand command)
    {
    if (ModelState.IsValid)
    {
    try
    {
    CommandBus.Execute(command);
    FormsAuthentication.SetAuthCookie(command.Email, false);

    return RedirectToAction("Index", "Home");
    }
    catch (Exception ex)
    {
    ModelState.AddModelError("Error", ex);
    }
    }

    return View(command);
    }
    }
    复制代码


    CommandBus的实现也很简单。首先,我们需要让CommandExecutor都实现一个泛型接口:

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

    其中ICommand是一个空接口,没有任何方法(即Marker Interface),它的作用是实现编译时约束,这样我们可以限制传入CommandExecutor的都是Command对象,而不是不小心传错的User对象(所有的Command对象都必须实现ICommand接口)。

    然后,把CommandBus写成这样:

    复制代码
    public static class CommandBus
    {
    public static void Send<TCommand>(TCommand cmd) where TCommand : ICommand {
    var type = typeof(TCommand);
    var executorType = FindExecutorType(type);
    var executor = Activator.CreateInstance(executorType);
    executor.Executor(cmd);
    }
    }
    复制代码

    在这个Send方法中,我们通过反射获取到泛型参数为传入的Command对象的具体类型的Executor类,再调用其Execute方法即可。上面的代码是伪代码,实际实现中我们可以通过IoC框架来简化这个过程,另外也可以做一些改进,例如将CommandBus设计为扩展点之一。另外我们还可以将UnitOfWork(相当于平常的EntityFramework中的IDbContext,Linq 2 SQL中的DataContext)的生命周期在CommandBus中进行控制。

    比较完整的CommandBus代码如下(仍有小部分伪代码):

    public interface ICommandBus
    {
    void Execute<TCommand>(TCommand cmd) where TCommand : ICommand;
    }
    复制代码
    public class DefaultCommandBus : ICommandBus
    {
    public void Send<TCommand>(TCommand cmd) where TCommand : ICommand
    {
    UnitOfWorkContext.StartUnitOfWork();

    var executor = ObjectContainer.Resolve<ICommandExecutor<TCommand>>();
    executor.Execute(cmd);

    UnitOfWorkContext.Commit();
    }
    }
    复制代码

    其它的代码不贴在文章中,所有代码可以文末处下载。

    这样我们就完成了CQRS中Command的一个基本实现。

    一些注意点

    (1) Command表示想要执行的命令,所以Command类的类名应当是动词的形式。例如RegisterCommand, ChangePasswordCommand等。不过Command后缀则是可选的,只要能保持一致即可。

    (2) Command和CommandExecutor是一一对应的。也就是说,一个Command只会对应一个CommandExecutor,这和后面的事件有区别,事件是一对多的,一个Event可以对应多个EventHandler。

    (3) 从文中的AccountController的Register Action中可以看到,Command对象也起到了DTO(Data Transfer Object,在这个例子中感觉称作View Model也无妨)的作用,这也是把Command和Executor相分离,不把Execute方法直接写在Command类中的原因之一。

    (4) 注意Command的类名的重要作用,每个Command类的名称都清晰地表达了一个意图,例如ChangePasswordCommand清晰的表达了这个命令是要修改密码,所以千万不要随意"复用"Command,这里的“复用”指的是,看到某两个Command中有完全一样的属性,就觉得没有必要使用两个Command,而把它们合并成一个Command,这样的"复用"会让系统变得越来越难以理解,虽然它可能的确减少了几行代码。

    (5) 命令通常是用“发送”来描述,而事件则是用“发布”来描述,所以CommandBus中的方法名称个人认为应该用Send比较合适,而不用Publish之类的。 

    代码下载

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

    说明:下载的代码和文章中的代码不完全一致,但也不会有太大差别。示例代码中只实现了Command和用户注册功能,其它的如事件之类皆未包含。

     

    PS: 关于技术文章的写作,我最怕的是自己的理解有偏差,以致于造成不好的影响,但不写又没有讨论。今晚突然想到一个自我感觉比较不错的建议:有兴趣的童鞋在阅读的过程中,若感觉某句或某观点不准确,可以以评论的形式提出,之后作者以不删原句的形式进行修改(将原句子用删除线划掉),这样既可以让文章变得更严谨,同时也会清楚的看到哪些观点经过了什么样的修正。

     
     
  • 相关阅读:
    Python--__init__方法
    Python--面向对象编程
    用R语言对NIPS会议文档进行聚类分析
    docker oracle install
    java 删除字符串左边空格和右边空格 trimLeft trimRight
    mysql 表名和字段、备注
    docker学习
    shell爬虫
    shell 解析json
    SecureCRT 7.1.1和SecureFx key 亲测可用
  • 原文地址:https://www.cnblogs.com/fan-yuan/p/3513944.html
Copyright © 2020-2023  润新知