在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅、通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件总线的实现。接下来对于事件驱动型架构的讨论,就需要结合一个实际的架构案例来进行分析。在领域驱动设计的讨论范畴,CQRS架构本身就是事件驱动的,因此,我打算首先介绍一下CQRS架构下相关部分的实现,然后再继续讨论事件驱动型架构实现的具体问题。
当然,CQRS架构本身的实现也是根据实际情况的不同,需要具体问题具体分析的,不仅如此,CQRS架构的实现也是非常复杂的,绝不是一套文章一套案例能够解释清楚并涵盖全部的。所以,我不会把大部分篇幅放在CQRS架构实现的细节上,而是会着重介绍与我们的主题相关的内容,并对无关的内容进行弱化。或许,在这个系列文章结束的时候,我们会得到一个完整的、能够运行的CQRS架构系统,不过,这套系统极有可能仅供技术研讨和学习使用,无法直接用于生产环境。
基于这样的前提,我们今天首先看一下CQRS架构中聚合与聚合根的实现,或许你会觉得目前讨论的内容与你本打算关心的事件驱动架构没什么关系,而事实是,CQRS架构中聚合与聚合根的实现是完全面向事件驱动的,而这部分内容也会为我们之后的讨论做下铺垫。不仅如此,我还会在本文讨论一些基于.NET/C#的软件架构设计的思考与实践(请注意文章中我添加了Note字样并且字体加粗的句子),因此,我还是会推荐你继续读完这篇文章。
CQRS架构知识回顾早在2010年,我针对CQRS架构总结过一篇文章,题目是:《EntityFramework之领域驱动设计实践【扩展阅读】:CQRS体系结构模式》,当然,这篇文章跟Entity Framework本没啥关系,只是延续了领域驱动设计这一话题进行的扩展讨论罢了。这篇文章介绍了CQRS架构模式所产生的背景、结构,以及相关的一些概念,比如:最近非常流行的词语:“事件溯源”、解决事件溯源性能问题的“快照”、用于存取事件数据的“事件存储(Event Store)”,还有重新认识了什么叫做“对象的状态”,等等。此外,在后续的博文中,我也经常对CQRS架构中的实现细节做些探讨,有兴趣的读者可以翻看我过去的博客文章。总体上讲,CQRS架构基本符合下图所描述的结构:
看上去是不是特别复杂?没错,特别复杂,而且每个部分都可以使用不同的工具、框架,以不同的形式进行实现。整个架构甚至可以是语言、平台异构的,还可以跟外部系统进行整合,实现大数据分析、呈现等等,玩法可谓之五花八门,这些统统都不在我们的讨论范围之内。我们今天打算讨论的,就是上图右上部分“领域模型”框框里的主题:CQRS架构中的聚合与聚合根。
说到聚合与聚合根,了解过领域驱动设计(DDD)的读者肯定对这两个概念非常熟悉。通常情况下,具有相同生命周期,组合起来能够共同表述一种领域概念的一组模型对象,就可以组成一个聚合。在每个聚合中,衔接各个领域模型对象,并向外提供统一访问聚合的对象,就是聚合根。聚合中的所有对象,离开聚合根,就不能完整地表述一个领域概念。比如:收货地址无法离开客户,订单详情无法离开订单,库存无法离开货品等等。所以从定义上来看,一个聚合大概就是这样:
聚合中的对象可以是实体,也可以是值对象
聚合中所有对象具有相同的生命周期
外界通过聚合根访问整个聚合,聚合根通过导航属性(Navigation Properties)进而访问聚合中的其它实体和值对象
通过以上两点,可以得出:工厂和仓储必须针对聚合根进行操作
聚合根是一个实体
聚合中的对象是有状态的,通常会通过C#的属性(Properties)将状态曝露给外界
好吧,对这些概念比较熟悉的读者来说,我在此算是多啰嗦了几句。接下来,让我们结合CQRS架构中命令处理器对领域模型的更改过程来看看,除了以上这些常规特征之外,聚合与聚合根还有哪些特殊之处。当命令处理器接到操作命令时,便开始对领域模型进行更改,步骤如下:
首先,命令处理器通过仓储,获取具有指定ID值的聚合(聚合的ID值就是聚合根的ID值)
然后,仓储访问事件存储数据库,根据需要获取的聚合根的类型,以及ID值,获取所有关联的领域事件
其次,仓储构造聚合对象实例,并依照一定的顺序,逐一将领域事件重新应用在新构建的聚合上
每当有一个领域事件被应用在聚合上时,聚合本身的内联事件处理器会捕获这个领域事件,并根据领域事件中的数据,设置聚合中对象的状态
当所有的领域事件全部应用在聚合上时,聚合的状态就是曾经被保存时的状态
然后,仓储将已经恢复了状态的聚合返回给命令处理器,命令处理器调用聚合上的方法,对聚合进行更改