CQRS:命令返回值[关闭]


76

关于命令是否应该具有返回值,似乎存在无尽的困惑。我想知道是否只是因为参与者没有说明他们的背景或情况而引起的困惑。

混乱

这是混乱的例子...

  • 乌迪·达汉(Udi Dahan)说,命令“没有将错误返回给客户端”,但在同一篇文章中,他展示了一个图,其中命令的确向客户端返回了错误。

  • Microsoft Press Store上的一篇文章指出“命令...不返回响应”,但随后给出了模糊的警告:

随着围绕CQRS的战场经验的增长,一些实践逐渐巩固并趋于成为最佳实践。在某种程度上与我们刚才所说的相反...今天普遍认为,命令处理程序和应用程序都需要知道事务操作的进行方式。结果必须是已知的...

  • 吉米·博加德(Jimmy Bogard)说“命令总是有结果”,但随后又花了很多功夫来说明命令如何返回无效。

那么,命令处理程序是否返回值?

答案?

从吉米·博加德(Jimmy Bogard)的“ CQRS神话”中获取线索,我认为该问题的答案取决于您在说什么程序化/上下文“象限”:

+-------------+-------------------------+-----------------+
|             | Real-time, Synchronous  |  Queued, Async  |
+-------------+-------------------------+-----------------+
| Acceptance  | Exception/return-value* | <see below>     |
| Fulfillment | return-value            | n/a             |
+-------------+-------------------------+-----------------+

验收(例如验证)

命令“接受”主要是指验证。假定验证结果必须同步地提供给调用方,而不管命令“完成”是同步的还是排队的。

但是,似乎许多从业者并不从命令处理程序中启动验证。从我所看到的,这是因为(1)他们已经找到了一种在应用程序层处理验证的绝佳方法(即,通过数据注释检查有效状态的ASP.NET MVC控制器)或(2)一种体系结构假设命令已提交到(进程外)总线或队列中。后一种异步形式通常不提供同步验证语义或接口。

简而言之,许多设计人员可能希望命令处理程序将验证结果作为(同步)返回值提供,但是他们必须忍受所使用的异步工具的限制。

履行

关于命令的“实现”,发出命令的客户端可能需要知道新创建的记录的scope_identity或故障信息,例如“帐户透支”。

在实时设置中,似乎最有意义的是返回值。异常不应用于传达与业务相关的失败结果。但是,在“排队”上下文中……返回值自然毫无意义。

这是所有混乱之处的总结:

许多(大多数?)CQRS从业者都认为他们现在或将来会合并异步框架或平台(总线或队列),因此宣称命令处理程序没有返回值。但是,某些从业人员无意使用此类事件驱动的构造,因此他们将认可(同步)返回值的命令处理程序。

因此,例如,我相信当Jimmy Bogard提供此示例命令界面时,假定了一个同步(请求-响应)上下文:

public interface ICommand<out TResult> { }

public interface ICommandHandler<in TCommand, out TResult>
    where TCommand : ICommand<TResult>
{
    TResult Handle(TCommand command);
}

毕竟,他的Mediatr产品是一种内存工具。考虑到所有这些,我认为Jimmy花时间从命令中产生空返回的原因不是因为“命令处理程序不应该具有返回值”,而是因为他只是希望Mediator类具有一致的接口:

public interface IMediator
{
    TResponse Request<TResponse>(IQuery<TResponse> query);
    TResult Send<TResult>(ICommand<TResult> query);  //This is the signature in question.
}

...即使不是所有命令都有返回的有意义的值。

重复并总结

我是否正确地说明了为什么对此主题感到困惑?有什么我想念的吗?

更新(6/2020)

在给出答案的帮助下,我认为我已经消除了混乱。简而言之,如果CQRS命令能够返回指示完成状态的成功/失败,则返回值是有意义的。这包括返回新的数据库行标识,或者返回不读取或返回域模型(业务)内容的任何结果。

我认为“ CQRS命令”混乱的出现超出了“异步”的定义和作用。“基于任务的”异步IO与异步体系结构(例如,基于队列的中间件)之间存在很大差异。在前者中,异步“任务”可以并且将为异步命令提供完成结果。但是,发送到RabbitMQ的命令将不会类似地接收请求/响应完成通知。正是异步架构的后一种上下文导致一些人说“没有异步命令之类的东西”或“命令不返回值”。


好的问题,但不太清楚。命令只是VO,而不是方法,它们不返回任何内容。可能您是指命令的处理;请指定级别/层:演示(即休息),应用程序,业务级别(汇总)。
康斯坦丁·加尔贝努

Answers:


21

遵循Vladik Khononov在CQRS解决复杂性中的建议,建议命令处理可以返回与其结果有关的信息。

在不违反任何[CQRS]原则的情况下,命令可以安全地返回以下数据:

  • 执行结果:成功或失败;
  • 错误消息或验证错误,以防万一;
  • 聚合的新版本号(如果成功);

该信息将极大地改善系统的用户体验,因为:

  • 您不必轮询外部源来获得命令执行结果,而马上就可以了。验证命令并返回错误消息变得很简单。
  • 如果要刷新显示的数据,则可以使用聚合的新版本来确定视图模型是否反映了已执行的命令。不再显示过时的数据。

Daniel Whittaker提倡从包含此信息的命令处理程序中返回“公共结果”对象。


4
创建新对象的命令该怎么办,它应该至少不返回所创建对象的ID?
Greyshack

4
@Greyshack这大概就是为什么大多数ES / CQRS资源建议使用GUID来获取聚合/实体ID的原因-客户端可以生成ID并将其包含在创建命令中,而不是将其留给后端来生成ID。
山姆·琼斯

9

那么,命令处理程序是否返回值?

他们不应返回业务数据,而应仅返回元数据(关于执行命令的成功或失败)。CQRSCQS提升到更高的水平。即使您违反纯粹主义者的规则并返回某些东西,您还会得到什么?在CQRS中,命令处理程序是的方法application service,先加载,aggregate然后在上调用方法,aggregate然后持久化aggregate。命令处理程序的目的是修改aggregate。您将不知道要返回的内容与调用者无关。每个命令处理程序调用者/客户端都想了解有关新状态的其他信息。

如果命令执行被阻止(又名同步),那么您只需要知道命令是否成功执行即可。然后,在更高的层中,您将使用最适合您的需求的查询模型查询有关新应用程序状态的确切信息。

否则请考虑,如果您从命令处理程序中返回某些内容,您将承担以下两项责任:1.修改聚合状态和2.查询某些读取模型。

关于命令验证,至少有两种类型的命令验证:

  1. 命令健全性检查,用于验证命令是否具有正确的数据(即,电子邮件地址有效);这是在命令到达集合之前,在命令处理程序(应用程序服务)或命令构造函数中完成的;
  2. 在命令到达聚合(在聚合上调用方法之后)之后,在聚合内部执行域不变式检查,并检查聚合是否可以突变为新状态。

但是,如果我们在的客户端Presentation layer(即REST端点)上进行了某种级别的提升,则Application layer我们可以返回任何内容,并且不会违反规则,因为这些端点是根据用例设计的,因此您确切地知道您想要什么在每种情况下,执行命令后返回。


2
“在每种用例中,您确切知道执行命令后要返回的内容” -不,我不知道。如果客户端请求具有CQRS的服务A,并且内部命令处理程序重新映射一些数据并在后台(代表客户端)调用服务B,然后返回新创建资源的ID,如果我将如何将此ID返回给客户端服务A的命令不返回值?
维罗纳

2
@Wirone通常,在CQRS中使用GUID,因此即使在执行命令之前,您也知道实体的ID 。因此,您知道ID,就知道用例=>您知道了Readmodel
Constantin Galbenu,

2
“他们不应该。CQRS是更高级别的CQS。” CQRS是更高层次的想法,它并不关心方法。CQRS的主要思想是避免混合读写模型,但是命令处理程序可以(必须)返回错误,甚至产生事件。
Misanthrope

2
@Misanthrope我同意,请再次阅读我的答案,尤其是最后一段。但是,命令处理程序(请确保:加载Aggregate的方法,调用命令方法然后持久保存Aggregate的方法)不应返回任何内容。如果您需要事件,请订阅它们,HTTP处理程序(不是Command处理程序!)可以收集这些事件并返回它们。这也适用于例外。糟糕的是,HTTP处理程序(或Command处理程序上方的任何内容)可以查询readmodel或其他内容并返回某些状态。
康斯坦丁·加尔贝努

2
“如果您需要事件,请订阅它们”在这种情况下,我认为没有理由这样做。一般来说,命令处理程序只是将命令转换为事件。在这种情况下,获取事件只是更明确的方式。
Misanthrope

4

CQRS和CQS类似于微服务和类分解:主要思想是相同的(“倾向于小的内聚模块”),但是它们位于不同的语义级别上。

CQRS的重点是使写/读模型分离。诸如特定方法的返回值之类的底层细节是完全不相关的。

注意以下福勒的报价

CQRS引入的更改是将概念模型拆分为单独的模型以进行更新和显示,按照CommandQuerySeparation的词汇分别称为Command和Query。

它是关于模型,而不是方法

命令处理程序可能会返回读取模型以外的任何内容:状态(成功/失败),生成的事件(命令处理程序的主要目标,顺便说一句:为给定命令生成事件),错误。命令处理程序经常抛出未经检查的异常,这是命令处理程序输出信号的示例。

此外,该术语的作者Greg Young表示,命令始终是同步的(否则,它将变为事件):https : //groups.google.com/forum/#!topic/dddcqrs/xhJHVxDx2pM

格雷格·杨(Greg Young)

实际上我说过异步命令不存在:)它实际上是另一个事件。


2

回复@Constantin Galbenu,我面临极限。

@Misanthrope那这些事件您到底怎么办?

@Constantin Galbenu,当然,在大多数情况下,由于命令的缘故,我不需要它们。在某些情况下-我需要在响应此API请求时通知客户端。

在以下情况下非常有用:

  1. 您需要通过事件而不是异常来通知有关错误的信息。通常会在需要保存模型的情况下发生这种情况(例如,它会计算错误代码/密码的尝试次数),即使发生错误也是如此。另外,有些人根本不将异常用于业务错误,仅是事件(http://andrzejonsoftware.blogspot.com/2014/06/custom-exceptions-or-domain-events.html没有特殊原因认为从命令处理程序中抛出业务异常是可以的,但返回域事件则不可行
  2. 当事件仅在总根内部的某些情况下发生时。

我可以提供第二种情况的示例。想象我们提供了类似Tinder的服务,我们有LikeStranger命令。如果我们喜欢以前已经喜欢我们的人,则此命令可能导致StrangersWereMatched。我们需要通知移动客户端以响应是否匹配。如果您只想在命令后检查matchQueryService,则可能会找到匹配项,但现在不保证匹配已发生,因为SOMETIMES Tinder显示已匹配的陌生人(可能在人口稀少的地区,可能不一致,也许您只是第二设备等)。

检查StrangersWereMatched是否真的现在发生的响应非常简单:

$events = $this->commandBus->handle(new LikeStranger(...));

if ($events->contains(StrangersWereMatched::class)) {
  return LikeApiResponse::matched();
} else {
  return LikeApiResponse::unknown();
}

是的,例如,您可以引入命令ID,并使Match读取模型以使其保持:

// ...

$commandId = CommandId::generate();

$events = $this->commandBus->handle(
  $commandId,
  new LikeStranger($strangerWhoLikesId, $strangerId)
);

$match = $this->matchQueryService->find($strangerWhoLikesId, $strangerId);

if ($match->isResultOfCommand($commandId)) {
  return LikeApiResponse::matched();
} else {
  return LikeApiResponse::unknown();
}

……但是考虑一下:为什么您认为具有直接逻辑的第一个示例更糟糕?无论如何它都没有违反CQRS,我只是将隐式明确了。这是无状态的不变方法。遇到错误的机会更少(例如,如果matchQueryService被缓存/延迟[不立即一致],则您有问题)。

是的,如果匹配的事实还不够,并且您需要获取数据以进行响应,则必须使用查询服务。但是没有什么可以阻止您从命令处理程序接收事件。

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.