我应该在控制器和域服务之间放置命令总线吗?

Should I put command bus between controller and domain service?

我正在后端工作并尝试实现 CQRS 模式。 我对事件很清楚,但有时会遇到命令。

我看到命令是用户请求的,例如 ChangePasswordCommand。然而在实现级别用户只是调用一个端点,由一些控制器处理。

我可以向我的控制器注入一个 UserService,它将处理领域逻辑,这就是基本教程的做法(我使用 Nest.js)。但是我觉得也许这是我应该使用命令的地方——所以我应该在我的控制器中执行命令 ChangePasswordCommand 然后域模块会处理它吗?

重要的是我需要来自命令的 return 值,从实现的角度来看这不是问题,但它在 CQRS 方面看起来不太好 - 我应该同时添加和获取时间.

或者最后一个选项可能是在控制器中执行命令,然后在命令处理程序中发出事件 (PasswordChangedEvent)。接下来,等到事件返回并且return控制器中的值。

最后一个选项对我来说似乎很好,但我在请求生命周期内的清晰实现方面遇到了问题。

我根据 https://docs.nestjs.com/recipes/cqrs

随着架构的发展,如果您使用 Processes/Sagas 来管理工作流和 inter-aggregate 通信,您可能会发现需要命令总线。如果是这种情况,那么使用该总线执行所有命令自然是有意义的。

以下是我更喜欢的方法:

execute the command in controller and then emit an event (PasswordChangedEvent) in command handler. Next, wait till event comes back and return the value in controller.

至于实现细节,在 .NET 中,我们使用 SignalR websockets 服务,该服务将读取事件总线(发布所有事件的地方)并将事件转发给已订阅它们的客户端。

在这种情况下,工作流程将是:

  1. 用户向控制器发帖。
  2. 控制器将命令附加到命令总线。
  3. 控制器 returns 标识命令的 ID。
  4. 客户端(浏览器客户端)订阅与此命令相关的事件。
  5. 命令由域服务接收并处理。一个事件被发送到事件存储。
  6. 事件发布到事件总线。
  7. 事件侦听器订阅服务接收事件,找到订阅,并将事件发送到客户端。
  8. 客户端接收事件并通知用户。

虽然@cperson 的 在技术上是正确的,但我想添加一些细微差别。

首先,答案描述可能不清楚,它建议“在命令处理程序中发出一个事件 (PasswordChangedEvent)”。这也是我想要的,但请注意:

  • Command 是基础设施层的一部分,Event 是域的一部分。
  • 因此,您应该根据命令在发出事件的 AggregateRoot 上触发代码。
  • 这可以用 mergeObjectContexteventBus.publish 来完成(参见 NestJS docs)。
  • 可以从其他域对象应用事件,但聚合通常是发射器(在提交时)。

我想说明的另一点是假定事件源架构,即应用 CQRS/ES。虽然 CQRS 通常与事件溯源结合使用,但没有任何规定这样做。事件溯源可以提供额外的优势,但也带来了显着增加的复杂性。你应该仔细权衡拥有ES的利弊。

在许多情况下,您不需要事件溯源。只有 CQRS 已经给你带来了很多好处,例如让你的域/限界上下文得到很好的包含。读写分离、单一职责命令 + 查询(通常更 SOLID)、更简洁的架构等。在更高级别上,更容易将焦点从 'how do I implement this (CRUD-wise)?' 转移, 至 'how do these user requirements fit in the domain model?'.

没有 ES,你可以有一个单一的关系数据库,例如坚持使用 TypeORM。您可以保留事件,但这不是必需的。在许多情况下,您可以避免客户端需要订阅事件的最终一致性(也许您只是使用它们来驱动 saga 并更新读取端 views/projections)。

您始终可以仅从 CQRS 开始,然后在需要时添加事件溯源。