耿大侠 Diss国外架构师文章《From CQS to CQRS》

看到耿大侠发了一篇怼InfoQ上的一篇译文的技术文章,仔细拜读之后,收获良多,在征得原作者的同意后,把文章发在这里,供大家品鉴,滋取养分。

大多数程序开发人员一定拜读过设计模式的相关文献,想必,也一定在自己的日常工作中,套用个别设计模式优化过自己的局部代码,在局部代码的优化上,设计模式是死的,当你把设计模式放在更宽广的领域,设计模式会变成活的。本文解析了命令模式从面向对象设计过渡到领域驱动设计的思维过程,为您打开视野提供帮助。下面让我们一起看下耿大侠的文章吧。

似曾相识

最近在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、分布式存储和领域驱动设计有丰富的实践经验,喜欢摄影和旅行。


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CQRS(Command Query Responsibility Segration)架构,大家应该不会陌生了。简单的说,就是一个系统,从架构上把它拆分为两部分:命令处理(写请求)+查询处理(读请求)。然后读写两边可以用不同的架构实现,以实现CQ两端(即Command Side,简称C端;Query Side,简称Q端)的分别优化。CQRS作为一个读写分离思想的架构,在数据存储方面,没有做过多的约束。所以,我觉得CQRS可以有不同层次的实现,比如: 1.CQ两端数据库共享,CQ两端只是在上层代码上分离;这种做法,带来的好处是可以让我们的代码读写分离,更好维护,且没有CQ两端的数据一致性问题,因为是共享一个数据库的。我个人认为,这种架构很实用,既兼顾了数据的强一致性,又能让代码好维护。 2.CQ两端数据库和上层代码都分离,然后Q的数据由C端同步过来,一般是通过Domain Event进行同步。同步方式有两种,同步或异步,如果需要CQ两端的强一致性,则需要用同步;如果能接受CQ两端数据的最终一致性,则可以使用异步。采用这种方式的架构,个人觉得,C端应该采用Event Sourcing(简称ES)模式才有意义,否则就是自己给自己找麻烦。因为这样做你会发现会出现冗余数据,同样的数据,在C端的db中有,而在Q端的db中也有。和上面第一种做法相比,我想不到什么好处。而采用ES,则所有C端的最新数据全部用Domain Event表达即可;而要查询显示用的数据,则从Q端的ReadDB(关系型数据库)查询即可。
目錄 第1章 我們的領域: 會議管理系統1 1.1 Contoso公司簡介1 1.2 誰與我們同行2 1.3 Contoso會議管理系統3 1.3.1 系統概覽3 1.3.2 非功能性需求4 1.4 開始我們的旅程5 1.5 更多信息5 第2章 領域分解——站點規劃6 2.1 本章術語定義6 2.2 會議管理系統裡面的有界上下文7 2.2.1 訂單和註冊有界上下文7 2.2.2 會議管理有界上下文7 2.2.3 支付有界上下文8 2.2.4 不包括在內的有界上下文8 2.2.5 Contoso會議管理系統的上下文路線圖9 2.3 為什麼選擇這些有界上下文10 2.4 更多信息11 第3章 訂單和註冊有界上下文12 3.1 訂單和註冊有界上下文簡介12 3.2 本章術語定義13 3.3 領域定義(普適語言)14 3.4 訂單創建的需求分析16 3.5 系統架構17探索CQRS和事件源目錄3.6模式和概念17 3.6.1 系統驗證21 3.6.2 交易邊界22 3.6.3 併發處理22 3.6.4 Aggregates和Aggregate Roots22 3.7 實現細節23 3.7.1 高層架構23 3.7.2 寫者模型28 3.7.3 使用Windows Azure服務匯流排37 3.8 對測試的影響44 3.9 本章小結47 3.10 更多信息47 第4章 擴展和改進訂單和註冊有界上下文48 4.1 修改有界上下文48 4.1.1 本章術語定義49 4.1.2 用戶需求49 4.1.3 系統架構49 4.2 模式和概念51 4.2.1 記錄定位器51 4.2.2 讀者端查詢51 4.2.3 向讀者端提供部分履行的訂單信息54 4.2.4 CQRS命令驗證55 4.2.5 倒計時定時器和讀者模型56 4.3 實現細節56 4.3.1 訂單訪問碼(記錄定位器)57 4.3.2 倒計時定時器58 4.3.3 使用ASP.NET MVC驗證60 4.3.4 將改動推送到讀者端62 4.3.5 重構SeatsAvailability aggregate66 4.4 對測試的影響68 4.4.1 接受測試和領域專家68 4.4.2 使用SpecFlow功能來定義接受測試68 4.4.3 通過測試來幫助開發人員理解消息流75 4.5 代碼理解的旅程: 痛苦、釋放和學習的故事77 4.5.1 測試很重要77 4.5.2 領域測試78 4.5.3 硬幣的另外一面80 4.6 本章小結83 4.7 更多信息84 第5章 準備V1發布85 5.1 Contoso會議管理系統的V1發布版85 5.1.1 本章術語定義85 5.1.2 用戶需求86 5.1.3 系統架構87 5.2 模式和概念91 5.2.1 事件源91 5.2.2 基於任務的用戶界面92 5.2.3 有界上下文之間的集成95 5.2.4 分散式交易和事件源98 5.2.5 自治與集權99 5.2.6 讀者端的實現方法100 5.2.7 最終一致性100 5.3 實現細節101 5.3.1 會議管理有界上下文101 5.3.2 支付有界上下文102 5.3.3 事件源105 5.3.4 基於Windows Azure表格的事件庫111 5.3.5 訂單總價計算114 5.4 對測試的影響114 5.4.1 時序問題114 5.4.2 引入領域專家115 5.5 本章小結115 5.6 更多信息115 第6章 系統版本控制116 6.1 本章術語定義116 6.1.1 用戶需求116 6.1.2 系統架構117 6.2 模式和概念118 6.2.1 修改事件定義118 6.2.2 確保消息的自洽性119 6.2.3 集成事件的保存121 6.2.4 消息排序122 6.3 實現細節123 6.3.1 對零成本訂單的支持123 6.3.2 顯示剩餘座位數127 6.3.3 刪除重複命令130 6.3.4 確保消息排序131 6.3.5 保存會議管理有界上下文的事件135 6.3.6 從V1版本遷移到V2版本139 6.4 對測試的影響140 6.4.1 重訪SpecFlow140 6.4.2 在遷移過程中發現錯誤143 6.5 本章小結143 6.6 更多信息144 第7章 加入彈性和優化性能145 7.1 本章術語定義145 7.2 系統架構145 7.3 加入彈性147 7.3.1 增加事件重複處理時的彈性148 7.3.2 確保命令的發送148 7.4 優化性能148 7.4.1 優化前的用戶界面流程149 7.4.2 用戶界面優化150 7.4.3 基礎設施優化151 7.5 無停機遷移158 7.6 實現細節159 7.6.1 改進RegistrationProcessManager類160 7.6.2 用戶界面流程優化165 7.6.3 消息的非同步接收、處理和發送170 7.6.4 在流程內部對命令進行同步處理171 7.6.5 使用備忘錄模式來實現快照173 7.6.6 對事件進行並行發布175 7.6.7 在訂購服務裡面對消息進行過濾176 7.6.8 為SeatsAvailability aggregate創建專門的SessionSubscriptionReceiver實例177 7.6.9 緩存讀者模型數據179 7.6.10 使用多個議題來劃分服務匯流排180 7.6.11 其他的優化和強化措施181 7.7 對測試的影響184 7.7.1 集成測試185 7.7.2 用戶界面測試185 7.8 本章小結185 7.9 更多信息185 第8章 尾聲: 經驗教訓186 8.1 我們學到了什麼186 8.1.1 性能很重要186 8.1.2 實現消息驅動並不簡單187 8.1.3 雲平台的挑戰187 8.1.4 不同的CQRS188 8.1.5 事件源和交易日誌記錄190 8.1.6 引入領域專家190 8.1.7 什麼時候該使用CQRS190 8.2 如果重新來過,我們會做的有什麼不同191 8.2.1 以牢靠的消息和保存基礎設施為起點191 8.2.2 更好地利用基礎設施的能力191 8.2.3 採納更加系統化的方法來實現流程管理器192 8.2.4 對應用程序實施不同的劃分192 8.2.5 以不同方式組織項目團隊192 8.2.6 對領域和有界上下文的CQRS適用性進行評估192 8.2.7 為性能進行規劃192 8.2.8 重新考慮用戶界面193 8.2.9 探索事件源的其他用處193 8.2.10 探索有界上下文的集成問題193 8.3 更多信息194
第1章我们的领域: 会议管理系统1 1.1Contoso公司简介1 1.2谁与我们同行2 1.3Contoso会议管理系统3 1.3.1系统概览3 1.3.2非功能性需求4 1.4开始我们的旅程5 1.5更多信息5 第2章领域分解——站点规划6 2.1本章术语定义6 2.2会议管理系统里面的有界上下文7 2.2.1订单和注册有界上下文7 2.2.2会议管理有界上下文7 2.2.3支付有界上下文8 2.2.4不包括在内的有界上下文8 2.2.5Contoso会议管理系统的上下文路线图9 2.3为什么选择这些有界上下文10 2.4更多信息11 第3章订单和注册有界上下文12 3.1订单和注册有界上下文简介12 3.2本章术语定义13 3.3领域定义(普适语言)14 3.4订单创建的需求分析16 3.5系统架构17探索CQRS和事件源目录3.6模式和概念17 3.6.1系统验证21 3.6.2交易边界22 3.6.3并发处理22 3.6.4Aggregates和Aggregate Roots22 3.7实现细节23 3.7.1高层架构23 3.7.2写者模型28 3.7.3使用Windows Azure服务总线37 3.8对测试的影响44 3.9本章小结47 3.10更多信息47 第4章扩展和改进订单和注册有界上下文48 4.1修改有界上下文48 4.1.1本章术语定义49 4.1.2用户需求49 4.1.3系统架构49 4.2模式和概念51 4.2.1记录定位器51 4.2.2读者端查询51 4.2.3向读者端提供部分履行的订单信息54 4.2.4CQRS命令验证55 4.2.5倒计时定时器和读者模型56 4.3实现细节56 4.3.1订单访问码(记录定位器)57 4.3.2倒计时定时器58 4.3.3使用ASP.NET MVC验证60 4.3.4将改动推送到读者端62 4.3.5重构SeatsAvailability aggregate66 4.4对测试的影响68 4.4.1接受测试和领域专家68 4.4.2使用SpecFlow功能来定义接受测试68 4.4.3通过测试来帮助开发人员理解消息流75 4.5代码理解的旅程: 痛苦、释放和学习的故事77 4.5.1测试很重要77 4.5.2领域测试78 4.5.3硬币的另外一面80 4.6本章小结83 4.7更多信息84 第5章准备V1发布85 5.1Contoso会议管理系统的V1发布版85 5.1.1本章术语定义85 5.1.2用户需求86 5.1.3系统架构87 5.2模式和概念91 5.2.1事件源91 5.2.2基于任务的用户界面92 5.2.3有界上下文之间的集成95 5.2.4分布式交易和事件源98 5.2.5自治与集权99 5.2.6读者端的实现方法100 5.2.7最终一致性100 5.3实现细节101 5.3.1会议管理有界上下文101 5.3.2支付有界上下文102 5.3.3事件源105 5.3.4基于Windows Azure表格的事件库111 5.3.5订单总价计算114 5.4对测试的影响114 5.4.1时序问题114 5.4.2引入领域专家115 5.5本章小结115 5.6更多信息115 第6章系统版本控制116 6.1本章术语定义116 6.1.1用户需求116 6.1.2系统架构117 6.2模式和概念118 6.2.1修改事件定义118 6.2.2确保消息的自洽性119 6.2.3集成事件的保存121 6.2.4消息排序122 6.3实现细节123 6.3.1对零成本订单的支持123 6.3.2显示剩余座位数127 6.3.3删除重复命令130 6.3.4确保消息排序131 6.3.5保存会议管理有界上下文的事件135 6.3.6从V1版本迁移到V2版本139 6.4对测试的影响140 6.4.1重访SpecFlow140 6.4.2在迁移过程中发现错误143 6.5本章小结143 6.6更多信息144 第7章加入弹性和优化性能145 7.1本章术语定义145 7.2系统架构145 7.3加入弹性147 7.3.1增加事件重复处理时的弹性148 7.3.2确保命令的发送148 7.4优化性能148 7.4.1优化前的用户界面流程149 7.4.2用户界面优化150 7.4.3基础设施优化151 7.5无停机迁移158 7.6实现细节159 7.6.1改进RegistrationProcessManager类160 7.6.2用户界面流程优化165 7.6.3消息的异步接收、处理和发送170 7.6.4在流程内部对命令进行同步处理171 7.6.5使用备忘录模式来实现快照173 7.6.6对事件进行并行发布175 7.6.7在订购服务里面对消息进行过滤176 7.6.8为SeatsAvailability aggregate创建专门的SessionSubscriptionReceiver 实例177 7.6.9缓存读者模型数据179 7.6.10使用多个议题来划分服务总线180 7.6.11其他的优化和强化措施181 7.7对测试的影响184 7.7.1集成测试185 7.7.2用户界面测试185 7.8本章小结185 7.9更多信息185 第8章尾声: 经验教训186 8.1我们学到了什么186 8.1.1性能很重要186 8.1.2实现消息驱动并不简单187 8.1.3云平台的挑战187 8.1.4不同的CQRS188 8.1.5事件源和交易日志记录190 8.1.6引入领域专家190 8.1.7什么时候该使用CQRS190 8.2如果重新来过,我们会做的有什么不同191 8.2.1以牢靠的消息和保存基础设施为起点191 8.2.2更好地利用基础设施的能力191 8.2.3采纳更加系统化的方法来实现流程管理器192 8.2.4对应用程序实施不同的划分192 8.2.5以不同方式组织项目团队192 8.2.6对领域和有界上下文的CQRS适用性进行评估192 8.2.7为性能进行规划192 8.2.8重新考虑用户界面193 8.2.9探索事件源的其他用处193 8.2.10探索有界上下文的集成问题193 8.3更多信息194

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值