如何准确地验证CQRS命令并将其转换为域对象?


22

我一直在修改穷人的CQRS 1,因为我喜欢它的灵活性,可以在一个数据存储中存储细粒度的数据,这为分析提供了极大的可能性,从而增加了商业价值,并且在需要时还提供了另一种包含非规范化数据的读取以提高性能。 。

但是不幸的是,从一开始,我就一直在为应该在这种架构中放置业务逻辑的问题而苦苦挣扎。

据我了解,命令是传达意图的手段,它本身与域没有关系。它们基本上是数据(哑巴,如果需要的话)传输对象。这是为了使命令可以在不同技术之间轻松转移。对于成功完成的事件的响应,同样适用于事件。

在典型的DDD应用程序中,业务逻辑驻留在实体,值对象,集合根中,它们既包含数据又包含行为。但是命令不是域对象,因此命令不应仅限于数据的域表示形式,因为这会对它们造成很大的压力。

因此,真正的问题是:逻辑到底在哪里?

我发现我在尝试构建一个非常复杂的集合(该集合为它的值的组合设置一些规则)时往往最容易遇到这种斗争。另外,在对域对象建模时,我喜欢遵循快速失败范例,知道对象何时到达处于有效状态的方法。

假设聚合Car使用两个组件:

  • Transmission
  • Engine

两个TransmissionEngine值对象被表示为超级类型和具有根据子类型,AutomaticManual传输,或PetrolElectric分别引擎。

在此域中,完全依靠成功创建的Transmission,,AutomaticManual或任意一种类型来生存Engine。但是Car聚合引入了一些新规则,这些规则仅在TransmissionEngine对象在同一上下文中使用时才适用。即:

  • 当汽车使用Electric发动机时,唯一允许的变速箱类型是Automatic
  • 当汽车使用Petrol发动机时,可能有两种类型的发动机Transmission

我可以在创建命令的级别上捕获这种违反组件组合的行为,但是正如我之前所说,据我所知,不应这样做,因为命令将包含业务逻辑,而业务逻辑应限于域层。

一种选择是将这种业务逻辑验证移至命令验证器本身,但这似乎也不对。感觉就像我将解构该命令,检查使用getter检索的属性,并在验证器中比较它们并检查结果。这让我尖叫得像是违反了得墨meter耳法律

放弃提到的验证选项,因为它似乎不可行,似乎应该使用该命令并从中构造聚合。但是这种逻辑应该在哪里存在?是否应该在负责处理具体命令的命令处理程序中?还是应该在命令验证器中(我也不喜欢这种方法)?

我当前正在使用命令,并在负责的命令处理程序中从中创建一个聚合。但是,当我执行此操作时,如果我有一个命令验证器,它将根本不包含任何内容,因为如果该CreateCar命令存在,则它将包含我知道在单独的情况下有效的组件,但集合可能表示不同。


让我们想象一个混合了不同验证过程的场景-使用CreateUser命令创建一个新用户。

该命令包含Id将要创建的一个用户及其Email

系统为用户的电子邮件地址规定以下规则:

  • 必须是唯一的
  • 不能为空,
  • 最多包含100个字符(数据库列的最大长度)。

在这种情况下,即使有一个唯一的电子邮件是一条业务规则,但对其进行汇总检查几乎没有任何意义,因为我需要将系统中的全部当前电子邮件加载到内存中,并在命令中检查该电子邮件(Eeeek!某物,某物,性能)。因此,我将此检查移至命令验证程序,该程序将UserRepository作为依赖项,并使用存储库来检查是否存在使用命令中存在电子邮件的用户。

当涉及到这一点时,突然将其他两个电子邮件规则也放入命令验证器中是有意义的。但是我觉得规则应该确实存在于User聚合中,并且命令验证程序应该只检查唯一性,如果验证成功,我应该继续在中创建User聚合CreateUserCommandHandler并将其传递到要保存的存储库中。

我之所以这样,是因为存储库的save方法很可能会接受一个聚合,该聚合可确保一旦传递了聚合,所有不变量都将得到满足。当逻辑(例如,非空性)仅出现在命令验证本身中时,另一位程序员可以完全跳过此验证,并直接UserRepository使用User对象调用对象中的save方法,这可能导致致命的数据库错误,因为电子邮件可能包含太久了。

您个人如何处理这些复杂的验证和转换?我大多对自己的解决方案感到满意,但是我觉得我需要肯定的是,我对自己的选择并不满意,但我的想法和方法并不完全愚蠢。我完全接受完全不同的方法。如果您有自己尝试过的东西并且为您做得很好,我很乐意看到您的解决方案。


1作为负责创建RESTful系统的PHP开发人员,我对CQRS的解释与标准的异步命令处理方法略有不同,例如有时由于需要同步处理命令而从命令返回结果。


我需要一些示例代码。您的命令对象是什么样的,以及在哪里创建它们?
伊万

@Ewan我将在今天晚些时候或明天添加代码示例。几分钟后出发。
安迪

作为一名PHP程序员,我建议看看我的CQRS + ES实现:github.com/xprt64/cqrs-es
Constantin Galbenu

@ConstantinGALBENU如果我们认为格雷格·扬(Greg Young)对CQRS的解释是正确的(我们可能应该这样做),那么您对CQRS的理解是错误的-至少您的PHP实现是正确的。命令不得直接由集合处理。命令将由命令处理程序处理,该命令处理程序可能会更改聚合,然后更改生成的事件将用于状态复制。
安迪

我认为我们的解释没有不同。您只需要深入研究DDD(在“聚合”的战术层面)或睁大眼睛即可。至少有两种实现CQRS的样式。我用其中之一。我的实现更类似于Actor模型,并使Application层非常薄,这始终是一件好事。我观察到这些应用程序服务中存在很多代码重复,因此决定用替换它们CommandDispatcher
康斯坦丁·加尔贝努

Answers:


22

以下的答案必须是由促进了CQRS风格的背景下cqrs.nu在命令直接对总量到达。在这种体系结构样式中,应用程序服务将由基础结构组件(CommandDispatcher)代替,该基础结构组件可标识聚合,加载聚合,向其发送命令,然后持久存储聚合(如果使用事件源,则作为一系列事件)。

因此,真正的问题是:逻辑到底在哪里?

有多种(验证)逻辑。通常的想法是尽早执行逻辑-如果需要,可以快速失败。因此,情况如下:

  • 命令对象本身的结构;该命令的构造函数具有一些必填字段,这些字段必须存在才能创建命令;这是第一个也是最快的验证;这显然包含在命令中。
  • 低级字段验证,例如某些字段的非空性(例如用户名)或格式(有效的电子邮件地址)。这种验证应包含在命令本身内部的构造函数中。拥有方法的另一种isValid方式,但是对我来说这似乎毫无意义,因为实际上成功的命令实例化就足够了,因此必须记住调用此方法。
  • 单独的command validators类,负责验证命令。当我需要检查来自多个汇总或外部来源的信息时,可以使用这种验证。您可以使用它来检查用户名的唯一性。Command validators可以注入任何依赖项,例如存储库。请记住,此验证最终与聚合一致(即,当创建用户时,可以同时创建另一个具有相同用户名的用户)!另外,不要试图将应该驻留在集合中的逻辑放在这里!命令验证器与Sagas / Process管理器不同,后者基于事件生成命令。
  • 接收和处理命令的聚合方法。这是发生的最后(某种)验证。聚合从命令中提取数据,并使用一些核心业务逻辑接受(拒绝执行状态更改)。以非常一致的方式检查此逻辑。这是最后一道防线。在您的示例中,When a car uses Electric engine the only allowed transmission type is Automatic应在此处检查规则。

我之所以这样,是因为存储库的save方法很可能会接受一个聚合,该聚合可确保一旦传递了聚合,所有不变量都将得到满足。当逻辑(例如,非空性)仅出现在命令验证本身中时,另一个程序员可以完全跳过此验证,并直接使用User对象调用UserRepository中的save方法,这可能导致致命的数据库错误,因为电子邮件可能已经太久了。

使用以上技术,任何人都无法创建无效命令或绕过聚合内的逻辑。命令验证器由加载并自动调用,CommandDispatcher因此没有人可以直接向集合发送命令。可以通过传递命令来调用总体上的方法,但无法持久保存更改,因此这样做毫无意义/无害。

作为负责创建RESTful系统的PHP开发人员,我对CQRS的解释与标准的异步命令处理方法有所不同,例如由于需要同步处理命令,有时会从命令返回结果。

我也是一名PHP程序员,并且我没有从命令处理程序(以形式聚合方法)中返回任何内容handleSomeCommand。但是,我确实经常将信息返回到客户端/浏览器中的信息HTTP response,例如,新创建的聚合根的ID或来自读取模型的某些信息,但我从未(实际上从不)从聚合命令方法返回任何信息。简单的事实就是接受该命令(并对其进行处理-我们正在谈论同步PHP处理,对吗?!)就足够了。

我们将某些内容返回给浏览器(并且仍然按书进行CQRS),因为CQRS不是高级架构

命令验证器如何工作的示例:

命令通过命令验证器到达聚集的路径


关于您的验证策略,第二点让我跳出一个可能经常重复逻辑的地方。肯定有人希望用户汇总以验证非空且格式正确的电子邮件吗?当我们引入ChangeEmail命令时,这一点显而易见。
国王侧滑

@ king-side-slide如果您有一个EmailAddress可以自我验证的值对象,则不会。
康斯坦丁·加尔贝奴

那是完全正确的。可以封装一个EmailAddress以减少重复。不过,更重要的是,这样做还可以将逻辑从命令中移出并移入域中。值得注意的是,这可能太过分了。相似的知识(值对象)通常取决于使用它们的人而具有不同的验证要求。EmailAddress这是一个方便的示例,因为此值的整个概念都具有全局验证要求。
国王侧滑

同样,似乎没有必要使用“命令验证器”。目的不是要防止无效命令的创建和分发。目的是防止它们执行。例如,我可以使用URL传递我想要的任何数据。如果无效,系统将拒绝我的请求。该命令仍将创建并分派。如果一个命令需要多个聚合来进行验证(即一组用户来检查电子邮件的唯一性),那么域服务将是一个更好的选择。诸如“ x验证器”之类的对象通常是贫乏模型的标志,其中数据与行为分离了。
国王侧滑

1
@ king-side-slide一个具体的例子是UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator。您可以看到,这是Orders的一个单独域,因此OrderAggregate本身无法对其进行验证。
康斯坦丁·加尔贝奴

6

DDD的一个基本前提是领域模型可以自我验证。这是一个关键概念,因为它提升了您作为负责确保您的业务规则得到执行的责任方的域名。它还使您的域模型成为开发的重点。

CQRS系统(如您正确指出的那样)是一个实现细节,代表一个通用子域,该子域实现了其自身的内聚机制。您的模型绝不应该依赖任何 CQRS基础架构来根据您的业务规则运行。DDD的目标是对系统行为进行建模,以使结果成为核心业务领域功能需求的有用抽象。将这种行为的任何一部分移出模型,无论多么诱人,都会降低模型的完整性和凝聚力(并降低其实用性)。

仅通过扩展示例以包含ChangeEmail命令,我们就可以完美地说明为什么您不希望在命令基础结构中使用任何业务逻辑,因为您需要复制规则:

  • 电子邮件不能为空
  • 电子邮件不能超过100个字符
  • 电子邮件必须是唯一的

因此,现在我们可以确定我们的逻辑需要在我们的领域内,让我们解决“哪里”的问题。前两个规则可以很容易地应用于我们的User合计,但是最后一个规则则有些细微差别。需要进一步掌握知识以获取更深入的见识的方法。从表面上看,此规则似乎适用于User,但实际上并不适用。电子邮件的“唯一性”适用于Users(根据某些范围)的集合。

啊哈!考虑到这一点,很明显您UserRepository(您的的内存集合Users)可能是实施此不变式的更好人选。“保存”方法可能是包含检查(您可以在其中引发UserEmailAlreadyExists异常)的最合理的位置。或者,UserService可以使域负责创建新Users属性并更新其属性。

快速失败是一种很好的方法,但是只能在适合模型其余部分的时间和地点进行。当您(开发人员)知道调用将在过程的更深处失败时,在进一步处理之前尝试检查应用程序服务方法(或命令)上的参数可能会非常诱人。但是这样做的话,您将以某种方式重复(和泄漏)知识,而当业务规则更改时,可能需要对代码进行多个更新。


2
我同意这一点。我到目前为止的阅读(没有CQRS)告诉我,验证应始终在域模型中进行,以保护不变式。现在,我正在阅读CQRS,它告诉我将验证放入Command对象。这似乎与直觉相反。您是否知道例如在GitHub上将验证放在域模型而不是Command中的任何示例?+1。
w0051977
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.