在面向对象设计中,常常忽略了一个概念,对象是用于描述一个单一职责的,它要求我们:一个对象包含自身属性、行为、内在调用等行为。对象间通过相互调用完成一项工作。因此,我们在做面向对象设计时,应该分清一个对象应该包含多少东西,对象的属性是否 belong to
它自身。如果一个对象的行为模糊不清,是否该对象设计合理?
¶什么是CQRS?
CQRS即 Command Query Responsibility Segregation
——命令查询职责分离。该术语描述了一个对象的方法应该 要么是 commands 要么是 queries
。
- 一个
query
返回数据但不会更改对象的状态。 - 一个
command
改变对象的状态,但不会返回任何数据。
从该术语的原则性上带来的好处是:系统状态发生改变时,可以清晰理解状态的变更做了什么。
CQRS在该原则上更进一层,并定义了一个简单的模式。
前面说到,一个对象的方法要么是 commands 要么是 queries, CQRS则是从一个对象分离两个对象的实现。这种分离的依据是方法是 command
还是 query
(a command is any method that mutates state and a query is any method that returns a value
)。
在构建企业级系统中,这种简朴的模式带来什么重要的令人关注的方面?主要是迎合大规模、大范围架构的挑战,诸如:
- 实现可伸缩性
- 管理复杂性
- 已有系统业务部分内容频繁变更
“CQRS is a simple pattern that strictly segregates the responsibility of handling command input into an autonomous system from the responsibility of handling side-effect-free query/read access on the same system. Consequently, the decoupling allows for any number of homogeneous or heterogeneous query/read modules to be paired with a command processor. This principle presents a very suitable foundation for event sourcing, eventual-consistency state replication/fan-out and, thus, high-scale read access. In simple terms, you don’t service queries via the same module of a service that you process commands through. In REST terminology, GET requests wire up to a different thing from what PUT, POST, and DELETE requests wire up to.”
—Clemens Vasters (CQRS Advisors Mail List)
“大意:CQRS的模式将 command
处理的职责分离出来,实现自治。并且允许任意数量的同构或异构的 query/read
搭配到 command processor
。该原则非常适用于event sourcing
的实现——最终一致性状态 应答/扇出、大规模read访问。简言之,服务通过command
处理,不在同一个模块提供 query
。 ”
¶Read and write sides
下图是 CQRS 在企业级系统的典型应用。
CQRS 模式中实现了读写分离。read side 仅包含 query methods;write side 仅包含 command methods。这样分离的动机主要考虑到:
- 在多数业务系统,读和写的数量是不对等的(异构的)。每个
write
可能同时处理上千个reads
。分离这两部分使得我们能够单独进行优化。例如,对reads
部分进行水平扩展… - 典型地,
commands
涉及到复杂业务逻辑,要确保系统能写入正确的、一致的数据到数据存储中。read
操作比起write
操作要简单许多。一个简单的同时封装read
和write
操作的模型(例如 JPA)可能两方面都处理不当。从最终结果分离read
和write
,得到更容易维护、更灵活的模型。 - 分离也可以从数据存储上着手。
write
部分使用满足第三范式(3NF)的数据架构;read
部分使用非规范型数据库用于优化得到更快速的query
操作。
注意:
上图描述了CQRS在read
和write
使用了不同的数据存储,不是要求你使用不同的数据库。它仅仅描述CQRS这个模型,鼓励你分离读写部分。
上图建议write
和read
可能存在一一对应(one-to-one)关系,实际上没有必要要求建立这种关系。仅当你使用接口时,这种一一对应关系才明显。
在实践和适应这种架构的同时,可能会潜在一系列问题:
- 尽管单独
read
和write
两个模型比起一个复合模型要简单,当业务量需求扩增时,整体的架构要比传统的方式要复杂。如何转移该复杂性? - 在
read
和write
两方面,如何传播数据的变更? - 在
write
部分若发生延迟,如何传递到read
部分? - CQRS的模型是什么?它能做什么?
¶CQRS and DDD
领域驱动(Domain-Driven Design)设计理念要求有:
- Models should be bound to the implementation.
- You should cultivate a language based on the model.
- Models should be knowledge rich.
- You should brainstorm and experiment to develop the model.
当讨论在系统中实现CQRS,意味着你应该实现一个带有bounded context
的CQRS模式。
在DDD,bounded context
定义了一个模型的语义、通用语言(ubiquitous language)的作用域(scope)。实现CQRS模式理所当然带来了 scalability、simplicity、maintainability。因此,在讨论 bounded context
时候,CQRS模式用于实现业务组件,DDD模式用于实现上下文边界(bounded context
)。
a business component can exist in only one bounded context.
以前在DDD概念中,bounded context
还没有一个比较明确的定位,术语bounded context
和 business component
表述的是同一个东西。引入CQRS模式后,术语bounded context
优于business component
使用。
总之,你不应该
将CQRS模式用于系统的顶层设计。CQRS应该明确定义在系统中独立的、具有明显业务逻辑的功能。
¶Commands, Events, Messages
DDD 属于一种分析和设计方法,它鼓励在使用模型(models)
和通用语言(ubiquitous language)
的基础上,使用领域的概念培养开发团队的共识, 构建业务 和 开发团队的桥接。必然地,DDD的方式面向分析行为,而不是业务领域中的数据,它导向于行为的建模和实现。较直接实现领域模型(domain model)的方式,就是使用commands
和events
。
commands
是祈使句:它由系统发起处理一个任务或动作。命令通常处理一次。events
是通告(notifications):告知某事已经发生。事件可以发生多次,被多处消费。
commands
和events
都属于message
,都用于对象间数据交换。在DDD术语中,message
表示业务行为,帮助系统捕获业务意图。
CQRS的一个可能的实现方式是分离read
和write
存储;每个数据存储被优化。Event
提供了一个基本的机制用于异步write
和read
操作。当write
部分发起一个事件表示应用状态变更,read
部分响应该事件,并更新数据。下图为命令和事件在CQRS模式中的实现。
¶为什么用CQRS?
回到DDD领域设计,CQRS适用于DDD的bounded context
允许标识和专注于系统逻辑最为复杂部分的实现。CQRS仅仅是bounded context
一个具体的实践,但不是唯一的模式。
对于业务逻辑带来以下几点好处:
¶Scalability
在多数企业系统中,读操作远超过写操作,因此,读写操作的扩展要求是不一样的。在bounded context
中分离read
和write
,成为单独的模型,可以单独扩展各自的功能。
另外,伸缩性并不是实现CQRS模式的唯一理由:在一个非协作领域,你可以水平地添加更多的数据库来支持更多用户、更多的请求。
¶Reduced complexity
在领域的复杂区域,设计和实现对象,都会加剧复杂性。大多数情况下,复杂的业务逻辑仅仅在处理更新或事物处理上发生;相反,读的逻辑通常很简单。当读操作和其它业务逻辑混淆在相同的模型时,就变得难于处理问题,诸如多用户、共享数据、性能、事物、一致性、脏数据。因此要分离read logic
和business logic
到不同的模型中。然而,这种分离的代价是需要花费较大的努力,并且要求开发者自身对已有的模型有充足的理解。
分离职责是CQRS的核心动机,因为query
会被用于非常多的场景、会被各处引用,每个修改都需要特别小心。如何设计一个复杂度更低的系统,正是bounded context
的潜力所在,你可能需要TDD、refactor、或更多优秀的编码习惯来贯穿。
¶Flexibility
CQRS的灵活性,来源于read-side
和write-side
模型的扩展。read-side
的扩展非常容易,因为读操作复杂度最低,并且不会受到业务逻辑的影响。对于write-side
,它有单独的模型处理业务逻辑,模型相互独立,不依赖其它核心逻辑(从设计上来说,如果有依赖,要么设计有问题,要么面临重构)。
从长远来看,业务逻辑的灵活性直接与商业价值挂钩。它决定了你是否能够持续交付新功能、能否进行敏捷开发、能否满足同行的恶劣竞争。
灵活性和敏捷性与DDD的持续集成概念相关:
“Continuous integration means that all work within the context is being merged and made consistent frequently enough that when splinters happen they are caught and corrected quickly.”
—Eric Evans, “Domain-Driven Design,” p342.
¶Focus on the business
¶Facilitates building task-based UIs
¶When should I use CQRS?
什么时候用CQRS?微服务。CQRS模式明显是用于bounded context
中的。bounded context
的另一个术语是business components
。说白了就是我们要实现的服务,具体是什么服务?如何划分?服务之间的相互关系是怎样?这都属于bounded context
中描述的内容。CQRS可能不是最好的,但它提供了 increased adaptability、flexibility、reduced maintenance costs各方面的优势。
¶Collaborative domains
DDD概念需要解决的问题是如何描述有界上下文,开发者要与领域专家进行交流,就需要明确这个bounded context
是什么。
CQRS参与了涉及复杂决定的协作过程——结果(产出)什么。这种协作往往是系统中最复杂、不固定、最需要关注的bounded contexts
。
¶Stale data
在协作环境中,多个用户可能同时操作同一份数据,你将面临脏数据问题;如一个用户正在浏览数据,另一个用户在进行更改,那么第一个的用户看到的是脏数据。
前面两个举例是最常见的场景;大多数协作企业系统比这更多。CQRS从架构层面上解决脏数据的问题。我们切换到write-side
,用户从read-side
读取数据。无论用什么机制,将write-side
的数据 push
到read-side
;read-side
就以相同的机制操作数据。常见的机制就是ES(Event sourcing)
。
¶When should I avoid CQRS?
非协作的、简单的、静态的用于处理分析、建模之类的复杂实现bounded contexts
。应该避免使用CQRS。