CQRS:命令返回值[关闭]

问题描述 投票:51回答:4

关于命令是否应该具有返回值似乎存在无穷无尽的混淆。我想知道这种混乱是否仅仅是因为参与者没有陈述他们的背景或情况。

The Confusion

以下是混淆的例子......

  • Udi Dahan说命令“没有向客户端返回错误”,但是in the same article他显示了一个图表,其中命令确实将错误返回给客户端。
  • 微软新闻商店文章指出“命令......没有返回响应”,但接着提出了一个含糊不清的警告:

随着围绕CQRS的战场经验的增长,一些实践巩固并倾向于成为最佳实践。与我们刚才所说的部分相反......今天人们普遍认为命令处理程序和应用程序都需要知道事务操作是如何进行的。必须知道结果......

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

The Answer?

从Jimmy Bogard的“CQRS Myths”中得到启示,我认为这个问题的答案取决于你所说的程序/语境“象限”:

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

Acceptance (e.g. validation)

命令“Acceptance”主要指验证。推测验证结果必须与调用者同步,无论命令“履行”是同步还是排队。

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

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

Fulfillment

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

在实时设置中,似乎返回值最有意义;不应使用例外来传达与业务相关的失败结果。但是,在“排队”环境中......返回值自然没有意义。

这是所有困惑可能总结的地方:

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

因此,例如,我相信当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之所以不是因为“命令处理程序不应该有返回值”,而是因为他只是希望他的Mediator类具有一致的接口:

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

...即使并非所有命令都具有有意义的返回值。

Repeat and Wrap Up

我是否正确地捕捉到为什么这个主题存在混淆?有什么我想念的吗?

design-patterns architecture cqrs command-pattern
4个回答
14
投票

根据Vladiz Khononov在Tackling Complexity in CQRS的建议,建议命令处理可以返回与其结果有关的信息。

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

  • 执行结果:成功或失败;
  • 发生故障时出现错误消息或验证错误;
  • 汇总的新版本号,如果成功;

此信息将显着改善系统的用户体验,因为:

  • 您不必轮询外部源以获取命令执行结果,您可以立即使用它。验证命令和返回错误消息变得微不足道。
  • 如果要刷新显示的数据,可以使用聚合的新版本来确定视图模型是否反映执行的命令。不再显示陈旧数据。

Daniel Whittaker主张从包含此信息的命令处理程序返回“common result”对象。


4
投票

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

他们不应该返回业务数据,只返回元数据(关于执行命令的成功或失败)。 CQRSCQS被提升到更高的水平。即使你打破纯粹主义者的规则并归还某些东西,你会回报什么?在CQRS中,命令处理程序是application service的一种方法,它加载aggregate然后调用aggregate上的方法然后它持续aggregate。命令处理程序的意图是修改aggregate。您不知道返回什么将独立于调用者。每个命令处理程序调用者/客户端都想知道有关新状态的其他信息。

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

如果你从命令处理程序返回一些东西,你会给它两个职责:1。修改聚合状态,2。查询一些读模型。

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

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

然而,如果我们升级一些,在Presentation layer(即REST端点),Application layer的客户端,我们可以返回任何东西,我们不会破坏规则,因为端点是在用例之后设计的,你确切知道什么在每个用例中,您希望在执行命令后返回。


1
投票

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

CQRS的目的是使写/读模型分离;像特定方法的返回值这样的低级细节是完全无关紧要的。

请注意以下Fowler's quote

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

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

除了读取模型之外,命令处理程序可以返回任何内容:状态(成功/失败),生成的事件(它是命令处理程序的主要目标,btw:为给定命令生成事件),错误。命令处理程序经常抛出未经检查的异常,它是来自命令处理程序的输出信号的示例。

此外,该术语的作者Greg Young说,命令总是同步的(否则,它就变成了事件):https://groups.google.com/forum/#!topic/dddcqrs/xhJHVxDx2pM

格雷格杨

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


0
投票

回复@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缓存/延迟[不是立即一致],则表示您遇到问题)。

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

© www.soinside.com 2019 - 2024. All rights reserved.