本文通过解读Greg Young的论文,简单梳理了CQRS与Event Sourcing的发展脉络,厘出其中的主要技术重点,并提出以Akka作为落地方案,以求对这两个主题有一个较为全面的总结。

引言

DDD是近年软件设计的热门。CQRS与Event Sourcing作为实施DDD的一种选择,也逐步进入人们的视野。围绕这两个主题,软件开发的大咖[Martin Fowler][Greg Young][Udi Dahan]分别有所论述,[MSDNC QRS Journey][Implementing DDD][Patterns, Principles, and Practices of DDD]等著述也提供了范例,国内外各大论坛的文章和DDD开源框架更是数不胜数,为学习CQRS和Event Sourcing提供了大量指导。

其中,Greg Young的论文最为系统。故本文通过解读其论文,简单梳理了CQRS与Event Sourcing的发展脉络,厘出其中的主要技术重点,并提出以Akka作为落地方案,以求对这两个主题有一个较为全面的总结。错谬之处,还望指正。

本文并未就DDD的相关方法和战术模式等进行介绍。

传统系统等于数据库表

这是典型的传统架构,其中Application Service是Domain Model的屏风,负责与Client打交道:

Figure

在传统架构下,数据在UI、模型和持久化的数据库之间流动,遵循下图所示的循环。

Figure

系统从数据库中读出DTO(数据传输对象,Data Transfer Object)后,根据DTO与领域对象的映射规则,将其转换为领域对象,同时呈现在UI上。当用户在UI上完成修改后,又根据同样的映射规则,将其更新到领域对象上,同时持久化到数据库当中。最后,系统根据持久化结果刷新UI,完成一次领域操作,从而保证了从UI到领域对象,再到持久化数据之间的一致性。这当中,DTO只是为防止暴露模型细节而设计的领域对象的投影,UI则体现为对该DTO各字段的对照呈现。

Figure

很自然地,系统的所有业务流程也随之演变成一系列围绕DTO的CRUD操作(Create-Read-Update-Delete)。于是在很长一段时间里,下图这样千篇一律的界面发展成为MIS(信息管理系统,Management Infomation System)类软件的主流,图中左下角的记录导航条成为标配元素。在这种情况下,如果把DTO当作数据库里的一行记录,那么整个系统可以视作以DTO为Row、以CRUD操作为主要事务的一系列数据库Table。

Figure

传统架构利弊明显

传统架构(包括分层架构)简单直观,只要设计好数据模型,系统设计就算成功了一大半,而且所有写入操作都由事务包裹,能够达到强一致性要求。但它也有以下几个弊端:

  • 在经过Application Service这层屏风后,用户意图全部被分解为CRUD操作,在领域对象之间无法得以体现。
  • 为保证DTO的信息完整和数据一致性,部分与操作无关的信息也将一并被纳入DTO,查询和构造DTO将成为系统的主要任务,而领域模型的业务流程相应被肢解和冲淡。
  • 完成一次领域操作,需要在DTO与领域对象间进行多次转译,增加了系统额外负担。这种转译被称为阻抗失配(Impedance Mismatch),其实质就是多维的对象图Graph与二维的关系Relationship之间相互转换时发生的、不可避免的信息丢失。
  • 读写操作将围绕同一数据模型展开,即使有数据库分库分表方案支持,其效率也不可避免地要受到竞态影响。

贫血模型换汤不换药

传统架构中的CRUD模式,其最大的弊端在于语义与操作的脱节。Application Service中的API通常代表着用例的某个方面,因此尚含有领域语义,比如API PlaceOrder()表示用户下单。然而该API在到达内部模型后,就被拆解映射为CRUD操作Order.Create()。相应地,API AddOrderItem()被映射为Order.Update()CancelOrder()被映射为操作Order.Delete(),等等。这样的别扭,又在DTO转译的负担之上,给理解和维护模型带来了一定的困难。

于是,为了尽可能保留用户意图,我们首先想到通过命名规范和方法的二次封装,使CRUD操作在字面上接近API的语义,比如用Product.Rename()封装Product.Update(Name = "NewName")。但这样的做法并未能改变实质,因此即使套用了Aggregate、Value Object和Repository等DDD的战术概念,但这种完全以“DTO结构 + CRUD操作”为主要元素构成的模型,被Martin Fowler等人称为“[贫血模型]”(Anemic Model)。

接下来,吸取贫血模型的教训,开始着手建立富含领域行为的各种领域对象。当API PlaceOrder()最终交给Order.Place()完成时,工作似乎已经画上了圆满句号。

在函数式编程范式中,“抽象数据类型ADT + 代数方法”组成的模型,与“DTO + CRUD”的方式非常类似,但从函数式编程的视角,这才是最合理、最优雅的模型。那么,它究竟是不是贫血模型呢?

版权声明:本文为Abbey原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/Abbey/p/14259136.html