QCon 演讲火热征集中,快来分享技术实践与洞见! 了解详情
写点什么

命令模式:若只如“初见”

  • 2018-01-03
  • 本文字数:2713 字

    阅读完需:约 9 分钟

似曾相识

最近在 InfoQ 上看到一篇谈论命令模式与 CQRS 架构的译文《From CQS to CQRS》(建议先阅读此文,本文会针对该文的一些观点进行探讨),文章从命令模式谈起,然后提出了命令模式的升级版——命令总线

这个图总给人一种似曾相识的感觉,仔细回想了一下,发觉这不正是Struts2 架构中的核心部分吗?

作为一个基于命令模式的MVC 框架,Struts2 在对于命令的处理上和命令总线的设计如出一辙,它的 ActionInvocationInterceptor对应的正是命令总线里CommandBusDecorator。应该说两者是因为遵循了同样的 OO 设计准则才得到了如此高度的一致,可见优雅的设计都是相似的,模式之所以成为模式是有必然性的。

命令模式“错”了吗?

关于命令模式的详细介绍可以参考四人帮的《设计模式》和《Head First Design Patterns》,本文不做过多赘述。命令模式的核心设计用意是对调用端和执行端进行解耦,这种解耦是非常彻底的,即:在调用端不会出现任何执行端的 API,甚至在Command这个核心抽象上的execute方法上都是不带任何参数的:

复制代码
public interface Command {
public void execute();
}

这使得Command对于调用方而言完全是一个“黑盒”,调用方只知道下达命令,对于它将被如何执行毫不知情,命令的解释与执行是由命令和执行方共同完成的,这符合现实世界中很多事物之间的协作关系,确保了参与其中的各方职责单一,分工明确,否则角色混乱,各种事情搅在一起就会变成一团糟。

《From CQS to CQRS》一文为引出命令总线在介绍命令模式时阐述了它的一个"缺陷":即由于命令模式的“强”封装使得它不能很好地“包裹”数据,也就是命令参数。文章称这些经常变化的命令参数既不能通过execute 方法传递,又不适合作为命令类构造函数的参数,因此作者认为命令模式是“有问题”的,需要进行重构,而重构的结果就是命令总线。

然而文章对命令模式的diss 是站不住脚的,因为即使按照作者推荐的方式将所谓变化的部分(即“数据”)抽离到一个DTO 中,在调用端依然需要实例化它,为其设定各种参数,这些参数天然就是执行业务的前提,无论采用何种设计,作为下达命令的一方“把要干的事情讲明白”是最起码的“份内事”,所以在命令类的构造函数上传递参数和剥离到一个单独的DTO 中包裹数据没有任何本质的区别,后者的做法反而有“从富领域模型向贫血的领域模型开倒车”的嫌疑。

所以命令模式并没有错,命令总线也不是为解决命令模式所谓的“弊端”而来,它实际上是应更大的架构目标和应用场景而产生的。

更大的格局

原生的命令模式在它所适用的场景上表现自然是完美的,这些场景大多数是领域模型的一些“局部”,命令的类型和逻辑都是和业务紧密联系的。而另一方面,人们也认识到命令模式具有广泛的适用性,具备在更高级别的架构模式中扮演核心角色的能力,但是将命令模式提升到更加通用和完备的层面还需要解决以下一些问题:

1. 将命令的“数据”和“逻辑”剥离开,形成通用的“命令”和“命令处理机制”

在原生的命令模式里,每一个具体的命令类都会包含特定的字段和逻辑,通用化处理的第一步就需要把命令的数据和行为剥离开,数据剥离之后可以使用通用的数据结构如 Map 或更加抽象的类型如 Object 来替换,而行为上的通用化处理则要依靠下面几点来实现。

2. 抽象统一的命令处理流程

在一个特定的框架或业务系统里,命令的执行往往都有一定的“套路”,如果想让命令的执行通用化,势必要精心地总结和归纳各种命令在执行上的共性,提炼出一个通用的程序执行的“流程”,这个所谓的“流程”就是服务总线模式中的CommandBus和 Struts2 中的ActionInvocation,统一处理流程可以包含大量丰富的主题,比如日志、事务处理、安全拦截、性能跟踪、数据校验等等。

3. 基于配置的流程定义与组装

但是统一的处理流程并不意味着只能有一种,也不意味着一成不变,为了让流程处理具有广泛的适用性,通过配置的方式去定义和组装命令的处理流程是非常必要的,这样可以让流程变得灵活,可定制,流程中的环节也都是可插拔的,就如同Struts2使用struts.xml去描述interceptors栈和action那样。

4. 提供命令处理的公共基础设施

当统一的“流程”抽象出来之后,需要针对普遍存在的“环节”提供公共实现,例如前文命令总线上示意的LoggingDecoratorValidationDecorator等一系列的装饰器和 Struts2 中的loggervalidation等一系列的Interceptor,这些都会作为命令处理过程中的“公共基础设施”,一环一环地套接起来,让每一个命令逐一经过这些“环节”进行相应的处理。这种工作模式和面向切面编程中的“Around Advice”机制是完全一致的。

5. 给自定义命令处理逻辑留下接口

无论如何,这处理流程上的最后一环必定是留给命令“执行者”的,连同封装好的数据一起,落脚到一个回调的接口上,让命令“执行者”们补上属于它们的应尽之责:业务处理代码,则整个命令处理流程的“闭环”就算大功告成了。

原生的命令模式往往应用在领域模型上,与业务紧密关联,而命令总线的意图则是试图将命令模式提升到架构层面,在整个系统的某些“分层”(layer) 之间建立一种一致的全局的通信模式,从而实现“层间解耦”,例如像 Struts2 那样在 MVC 的视图层与模型层之间组织和传递 Action。为了实现这一目标,势必要对原生的命令模式进行改进,甚至是妥协,比如将命令的数据与行为进行拆分,这确实像是“从富领域模型向贫血的领域模型开倒车” ,但是为了实现更大的架构目标,局部的妥协是必须的,也是值得的。

终极产物

如从一条小溪最终汇入江河大海,命令模式被提升为命令总线之后进而又参与到了 CQRS 架构中,成为组成这一先进架构的核心模式之一,这也可以视为命令模式进化到现在的“终极产物”。CQRS 架构的核心思想是把系统和外界的信息交换进行了读写分离,在数据写入时,通过构建富领域模型进行业务计算,这是领域驱动设计擅长的领域,在这个过程中“命令”是驱动领域模型运转的钥匙。在数据读取时,CQRS 会绕过领域模型直接从持久层提取数据,这有助于提升性能,同时减轻领域模型的压力。

但是 CQRS 已经不再是本文关注的重点了,因为 CQRS 直接复用了命令总线,没有做其他的提升,本文写作的主要目的是想回顾命令模式从起源到终极产物的演化历程,阐述这些演化背后的真正用意以及实现这些目标的宝贵设计思想。

关于作者:耿立超,架构师,CSDN 博客专家,博客 http://blog.csdn.net/bluishglc 已从事多年大数据领域的研发工作,对企业级应用架构、SaaS、分布式存储和领域驱动设计有丰富的实践经验,喜欢摄影和旅行。


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2018-01-03 18:002736

评论

发布
暂无评论
发现更多内容

我赌一包辣条这是全网最详细的代码审计(没有之一)

网络安全学海

黑客 网络安全 信息安全 代码审计 漏洞分析

Ta想做一粒智慧的种子

白洞计划

架构训练营模块 1 作业 - 1班助教

听闻

模块8作业

杨彬

#架构实战营

图像的模板匹配,Python OpenCV 取经之旅第 29 天

梦想橡皮擦

7月日更

推荐系统的未来发展(三十三)

Databri_AI

价值观 推荐系统

公司内部使用的数仓开发规范

白贺BaiHe

数据仓库 开发规范 数仓规范 7月日更

【LeetCode】基于时间的键值存储Java题解

Albert

算法 LeetCode 7月日更

只更新代码,然后发布版本:基于 Serverless Devs 原子化操作阿里云函数计算

Serverless Devs

架构实战营-模块8作业-消息队列MySQL表格

Lane

在线脑图思维导图生成工具

入门小站

工具

Go 学习笔记之 Map

架构精进之路

Go 语言 7月日更

直接上干货!这些细节在Android面试上要注意了

欢喜学安卓

android 程序员 面试 移动开发

TEMS模型--衡量你的人生资源

俞凡

认知

网络攻防学习笔记 Day70

穿过生命散发芬芳

网络攻防 7月日更

王者荣耀商城异地多活架构设计

thewangzl

进来偷学一招,数据归档二三事儿

楼下小黑哥

Java 数据库 系统设计

暑假期间快手将重点整治平台:短视频平台如何完善内容审核机制

石头IT视角

全面了解Java并发编程基础!超详细!

程序员的时光

Java 并发编程

Kats-Facebook最新开源的时序分析工具

好孩子

PowerShell 正则表达式

耳东@Erdong

PowerShell 7月日更

业务架构模块8作业:设计消息队列存储消息数据的MySQL 表格

好吃不贵

正式加入字节跳动!如何才能更容易拿到大厂Offer

欢喜学安卓

android 程序员 面试 移动开发

Linux之find命令的参数详解

入门小站

Linux

第二周作业-熊猫潘戈项目利益相关方

小夏

产品经理训练营 邱岳

模块一作业

架构0期-Bingo

我为什么要学习业务建模?

escray

学习 极客时间 7月日更 如何落地业务建模

架构实战营 模块八课后作业

iProcess

架构实战营

架构实战营 - 模块 8- 作业

请弄脏我的身体

架构实战营

记录一次Neokylin_Server_V5系统已有分区的扩容操作

星河寒水

分区扩容

ACM金牌选手整理的【LeetCode刷题顺序】

编程熊

Java 面试 算法 面经 笔试

命令模式:若只如“初见”_架构_耿立超_InfoQ精选文章