CQRS初探-第1部分:设计

144 阅读6分钟

在本教程中,将会以咖啡店点餐这个业务场景作为示例。

核心概念:账单,它的主要作用是记录个人或团体在咖啡馆的消费记录。

场景描述

当顾客来到咖啡馆,在某个桌位坐下,此时会为他打开一个账单。然后顾客可以下单饮料和食物,服务员会随即为他提供饮料,但食物不同,它必须由厨师烹饪,有一个准备的过程,完成之后才可以提供。

在你用餐期间,顾客还可以下单其它额外的食物或饮料。并且如果顾客意识到自己点错了东西,他可以修改订单——但食物和饮料已经端上来并被接受之后不可以。

最后,顾客全额付清所有消费之后账单才能关闭。如果有未服务的项目,除非这些项目被标记为已服务或取消,否则不能关闭账单。

事件

在上面描述的场景中,着重标记了动词和名词。在以数据库为中心的方式工作时,通常要仔细听名词,将它们映射到表并将它们关联起来,这时候动词通常是次要的。但在以命令和事件为中心的设计上则将重点放在动词上,随后再考虑名词。

这是可以理解的,因为让一个领域有趣的东西往往是由动词而不是名词来决定的。每个企业都有客户(希望如此!)——重要的是他们为客户做了什么。

通看整个场景,我们把重点放在寻找过程中发生了什么事情,这些发生的事情会影响领域中的某些新信息。我们将这些发生事情的时刻映射为一系列事件。因为事件是关于已经发生的事情,所以它们用过去时来命名。

以下是我们从咖啡馆点餐场景中得出的一些事件:

TabOpened

DrinksOrdered

FoodOrdered

DrinksCancelled

FoodCancelled

DrinksServed

FoodPrepared

FoodServed

TabClosed

注意,这些事件非常具有领域特点。点饮料和点食物之间的区别很重要,所以我们将它们映射到不同的事件中。另外注意,这些动词应该来自领域,而不是像“创建”、“更新”或“删除”这样的通用术语。虽然表述为这样的事件并不一定错误,但你的大部分设计由这种术语构成,那么你应该更深入研究该领域,并避免crud思维。或者还有一种可能,您正在做的事情还不够复杂,不必应用DDD,这种情况下,可以使用更简单的方法。

命令

命令表征了对域的请求。当事件声明某些事情已经发生时,命令可以被接受或拒绝。一个被接受的命令将导致发出零个或多个事件,以将新的事实合并到系统中。被拒绝的命令会导致某种异常。

命令也可以通过动词来识别,不过,它们关注的是用户所认为的操作。例如,下单这个操作,虽然在本域中食物和饮料的处理方式不同是很重要,但服务员肯定不希望在系统中先输入饮料下单,然后再一次在系统中输入食物然后下单,因为这会导致无法获得所有订单的概览。另外餐桌上的每个顾客很可能会一起下单他们的食物和饮料,有些人可能会在知道他们的朋友点了什么后改变主意。因此,我们将会设计有一个单独的命令来下订单。

下面是我们到达的这个域的初始命令:

OpenTab

PlaceOrder

AmendOrder

MarkDrinksServed

MarkFoodPrepared

MarkFoodServed

CloseTab

注意这些名字是如何在祈使句语气中包含动词的。

异常

建模过程的一个重要部分是考虑可能导致命令被拒绝的因素。Edument CQRS Starter Kit在这点上有明确的观点:应该将这些“悲伤路径”建模为异常类型,就像命令和事件被表示为DTO一样。此外,这些异常类型可能包含请求不被接受的原因的详细信息。这是因为领域逻辑应该告诉前端哪里出了问题,而不是让它通过检查状态来询问——或者更糟的,去猜测。

看看这个场景,我们可以在模型中识别出三个值得注意的异常情况:

CannotCancelServedItem

TabHasUnservedItems

MustPayEnough

这里,名称试图解释命令失败的原因。

聚合

当然,现实生活中我们并不能仅仅依靠动词来生活,在某种程度上,必须有名词。更具体地说,必须有一种方法来谈论当前状态,以便决定是否应该接受命令。例如,要拒绝取消某些物品,我们必须知道该物品是否已经提供。

我们需要的所有信息都已经在过去事件流中捕获,因为它们捕获了引入系统的所有事实。然而,通常来说,我们对整个事件流并不感兴趣。相反,我们感兴趣的是与特定账单相关的事件,例如,每个账单自己的事件流。

这里可以用聚合来表征,每个聚合都有自己的事件流。综合起来,它们可以用来计算它的当前状态。聚合彼此之间完全隔离。关于是否要接受命令,决策过程完全基于命令本身和聚合过去已经发生的事件中包含的信息。

具体地说,聚合是:

一个单独的对象,它不引用任何其他对象。

一个隔离的对象图,其中一个对象被指定为集合的根。外界应该只知道这个根。

从设计的角度来看,这是一个要求,因为它迫使我们识别和分离业务概念。这也意味着我们必须真正关注一致性边界。

当前这个场景中的示例经过简化筛选,只有一种聚合类型——即账单。然而,在大多数系统中,为了识别聚合,我们需要做更多的工作。本教程的作者发现,从事件和命令开始,然后尝试根据不变量(需要维护的业务规则)对它们进行分组,是一种很好的策略。

下一步

我们现在准备开始编写测试用例并建立我们的领域逻辑。在此过程中,我们应该准备好回顾设计的各个方面,并且我们还需要充实我们的命令和事件包含的细节。