CQRS 命令和查询 - 它们属于域吗?

CQRS Commands and Queries - Do they belong in the domain?

在 CQRS 中,命令和查询属于域吗?

事件是否也属于域?

如果是这样,Command/Query 处理程序只是基础结构中的实现吗?

现在我的布局是这样的:

Application.Common
Application.Domain
  - Model
    - Aggregate
  - Commands
  - Queries
Application.Infrastructure
  - Command/Query Handlers
  - ...
Application.WebApi
  - Controllers that utilize Commands and Queries

另一个问题,你从哪里引发事件?命令处理程序还是域聚合?

命令和事件是 DTO。您可以在任何 layer/component 中拥有命令处理程序和查询。事件只是发生变化的通知。您可以拥有所有类型的事件:域、应用程序等。

事件可以由处理程序生成,聚合由您决定。但是,无论它们在何处生成,命令处理程序都应使用服务总线来发布事件。我更喜欢在聚合根内部生成域事件。

从DDD战略的角度来看,只有业务概念和用例。领域事件、命令、处理程序是技术细节。然而,所有域用例通常都作为命令处理程序实现,因此命令处理程序应该是域的一部分,查询处理程序也应该是实现域使用的查询的一部分。 UI 使用的查询可以是 UI 等的一部分。

CQRS 的要点是至少有 2 个模型,命令 应该是域模型本身。但是,您可以拥有一个 Query 模型,专门用于域使用,但它仍然是一个读取(简化)模型。考虑命令模型仅用于更新,读取模型仅用于查询。但是,您可以拥有多个读取模型(供特定层或组件使用)或只有一个通用模型(用于所有查询)。

CommandsEvents 可能是非常不同的关注点。它们可以是技术问题、集成问题、领域问题...

我假设如果您询问域,您正在实现一个域模型(甚至可能使用域驱动设计)。

如果是这种情况,我会尽量给你一个非常简单的回答,这样你就可以有一个起点:

  • Command:是一种商业意图,是你想让系统做的事情。保留域中命令的定义。从技术上讲,它只是一个纯粹的 DTO。命令的名称应始终是命令式“PlaceOrder”、“ApplyDiscount” 一个命令仅由一个命令处理程序处理,可以丢弃如果无效(但是您应该在将命令发送到您的域之前进行所有验证,这样它就不会失败)
  • 事件:这是过去发生的事情。对于企业来说,这是无法改变的不变事实。将域事件的定义保留在域中。从技术上讲,它也是一个 DTO 对象。但是,事件的名称应该始终是过去的“OrderPlaced”、“DiscountApplied”。事件一般是pub/sub。一个发布者多个处理者。

If that is the case are the Command/Query Handlers just implementations in the infrastructure?

命令处理程序在语义上类似于应用程序服务层。通常应用服务层负责编排域。它通常围绕业务用例构建,例如 "Placing an Order"。在这些用例中,通过聚合根、查询等调用业务逻辑(应始终封装在域中)。它也是处理 cross cutting concerns 交易、验证、安全等的好地方。

但是,应用层不是强制性的。这取决于功能和技术要求以及已做出的体系结构选择。 你的layring似乎是正确的。我最好将命令处理程序保留在系统的边界。如果没有合适的应用层,命令处理程序可以扮演用例编排器的角色。如果放在 Domain 中,就不能很容易地处理横切关注点。这是一个权衡。您应该了解您的解决方案的优缺点。它可能在一种情况下有效,而在另一种情况下无效。

至于事件处理程序。我一般在

处理
  • 应用层,如果事件触发了同一限界上下文中另一个聚合的修改,或者如果事件触发了一些基础设施服务。
  • 基础设施层,如果事件需要拆分给多个消费者或集成其他有界上下文。

无论如何,你不应该盲目遵守规则。总有权衡取舍,可以找到不同的方法。

Another question, where do you raise events from? The Command Handler or the Domain Aggregate?

我是从域聚合根执行的。因为域负责引发事件。 由于始终存在一条技术规则,即如果在聚合中持久化更改存在问题,则不应发布事件,反之亦然,我采用了事件溯源中使用的方法,这很实用。我的聚合根有一个 Unpublished 事件的集合。在我的存储库的实现中,我将检查 Unpublished 事件的集合并将它们传递给负责发布事件的中间件。很容易控制,如果有异常持久化聚合根,事件不发布。有人说这不是存储库的责任,我同意,但谁在乎呢。有什么选择。是否有笨拙的事件发布代码,这些代码会随着所有基础设施问题(事务、异常处理等)蔓延到您的域中,或者是务实的并在基础设施层中处理所有问题?两者我都做过,相信我,我更喜欢务实。

总而言之,没有单一的做事方式。始终了解您的业务需求和技术要求(可扩展性、性能等)。而不是基于此做出选择。我已经描述了我在大多数情况下通常所做的并且有效。这只是我的意见。

在某些 implementations, Commands and handlers are in the Application layer. In others 中,它们属于域。我在OO系统中经常看到前者,而后者更多的是在功能实现上,这也是我自己做的,不过YMMV。

如果您所说的事件是指领域事件,那么...是的,我建议在领域层中定义它们并从领域对象中发出它们。领域事件是您的通用语言的重要组成部分,例如,如果您练习 Event Storming,领域专家甚至会直接创造它,因此将它们放在那里绝对有意义。

但我认为您应该牢记的是,关于这些技术细节的规则不值得一成不变。关于 SO 上的 DDD 模板项目和分层以及代码“拓扑”有无数的问题,但坦率地说,我认为这些问题对于构建健壮、高性能和可维护的应用程序并不是决定性的,尤其是因为它们非常依赖于上下文。您很可能不会以与 50 人使用的博客发布平台相同的方式为每分钟有数百万次聚合更改的交易系统组织代码,即使两者都是使用 DDD 方法设计的。有时你必须根据自己的情况自己尝试并在过程中学习。