ASP.NET Core Web API下事件驱动型架构的实现(四):CQRS架构中聚合与聚合根的实现 (2)

在调用方法的时候,方法本身会产生一个领域事件,这个领域事件会立刻被聚合本身的内联事件处理器捕获,并进行处理。在处理的过程中,会更新聚合中对象的状态,同时,这个领域事件还会被缓存在聚合中

命令处理器在完成对聚合的更改之后,便会调用仓储,将更改后的模型保存下来

接着,仓储从聚合中获得所有缓存的未曾保存的领域事件,并将所有这些领域事件逐个保存到事件存储数据库。在成功完成保存之后,会清空聚合中的事件缓存

最后,仓储将所有的这些领域事件逐个地派发到事件消息总线

接下来在事件消息总线和事件处理器中将会发生的事情,我们今后还会讨论,这里就不多说了。从这个过程,我们不难得出:

CQRS的聚合中,更改对象状态必须通过领域事件,也就是说,不能向外界曝露直接访问对象状态的接口,更通俗地说,表示对象状态的属性(Property)不能有设置器(Setter)

CQRS聚合的聚合根中,会有一套内联的领域事件处理机制,用来捕获并处理聚合中产生的领域事件

CQRS聚合的聚合根会有一个保存未提交领域事件的本地缓存,对该缓存的访问应该是线程安全的

CQRS的聚合需要能够向仓储提供必要的接口,比如清除事件缓存的方法等

此外,CQRS聚合是有版本号的,版本号通常是一个64位整型,表述历史上发生在聚合上的领域事件一共有多少个。当然,这个值在我们目前的讨论中并非能够真正用得上,但是,在仓储重建聚合需要依赖快照时,这个版本号就非常重要了。我会在后续文章中介绍

听起来是不是非常复杂?确实如此。那我们就先从领域事件入手,逐步实现CQRS中的聚合与聚合根。

领域事件

领域事件,顾名思义,就是从领域模型中产生的事件消息。概念上很简单,比如,客户登录网站,就会由客户登录实体产生一个事件派发出去,例如CustomerLoggedOnEvent,表示客户登录这件事已经发生了。虽然在DDD的实践中,领域事件更多地在CQRS架构中被讨论,其实即便是非事件驱动型架构,也可以通过领域模型来发布消息,达到系统解耦的目的。

延续之前的设计,我们的领域事件继承了IEvent接口,并增加了三个属性/方法,此外,为了编程方便,我们实现了领域事件的抽象类,UML类图如下:

image

图中的绿色部分就是在之前我们的事件模型上新加的接口和类,用以表述领域事件的概念。其中:

aggregateRootId:发生该领域事件的聚合的聚合根的ID值

aggregateRootType:发生该领域事件的聚合的聚合根的类型

sequence:该领域事件的序列号

好了,如果说我们将发生在某聚合上的领域事件保存到关系型数据库,那么,当需要获得该聚合的所有领域事件时,只需要下面一句SQL就行了:

SELECT * FROM [Events] WHERE [AggregateRootId]=aggregateRootId AND [AggregateRootType]=aggregateRootType ORDER BY [Sequence] ASC

这就是最简单的事件存储数据库的实现了。不过,我们暂时不介绍这些内容。

事实上,与标准的事件(IEvent接口)相比,除了上面三个主要的属性之外,领域事件还可以包含更多的属性和方法,这就要看具体的需求和设计了。不过目前为止,我们定义这三个属性已经够用了,不要把问题搞得太复杂。

有了领域事件的基本模型,我们开始设计CQRS下的聚合。

聚合的设计与实现

由于外界访问聚合都是通过聚合根来实现的,因此,针对聚合的操作都会被委托给聚合根来处理。比如,当用户地址发生变化时,服务层会调用Customer.ChangeAddress方法,这个方法就会产生一个领域事件,并通过内联的事件处理机制更改聚合中Address值对象中的状态。于是,从技术角度,聚合的设计也就是聚合根的实现。

接口与类之间的关系

首先需要设计的是与聚合相关的概念所表述的接口、类及其之间的关系。结合领域驱动设计中的概念,我们得到下面的设计:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpsywf.html