DDD与CQRS的关系

领域驱动设计(DDD)系列

Posted by Bruce Wong on November 27, 2021

前言

今天介绍一个实际工作中遇到的概念理解不准确的实际例子。最近在和Team工作的时候发现大家把DDD和CQRS的思想有些混淆。有的开发会认为CQRS是DDD的升级。DDD已经过时了。所以结合我自己的理解来总结一下这其中的误解和我个人的想法。

正文

DDD全名(Domain Driven Design)领域驱动设计,它是一种从业务角度入手对软件架构进行设计思考的方式方法。它本身包括一系列自己的术语、思维方式、设计模式和衍生出来的一些架构。例如最典型的整洁架构,洋葱圈架构,还有六边形架构等都是以领域驱动设计为核心内核来考虑的。CQRS全名Command-Query Responsibility Segregation。CQRS是将紧缩(Stringent)对象(或者组件)设计原则和命令-查询分离(CQS)应用在架构模式中的结果。最简单的一个例子就是开发人员经常挂在嘴边的一个说法——“数据库读写分离”。可以把这个理解为CQRS思想的最直观的一个例子。

从上面的两者的简单描述能看出来,其实DDD是一种设计思想,而CQRS是一种架构实践。他们之间没有直接关系。但是在看DDD相关的书籍或者文章中能够看到CQRS其实是一个经常被谈及的落地场景。CQRS并不是简单的起到读写分离、提高系统性能效率这么简单,如果是这样那么CQS就可以了。而CQRS这正的用力为了解决DDD设计在落地中实际场景而存在的。
CQRS
上图是从《实现领域驱动设计》一书中截图的。大家可以看到CQRS的一个简单过程。从DDD的思想来看,当我们和业务方构建出了领域模型之后,应用面向对象编程思想会设计出一个代表业务模型的实体(暂且理解为一个类)。这个类中会有属性、字段来描述当前领域对象的状态,有方法来描述它能做的动作行为。当然可以很明显的区分出来一些改变状态的方法,还有一些查询结果的方法。在现实开发过程中会遇到查询不可能只查询某一个领域对象这么简单,很多时候是复杂的关联多个领域对象的查询,而且这种组合查询可能涉及到复杂的领域关系。而常规DDD的实体设计将方法和属性都归于一个类中的做法无法应对这种场景。如果硬套用会导致业务逻辑的泄露,反倒变成了一个大泥球了。而CQRS正是解决这种场景的一个方法。

有过数据仓库经验的人应该知道,复杂系统中高效的查询数据结构一定要经过ETL清洗过程。OLTP和OLAP对数据结构的设计思路是完全不同的。CQRS的思路其实参考了这个想法,领域对象实体中的方法都是服务当前领域对象自身的,主要集中在对象状态的更新和获取方面,这也就是上图中的命令处理器流程。而跨领域对象的查询从业务领域角度出发可以抽象出单独的领域服务,而这些领域服务主要的作用是组合多个业务对象返回业务需要的查询结果,这也就是上图中的查询处理器的作用。这个需求既体现了单独的业务需求如果没有一个可以抽象出来的业务实体与之对应,那么领域服务就该在此刻登场了。以一种服务的形式提供给应用服务来响应前端的请求。那么另一个问题来了,实体对象对应的操作都是OLTP的事务型操作,查询相关的领域服务是OLAP的查询型操作,那这两者之间的ETL如何来做呢?之前数据仓库的ETL可是一个漫长的过程,因为数据量的关系,可能需要数小时、数天甚至数周的过程处理。而CQRS并不需要,它巧妙的利用了领域事件机制,让更新和查询两个过程的相关数据保持最终一致性。

总结:查询的功能属于业务逻辑,可以通过领域服务来限定限界上下文,而命令的应用需要限定在实体/聚合对应的上下文中。这些是需要通过DDD的思想来划分领域对象,而不是简单把命令和查询分别放到不同类这么简单。类和类内部的方法如何设计这个是需要DDD的思想来引导的。

扩展一下可以发现,其实CQRS的架构思想可以简单也可以复杂。例如数据存储的形式可以有几种形式:共享数据库共享表;共享数据库分表;分数据库。应用程序也可以是单一的或者多个微服务结合。最简单的一个例子,结合一些市场上的数据分析工具,例如PowerBI,QuickSight,Tableau等他们链接到单独的数据仓库、数据集市、数据湖上,这些数据生成可以是CQRS对应Command部分的微服务生成的,而这些报表服务作为独立的服务行使的是Query部分职责。当然我们也可以自己编写报表服务代替使用这类现成的商业服务方案。我们可以组合出有很多的架构设计方案。

如果你想深入学习,可以阅读文末的推荐书籍。本文涉及到的DDD术语有:实体,领域服务,领域事件。欢迎有兴趣的小伙伴和我一起探讨DDD的实现。也欢迎大家和我一起阅读参考书籍,分享读后感。

参考书籍

《实现领域驱动设计》《领域驱动设计精粹》