【问题标题】:CQRS: Command Return Values [closed]CQRS:命令返回值
【发布时间】:2017-09-11 23:58:50
【问题描述】:

对于命令是否应该有返回值似乎有无穷无尽的困惑。我想知道这种混淆是否仅仅是因为参与者没有说明他们的背景或情况。

混乱

这里是混淆的例子......

  • Udi Dahan 说命令“不会向客户端返回错误”,但in the same article 他展示了一个图表,其中命令确实向客户端返回了错误。

  • Microsoft Press Store 的一篇文章指出“该命令...不返回响应”,但随后又给出了一个模棱两可的警告:

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

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

答案?

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

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

接受(例如验证)

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

但是,似乎许多从业者不会从命令处理程序中启动验证。从我所见,要么是因为(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.
}

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

重复和总结

我是否正确地理解了为什么在这个主题上存在混淆?我有什么遗漏吗?

更新(2020 年 6 月)

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

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

【问题讨论】:

标签: design-patterns architecture cqrs command-pattern


【解决方案1】:

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

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

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

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

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

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

【讨论】:

  • 创建新对象的命令,不应该返回至少创建对象的id吗?
  • @Greyshack 这大概就是为什么大多数 ES/CQRS 资源建议将 GUID 用于聚合/实体 ID 的原因——客户端可以生成 ID 并将其包含在创建命令中,而不是将其留给后端生成一个 ID。
【解决方案2】:

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

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

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

否则,如果您从命令处理程序返回某些内容,您将赋予它两个职责:1. 修改聚合状态和 2. 查询一些读取模型。

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

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

但是,如果我们更上一层楼,在Presentation layer(即REST 端点)中,Application layer 的客户端,我们可以返回任何内容并且我们不会违反规则,因为端点是设计的在用例之后,您就可以确切地知道在每个用例中执行命令后要返回的内容。

【讨论】:

  • “在每个用例中,您都知道在执行命令后想要返回什么” - 不,我不知道。如果客户端请求具有 CQRS 和内部命令处理程序的服务 A 重新映射一些数据并在后台(代表客户端)调用服务 B,然后返回新创建资源的 ID,如果我将如何将此 ID 返回给客户端服务A的命令没有返回值?
  • @Wirone 通常,在 CQRS 中使用 GUID,因此您甚至在执行命令之前就知道实体的 ID。所以,你知道 ID 并且你知道用例 => 你知道 Readmodel
  • “他们不应该这样做。CQRS 是将 CQS 提升到更高级别。” CQRS 是更高级别的想法,它不关心方法。 CQRS 的主要思想是避免混合写入和读取模型,但命令处理程序可以(必须)返回错误甚至产生事件。
  • @Misanthrope 我同意,请再次阅读我的答案,尤其是最后一段。但是,命令处理程序(只是为了确定:加载聚合的方法,调用命令方法然后持久化聚合)不应该返回任何东西。如果您需要事件,则订阅它们,HTTP 处理程序(而不是命令处理程序!)可以收集这些事件并返回它们。这也适用于例外情况。该死,HTTP 处理程序(或您在命令处理程序之上的任何东西)可以查询 readmodel 或其他东西并返回一些状态。
  • “如果您需要事件,请订阅它们”我认为在这种情况下没有理由这样做。命令处理程序,一般来说,只是将命令转换为事件。在这种情况下获取事件只是更明确的方式。
【解决方案3】:

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

CQRS 的重点是使写/读模型分离;诸如特定方法的返回值之类的低级细节完全无关紧要。

注意以下Fowler's quote

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

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

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

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

格雷格·杨

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

【讨论】:

    【解决方案4】:

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

    @Misanthrope 你到底是怎么处理这些事件的?

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

    在以下情况下非常有用:

    1. 您需要通过事件而不是异常来通知错误。 它通常发生在您的模型需要保存时(例如,它计算错误代码/密码的尝试次数),即使发生错误也是如此。 此外,有些人根本不使用异常来处理业务错误——只使用事件 (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,让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 被缓存/延迟 [不是立即一致],那么您就有问题了)。

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

    【讨论】:

      猜你喜欢
      • 2020-01-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-31
      • 2017-11-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多