CQRS:命令 Return 值

CQRS: Command Return Values

关于命令是否应该具有 return 值,似乎存在无穷无尽的困惑。我想知道混淆是否仅仅是因为参与者没有说明他们的背景或情况。

混乱

以下是混淆的示例...

As battlefield experience grows around CQRS, some practices consolidate and tend to become best practices. Partly contrary to what we just stated... it is a common view today to think that both the command handler and the application need to know how the transactional operation went. Results must be known...

嗯,命令处理程序 return 值与否?

答案?

从 Jimmy Bogard 的“CQRS Myths”中得到启发,我认为这个问题的答案取决于 programmatic/contextual 您所说的“象限”:

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

接受(例如验证)

命令“接受”主要是指验证。据推测,验证结果必须同步地提供给调用者,无论命令“fulfillment”是同步的还是排队的。

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

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

履行

关于命令的“执行”,发出命令的客户端可能需要知道 scope_identity 新创建的记录或失败信息 - 例如“帐户透支”。

在实时设置中,return 值似乎最有意义;不应使用异常来传达与业务相关的失败结果。但是,在“排队”上下文中...return 值自然没有意义。

这里或许可以总结所有的困惑:

Many (most?) CQRS practitioners assume they will now, or in the future, incorporate an asynchrony framework or platform (a bus or queue) and thus proclaim that command handlers do not have return values. However, some practitioners have no intention of using such event-driven constructs, and so they will endorse command handlers that (synchronously) return values.

因此,例如,我相信当 Jimmy Bogard provided this sample command interface:

时假定同步(请求-响应)上下文
public interface ICommand<out TResult> { }

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

毕竟,他的 Mediatr 产品是一种内存工具。考虑到这一切,我认为 Jimmy carefully took the time to produce a void return from a command 的原因不是因为“命令处理程序不应该有 return 值”,而是因为他只是希望他的 Mediator class 有一个一致的接口:

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

...即使并非所有命令都对 return 具有有意义的价值。

重复并结束

我是否正确地抓住了为什么在这个主题上存在混淆?有什么我想念的吗?

更新 (6/2020)

在给出的答案的帮助下,我想我已经解开了困惑。简而言之,如果 CQRS 命令能够 returning success/failure 指示 完成 状态,那么 return 值就有意义了。这包括 return 新的数据库行标识,或任何不读取或 return 域模型(业务)内容的结果。

我认为“CQRS 命令”出现混淆的地方在于“异步”的定义和作用。 “基于任务的”异步 IO 和异步架构(例如基于队列的中间件)之间存在很大差异。在前者中,异步“任务”可以并且将为异步命令提供完成结果。但是,发送到 RabbitMQ 的命令不会同样收到 request/reponse 完成通知。正是后一种异步架构的上下文导致一些人说“没有异步命令这样的东西”或“命令没有 return 值。”

按照 Vladik Khononov 在 Tackling Complexity in CQRS 中的建议,命令处理可以 return 与其结果相关的信息。

Without violating any [CQRS] principles, a command can safely return the following data:

  • Execution result: success or failure;
  • Error messages or validation errors, in case of a failure;
  • The aggregate’s new version number, in case of success;

This information will dramatically improve the user experience of your system, because:

  • You don’t have to poll an external source for command execution result, you have it right away. It becomes trivial to validate commands, and to return error messages.
  • If you want to refresh the displayed data, you can use the aggregate’s new version to determine whether the view model reflects the executed command or not. No more displaying stale data.

Daniel Whittaker 提倡return从包含此信息的命令处理程序中获取“common result”对象。

Well, do command handlers return values or not?

它们不应该 return 业务数据 ,只有元数据(关于执行命令的成功或失败)。 CQRSCQS 更上一层楼。即使你会打破纯粹主义者的规则和 return 一些东西,你会 return 什么?在 CQRS 中,命令处理程序是 application service 的一种方法,它加载 aggregate 然后调用 aggregate 上的方法,然后它持久化 aggregate。命令处理程序的目的是修改 aggregate。您不会知道要 return 独立于调用者的内容。每个命令处理程序 caller/client 都想知道关于新状态的其他信息。

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

否则,如果您 return 来自命令处理程序的内容,您会赋予它两项职责:1. 修改聚合状态和 2. 查询一些读取模型。

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

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

但是,如果我们更上一层楼,在 Presentation layer(即 REST 端点)Application layer 的客户端,我们可以 return 任何东西而且我们不会违反规则,因为端点是根据用例设计的,在每个用例中,在执行命令后,您确切地知道自己想要什么 return。

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

CQRS 的重点是write/read 模型分离; return 来自特定方法的值这样的低级细节完全无关紧要。

注意以下Fowler's quote

The change that CQRS introduces is to split that conceptual model into separate models for update and display, which it refers to as Command and Query respectively following the vocabulary of CommandQuerySeparation.

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

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

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

Greg Young

actually I said that an asynchronous command doesn't exist :) its actually another event.

回复@Constantin Galbenu,我遇到了限制。

@Misanthrope And what exactly do you do with those events?

@Constantin Galbenu,在大多数情况下,我当然不需要它们作为命令的结果。在某些情况下——我需要通知客户以响应此 API 请求。

在以下情况下非常有用:

  1. 您需要通过事件而不是异常来通知错误。 它通常发生在您的模型需要保存时(例如,它计算错误 code/password 的尝试次数),即使发生了错误。 此外,有些人根本不使用异常来处理业务错误——仅使用事件 (http://andrzejonsoftware.blogspot.com/2014/06/custom-exceptions-or-domain-events.html) 没有特别的理由认为从命令处理程序中抛出业务异常是可以的,但返回领域事件是不行的
  2. 当事件仅在聚合根内部的某些情况下发生时。

我可以提供第二种情况的例子。 想象一下我们制作类似 Tinder 的服务,我们有 LikeStranger 命令。 如果我们喜欢之前已经喜欢过我们的人,这个命令可能会导致 StrangersWereMatched。 我们需要响应通知移动客户端是否发生匹配。 如果你只是想在命令后检查 matchQueryService,你可能会在那里找到匹配,但不能保证匹配现在已经发生, 因为有时 Tinder 会显示已经匹配的陌生人(可能在人烟稀少的地区,可能不一致,可能你只有第二台设备,等等)。

如果现在真的发生了 StrangersWereMatched,检查响应非常简单:

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

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

是的,你可以引入command id,比如,make Match read model来保留它:

// ...

$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 是 cached/delayed [不立即一致],你就有问题了)。

是的,当事实匹配不够,需要获取数据响应时,就不得不使用查询服务。 但是没有什么能阻止您从命令处理程序接收事件。