代码改变世界

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

2013-06-22 16:05  youxin  阅读(425)  评论(0编辑  收藏  举报

读了“蓝皮书”距今差不多一年,它改变了我的软件开发和构建软件架构观。在我作为一名程序员期间,我尝试了许多不同的方式来构建软件。方法有很多,包括一个贫血的域模型(Anemic Domain Model)。构建贫血领域模型并无什么不妥,但对于较为复杂的业务逻辑应用,它可能不是最好的选择。最终结果只能是代码间高耦合的很多“意大利面条式的代码”贫血领域模型使得其业务逻辑遍布整个代码,如果业务规则改变,需要经常更新多个地方的代码,想避免这种情况,编码时请牢记这点。

 

编程的时候,总是想着那个维护你代码的人会是一个知道你住在哪儿的有暴力倾向的精神病患者。”—— Martin Golding

 

典型的富领域模型将所有业务逻辑隐藏在模型内部,而大多数对象间相互关联。试图构建一个完美的模型来解决领域的业务逻辑,这往往是以此方式开发软件失败之所在,其结果是一个非常庞大的模型,而应该考虑有限的上下文,但这不是本文的主题。

 

拆分模型

不要试图在一个模型中的解决所有事情;将其分割成较小的部分,并自成体系。创建者将它们自然低聚合在一起。以汽车和客户为例,在这个例子中,两者都是聚合源,不应该将它们在(同一)模型中同时创建两个对象,而应该聚合为另一模型:将它们当作模型内分立的小模型。这将会使模型持久化或加载到内存中时更易于处理。

Greg Yung有一个很好的例子来阐述聚合源(aggregate roots)及其工作机制。如果校长问老师要他学生的概况,他不会把所有的学生带到校长办公室,但他会带一份校长要的学生信息清单。大多数情况下,将所有对象放在一个庞大的模型里无充足的理由。

 

关系数据库的问题

如今许多人使用ORM框架将域模型数据持久化到数据库中,他们使用关系数据库来作为数据库(有别人的吗?;-))。关系数据库的问题,是必须在读和写数据方面做出妥协。一般情况下,关系数据库在插入、更新、删除数据时工作更好,读取数据时则不然,取决于索引;这些缺陷在大多数情况下将减缓数据的选取(select)操作,却有助于插入(insert)、删除(delete)及更新(update)数据操作。

 

(关于更新:我已经收到此发言的一些评论。我所想说的是:如果你添加索引,以改善更新/删除操作,这可能会影响你对数据的选择操作。你必须做妥协,要么读数据快要么写数据快。)

如果决定数据库在读数据(选择)时更好,一个选择是非规范化数据库。一个去规范化的数据库意味着表中存在重复数据,以及表中可能包含骇人听闻的列数。

作为开发人员,往往没有太多地考虑这个问题,如何规划(scale)解决方案?我认为在确立一个可能要为大量用户服务的解决方案之前是首先应该考虑的问题,但并非说应该试图预测未来的事情,而是从一开始就应该采取一些简单的措施,不必想太多以后的事情。

 

命令和查询的责任分离

大多数应用程序读取数据比写入数据更频繁。基于此点认知,这,使你可以轻松地添加更多的数据库用于读取的解决方案将是一个好主意,是否如此呢?因此,可否仅为读取数据设立专用数据库呢?更妙的是,如果以某种方式设计数据库,以便它的读取速度更快呢?如果你基于CQRS架构描述的模式设计应用,将有一个可扩展性好和数据读取快速的解决方案。命令查询分离(CQS是一种由Bertrand Meyer首先提出的模式。他基于对象级描述该模式。后来,这种模式摆脱了低水平的徘徊,被用于高级架构(模式)级别。我认为是Udi Dahan首先开始讨论这个架构原理(principle)。

在本文中我描述了一个非常接近Udi Dahan描述的架构。这种模式有几中实现(方式),其中之一正如由Greg Yung所述。Greg Yung的特色是用事件源(Event Sourcing)来描述CQRS,本文则不然。若是对该种描述有兴趣,Google一下即可。

 

CQRS架构简述

一个图形化的架构概述如下图所示,现对该图做一个简述。用户打开应用和第一个画面是加载,获取的数据来自查询侧。本例中它应用一WCF服务(诸如NHibernate)实现查询,从数据库读取数据到返回数据给图形用户界面的DTO中,该DTO定制,以适应用户在屏幕上观看。查询数据库通常是非(或去)规范化的,以便提高数据读取速度。用户可以通过不同的画面浏览数据,查询过程是相同的。为用户画面量身定制的DTO从数据库中返回。最后,用户要改变某一画面上的数据。那么发生的情况是:基于改变数据的画面创建创建一个命令消息,并将该消息发送到下图的左侧---命令侧。命令消息被发送到领域模型以便校验是否与业务规则冲突,若某些业务规则失效(可有不同的实现方法),一个错误消息发送回客户端。如果该消息经领域模型无差错,它将被持久化到数据库,并与读取数据库(组)同步。该命令侧除错误信息外不应返回任何数据信息。如果遵循这个规则,命令侧只包含行为。这使得域模型(事件)日志很容易记录,也极易追踪用户想做的事情,而非仅是其动作结果的记录。本文描述的方法与Greg Yung所述略有不同。他的解决方案中提倡命令侧无数据库,数据库只用于报表,这是一个伟大的方法,但它使事情变得有点复杂,它需要新开发(green-field)的项目(?),然我所描述的方法可用于既有(brown-field)应用而无需改变现有架构。大多数情况下,只需为原有架构添加查询侧(query-side)。

 

为什么添加查询端的DTO

在传统的领域模型,为解决领域的(业务)规则而创建对象,而非为浏览(或查阅)对象。他们具有行为,并非仅是(展现)形状。为使域对象更加可见(化),许多开发人员将域模型映射为定制的显示DTO,结果是开发者需要在很多对象和公开了getter和setter的领域模型之间进行映射(要了解为什么这样不好,可用Google搜索:getter和setter是邪恶的)。

如果您使用如同NHibernate的ORM,你必须为域模型添加getter(若要使用延迟装载(lazy loading)),这是可以确定的。该模型仍然受保护,以防止可以使之无效的不必要变化,每一变化须通过它的命令方法(实现)。

 

结论

从这个架构的所得就是:应用(程序)读取端(通常会获到大多数负载)的可扩展性,记录域模型所有事件的可行性(通过跟踪域模型中的命令和事件),以及只需要担心写入数据而非从数据库向GUI传送数据的域模型。我保证这能使很多事情更轻松。而最后一件事,是基于展现(view)创建对象(DTO),以帮助我们避免很多的映射和为画面(或展现)填入数据与数据库冲突的过多要求。我将在今后冠以体系结构部分的论述给出更多的细节。

 

注1)原架构图:

2)更新后架构图:

原文链接: http://blog.fossmo.net/post/Command-and-Query-Responsibility-Segregation-(CQRS).aspx

相关主题

The denormalizer in CQRS

CQRS made us more productive

WCF Data Services: A perfect fit for our CQRS implementation