概述

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

 
 

[转] (CQRS)命令和查询责任分离架构模式(二) 之 Command的实现的更多相关文章

  1. [转] (CQRS)命令和查询责任分离架构模式(一) 之 什么是CQRS

    什么是CQRS? 这个问题网上可以找到很多资料,未接触过的童鞋请先查看Udi Dahan, Grey Young, Rinat Abdullin,园子里dax.net,以及Jdon社区上的相关文章. ...

  2. 转:命令和查询责任分离(CQRS)架构模式

    读了“蓝皮书”距今差不多一年,它改变了我的软件开发和构建软件架构观.在我作为一名程序员期间,我尝试了许多不同的方式来构建软件.方法有很多,包括一个贫血的域模型(Anemic Domain Model) ...

  3. 危险!水很深,让叔来 —— 谈谈命令查询权责分离模式(CQRS)

    多年以前,那时我正年轻,做技术如鱼得水,甚至一度希望自己能当一辈子的一线程序员. 但是我又有两个小愿望想要达成:一个是想多挣点钱:另一个就是对项目的技术栈和架构选型能多有点主动权. 多挣点钱是因为当时 ...

  4. 架构模式: 命令查询职责分离 (CQRS)

    架构模式: 命令查询职责分离 (CQRS) 问题 如何在微服务架构中实现查询 结论 将应用程序拆分为两部分:命令端和查询端.命令端处理创建,更新和删除请求,并在数据更改时发出事件.查询端通过对一个或多 ...

  5. Command and Query Responsibility Segregation (CQRS) Pattern 命令和查询职责分离(CQRS)模式

    Segregate operations that read data from operations that update data by using separate interfaces. T ...

  6. 云计算设计模式(六)——命令和查询职责分离(CQRS)模式

    云计算设计模式(六)——命令和查询职责分离(CQRS)模式 隔离,通过使用不同的接口,从操作读取数据更新数据的操作.这种模式可以最大限度地提高性能,可扩展性和安全性;支持系统在通过较高的灵活性,时间的 ...

  7. MySQL读写分离-架构

    MySQL读写分离-架构 简介 对于很多大型网站(pv值百万.千万)来说,在所处理的业务中,其中有70%的业务是查询(select)相关的业务操作(新闻网站,插入一条新闻.查询操作),剩下的则是写(i ...

  8. ElasticSearch实战系列十: ElasticSearch冷热分离架构

    前言 本文主要介绍ElasticSearch冷热分离架构以及实现. 冷热分离架构介绍 冷热分离是目前ES非常火的一个架构,它充分的利用的集群机器的优劣来实现资源的调度分配.ES集群的索引写入及查询速度 ...

  9. Oracle读写分离架构

    读写分离是架构分布式系统的一个重要思想.不少系统整体处理能力并不能同业务的增长保持同步,因此势必会带来瓶颈,单纯的升级硬件并不能一劳永逸.针对业务类型特点,需要从架构模式上进行一系列的调整,比如业务模 ...

随机推荐

  1. 接口加密《二》: API权限设计总结

    来源:http://meiyitianabc.blog.163.com/blog/static/105022127201310562811897/ API权限设计总结: 最近在做API的权限设计这一块 ...

  2. JQuery hover鼠标变换

    一般而言,我们为非按钮.链接等元素添加hover事件时,虽然能够处理悬停事件,但是鼠标却并没有变化,会造成悬停事件不明显的结果,为此,我们可以添加CSS样式cursor:pointer,使得该元素的悬 ...

  3. ubuntu实用命令--软件管理

    近期重新拿起linux的书看了下,整理了一下linux的命令. ubuntu预装了APT和dpkg ,“APT”是 “Advanced Package Tool”的简写,“dpkg ”是“Debian ...

  4. scala练手之数字转汉字小工具

    输入数字,转换成汉字,在统计数据量时很好用,而输入数字转成大写汉字,可以用于填写收据报销单哦 下载链接 https://pan.baidu.com/s/1nv3Ci6l 效果图如下: 直接上代码 ob ...

  5. 新手上路---Java学习知识梳理

    我随笔 我相信也有那么一些人跟我一样,脱下军装后对未来感到迷茫,不知所措甚至怀疑起自己的能力,自身体会的经历过才能明白其中的辛酸!归咎一切:心态,信心,自信!再穷不过乞讨,不死终将出头.好了,闲言碎语 ...

  6. Mysql存储过程使用LEAVE实现MSSQL存储过程中return语法

    DELIMITER $$ USE `qrsoft_dyj_db`$$ DROP PROCEDURE IF EXISTS `proc_withdraw_approve`$$ CREATE PROCEDU ...

  7. 重庆/北京/江苏KS/快乐时时/七星/福运来菠菜电商开奖修复APP网站SSC网站程序开发php

    网站制作是指使用标识语言(markup language),通过一系列设计.建模.和执行的过程将电子格式的信息通过互联网传输,最终以图形用户界面(GUI)的形式被用户所浏览.简单来说,网页设计的目的就 ...

  8. 电商的噩梦?实体商家的福音——VR全景智慧城市

    我们不知道未来网络购物的样子,但对当前电商平台的问题是清楚的.从消费者角度来看,网购的顾虑主要在于商品的质量难以保证.物流效率不够高,以及网络购物的"眼见不为实". 正因为可以很好 ...

  9. 全国各省市GeoCoord SQL文件(不包括区县)

    /* Navicat MySQL Data Transfer Source Server : 192.168.0.234 Source Server Version : 50543 Source Ho ...

  10. dubbo+zookeeper+springmvc+mybatis+shiro+redis架构

    内容管理(CMS)系统,包括内容管理,栏目管理.站点管理.公共留言.文件管理.前端网站展示等功能: 在线办公(OA)系统,主要提供简单的流程实例. Jeesz提供了常用工具进行封装,包括日志工具.缓存 ...