关于命令是否应该返回值似乎存在无穷无尽的混淆。我想知道这种困惑是否仅仅是因为参与者没有说明他们的背景或情况。
以下是混淆的例子......
Udi Dahan说命令“没有向客户端返回错误”,但是in the same article他显示了一个图表,其中命令确实将错误返回给客户端。
微软新闻商店文章指出“命令......不会返回响应”,但接着提出了一个模棱两可的警告:
随着CQRS的战场经验不断增长,一些实践会巩固并趋向于成为最佳实践。部分地与我们刚才所说的相反......今天人们普遍认为命令处理程序和应用程序都需要知道事务操作是如何进行的。必须知道结果......
那么,命令处理程序是否返回值?
从吉米·博加德的“CQRS Myths”中得到启示,我认为这个问题的答案取决于你所说的程序/语境“象限”:
+-------------+-------------------------+-----------------+
| | Real-time, Synchronous | Queued, Async |
+-------------+-------------------------+-----------------+
| Acceptance | Exception/return-value* | <see below> |
| Fulfillment | return-value | n/a |
+-------------+-------------------------+-----------------+
命令“Acceptance”主要指验证。推测验证结果必须与调用者同步,无论命令“履行”是同步还是排队。
但是,似乎许多从业者不会在命令处理程序中启动验证。从我所看到的,它要么是因为(1)他们已经找到了在应用层处理验证的绝妙方法(即ASP.NET MVC控制器通过数据注释检查有效状态)或者(2)架构假设命令被提交给(进程外)总线或队列。后面这些异步形式通常不提供同步验证语义或接口。
简而言之,许多设计人员可能希望命令处理程序将验证结果作为(同步)返回值提供,但它们必须遵守它们使用的异步工具的限制。
关于命令的“履行”,发出命令的客户端可能需要知道新创建的记录的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.
}
...即使并非所有命令都有一个有意义的值返回。
我是否正确地捕捉到为什么这个主题存在混淆?有什么我想念的吗?
答案 0 :(得分:13)
遵循Vladik Khononov在Tackling Complexity in CQRS中的建议,建议命令处理可以返回与其结果相关的信息。
在不违反任何[CQRS]原则的情况下,命令可以安全地返回以下数据:
- 执行结果:成功或失败;
- 发生故障时出现错误消息或验证错误;
- 汇总的新版本号,如果成功;
此信息将显着改善您系统的用户体验,因为:
- 您不必轮询外部源以获取命令执行结果,您可以立即使用它。验证命令并返回错误消息变得微不足道。
- 如果要刷新显示的数据,可以使用聚合的新版本来确定视图模型是否反映执行的命令。不再显示陈旧数据。
Daniel Whittaker主张从包含此信息的命令处理程序返回“common result”对象。
答案 1 :(得分:4)
那么,命令处理程序是否返回值?
他们不应该返回业务数据,只返回元数据(关于执行命令的成功或失败)。 CQRS
被CQS提升到更高级别。即使你打破纯粹主义者的规则并归还某些东西,你会回报什么?在CQRS中,命令处理程序是application service
的一种方法,它加载aggregate
然后调用aggregate
上的方法然后它会持续aggregate
。命令处理程序的意图是修改aggregate
。你不会知道返回什么是独立于调用者的。每个命令处理程序调用者/客户端都想知道关于新状态的其他信息。
如果命令执行被阻塞(也就是同步),那么你需要知道命令是否成功执行。然后,在更高层中,您将使用最适合您需求的查询模型查询您需要了解的有关新应用程序状态的确切内容。
另外想想,如果从命令处理程序返回一些东西,你就要赋予它两个职责:1。修改聚合状态,2。查询一些读模型。
关于命令验证,至少有两种类型的命令验证:
但是,如果我们进一步升级,在Presentation layer
(即REST
端点),Application layer
的客户端,我们可以返回任何内容而我们不会&#39;打破规则是因为端点是在用例之后设计的,在每个用例中,您确切地知道在执行命令后要返回的内容。
答案 2 :(得分:1)
CQRS和CQS类似于微服务和类分解:主要思想是相同的(“趋向于小的内聚模块”),但是它们位于不同的语义级别。
CQRS的要点是使写/读模型分离;诸如特定方法的返回值之类的底层细节是完全不相关的。
注意以下Fowler's quote:
CQRS引入的更改是将概念模型拆分为单独的模型以进行更新和显示,按照CommandQuerySeparation的词汇分别称为Command和Query。
这是关于模型的,而不是方法。
命令处理程序可能会返回读取模型以外的任何内容:状态(成功/失败),生成的事件(命令处理程序的主要目标,顺便说一句:为给定命令生成事件),错误。命令处理程序经常会引发未经检查的异常,这是命令处理程序输出信号的示例。
此外,术语Greg Young的作者说,命令始终是同步的(否则,它将变为事件): https://groups.google.com/forum/#!topic/dddcqrs/xhJHVxDx2pM
格雷格·杨
实际上我说异步命令不存在:) 这实际上是另一件事。
答案 3 :(得分:0)
回复@Constantin Galbenu,我遇到了限制。
@Misanthrope那这些事件您到底怎么办?
@Constantin Galbenu,当然,在大多数情况下,由于命令的缘故,我不需要它们。在某些情况下-我需要在响应此API请求时通知客户端。
在以下情况下非常有用:
我可以提供第二种情况的示例。 想象我们提供了类似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,我只是使隐式明确。 这是无状态的不变方法。遇到错误的机会更少(例如,{/ {1}}被缓存/延迟[不是立即一致],则说明您有问题)。
是的,如果匹配的事实还不够,并且您需要获取数据进行响应,则必须使用查询服务。 但是没有什么可以阻止您从命令处理程序接收事件。