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

     
     
  • 相关阅读:
    www.insidesql.org
    kevinekline----------------- SQLSERVER MVP
    Sys.dm_os_wait_stats Sys.dm_performance_counters
    如何使用 DBCC MEMORYSTATUS 命令来监视 SQL Server 2005 中的内存使用情况
    VITAM POST MORTEM – ANALYZING DEADLOCKED SCHEDULERS MINI DUMP FROM SQL SERVER
    Cargo, Rust’s Package Manager
    建筑识图入门(初学者 入门)
    Tracing SQL Queries in Real Time for MySQL Databases using WinDbg and Basic Assembler Knowledge
    Microsoft SQL Server R Services
    The Rambling DBA: Jonathan Kehayias
  • 原文地址:https://www.cnblogs.com/fan-yuan/p/3513944.html
Copyright © 2011-2022 走看看