DDD 和 CQRS:从单个命令处理程序使用多个存储库?
DDD and CQRS: use multiple repositories from a single command handler?
简单电子商店的典型示例。
假设用户将一些商品添加到购物车并点击“结帐”。发出“创建订单”命令。现在,在实际创建状态为“预期付款”的订单记录和数据库中相应的订单行之前,我们必须检查用户选择的商品是否仍然可用(当用户将它们添加到购物车时,可能有些商品可用但现在不是了)。而且我们还要保留它们,这样它们就不会在用户还在结账的时候突然消失。
所以我的问题是如何执行这个“检查和保留”程序?在我看来,我有多种选择:
- 在“创建订单”命令处理程序中使用
ProductStockRepository
预订产品,然后在成功时使用 OrderRepository
创建订单。意思是,我们在单个处理程序中使用多个存储库。
- 不要直接在“创建订单”处理程序中使用
ProductStockRepository
,而是创建一个 ProductStockService
并在其上调用方法来检查和保留产品。我们仍然在单个处理程序中使用多个存储库,但是对库存存储库的使用进行了抽象。
- 创建一个内部“保留产品”命令并从“创建订单”命令处理程序内部调度和等待它。
- “结帐”按钮发送“预订产品”命令而不是“创建订单”。在“Reserve Products”处理程序中,我们尝试保留产品并在成功时调用“Products Reserved”域事件。触发相应的事件处理程序,我们在其中创建订单。
- 其他方式?
这不是关于如何最好地模拟电子商店结帐流程的问题。以上只是一个例子。我想在许多不同的应用程序中可能会有许多类似的场景。
您提出的问题的解决方案与编码“风格”或遵循良好的 DDD 实践无关。如果在单个处理程序中使用多个存储库解决了您的问题,我相信您应该认为这是一个不错的选择。
但这种情况的主要问题是在许多系统中,订单和库存在不同的 Services/Bounded 上下文中,因此在不同的数据库中。库存甚至可能位于不受您控制的外部系统中。这意味着您无法保留库存并以交易方式下订单,因此您可能会保留库存而不下订单,反之亦然。
之所以建议使用事件来处理这些场景,是因为使用事件可以可靠地开发这种类型的工作流,尽管这会带来新的复杂性。通过一些技术,可以可靠地保留库存并发布一个事件,另一方面,可靠地捕获付款并发布另一个事件,然后下订单并发布另一个事件等。这个工作流程可能涉及诸如发件箱模式、重试、sagas、补偿操作(在一个步骤失败的情况下回滚之前的步骤)等
命令应该更新单个聚合,否则您将违反“聚合”契约,只要这是真的,您可以在处理程序中执行您想要的读取次数。
事件是针对此类情况的最一致的解决方案,但您以这种方式编写软件需要付出复杂性的代价。
你应该使用存储库还是服务,这取决于你正在读取的数据是处理程序(存储库)的同一有界上下文的一部分还是在不同的处理程序(服务)中。
引入 ReserveProduct 命令,您正在定义域行为,并且在技术上如何做事是一个不同的问题,您可能想做或不想做,但这取决于域。
如其他答案中所述,您不希望 CommandHandler 维护多个聚合。这应该委托给一个 DomainService,通过 DomainEvents 来实现,或者将 Products 传递到 Order 聚合中来维护。解决方案还取决于预订流程和订单流程是否在同一个限界上下文中。
同一 BC 中的预订和订购,选项 1(域服务):
- CreateOrderCommand 已调度
- 基础设施开始交易
- CreateOrderCommandHandler 调用 CreateOrderDomainService
-
- CreateOrderDomainService 从存储库中检索产品并尝试保留,失败时抛出
-
- CreateOrderDomainService 尝试创建订单并添加到存储库,失败时抛出
- 如果没有错误,基础设施提交。
- 如果出现错误,基础架构将中止。
同一 BC 中的预订和订单,选项 2(领域事件):
- CreateOrderCommand 已调度
- 基础设施开始交易
- CreateOrderCommandHandler 创建订单并添加到存储库
- 订单创建域事件“OrderCreated”
- OrderCreatedEventHandler 从存储库中检索产品并尝试保留
- 如果没有错误,基础设施提交。
- 如果出现错误,基础架构将中止。
同一 BC 中的预订和订购,选项 3(插入产品):
- CreateOrderCommand 已调度
- 基础设施开始交易
- CreateOrderCommandHandler 检索订单中使用的产品
- CreateOrderCommandHandler 创建传入所有产品的订单
- 订单尝试使用产品域实体预订产品,失败
- 如果没有错误,基础设施提交。
- 如果出现错误,基础架构将中止。
不同 BC 中的预订和订购,选项 1(域服务):
- CreateOrderCommand 已调度
- 基础设施开始交易
- CreateOrderCommandHandler 调用 CreateOrderDomainService
-
- CreateOrderDomainService 将 ReserveProduct 命令分派给 Reservations BC 并等待。
- Reservations BC 完成预订,失败时抛出
-
- CreateOrderDomainService 尝试创建订单并添加到存储库
- 如果失败,则通过将 UnreserveProduct 命令发送到 Reservations BC 进行补偿,然后抛出。
- 如果没有错误,基础设施提交。
- 如果出现错误,基础架构将中止。
不同 BC 中的预订和订购,选项 2(领域事件):
- CreateOrderCommand 已调度
- 基础设施开始交易
- CreateOrderCommandHandler 创建订单并添加到存储库
- 订单创建域事件“OrderCreated”
- OrderCreatedEventHandler 将 ReserveProduct 命令分派给 Reservations BC 并等待。
- 如果失败,则抛出。
- 如果没有错误,基础设施提交。
- 如果出现错误,基础架构将中止。
在所有情况下,在产品上使用并发令牌以防止并发 'over-reservation'。
简单电子商店的典型示例。
假设用户将一些商品添加到购物车并点击“结帐”。发出“创建订单”命令。现在,在实际创建状态为“预期付款”的订单记录和数据库中相应的订单行之前,我们必须检查用户选择的商品是否仍然可用(当用户将它们添加到购物车时,可能有些商品可用但现在不是了)。而且我们还要保留它们,这样它们就不会在用户还在结账的时候突然消失。
所以我的问题是如何执行这个“检查和保留”程序?在我看来,我有多种选择:
- 在“创建订单”命令处理程序中使用
ProductStockRepository
预订产品,然后在成功时使用OrderRepository
创建订单。意思是,我们在单个处理程序中使用多个存储库。 - 不要直接在“创建订单”处理程序中使用
ProductStockRepository
,而是创建一个ProductStockService
并在其上调用方法来检查和保留产品。我们仍然在单个处理程序中使用多个存储库,但是对库存存储库的使用进行了抽象。 - 创建一个内部“保留产品”命令并从“创建订单”命令处理程序内部调度和等待它。
- “结帐”按钮发送“预订产品”命令而不是“创建订单”。在“Reserve Products”处理程序中,我们尝试保留产品并在成功时调用“Products Reserved”域事件。触发相应的事件处理程序,我们在其中创建订单。
- 其他方式?
这不是关于如何最好地模拟电子商店结帐流程的问题。以上只是一个例子。我想在许多不同的应用程序中可能会有许多类似的场景。
您提出的问题的解决方案与编码“风格”或遵循良好的 DDD 实践无关。如果在单个处理程序中使用多个存储库解决了您的问题,我相信您应该认为这是一个不错的选择。
但这种情况的主要问题是在许多系统中,订单和库存在不同的 Services/Bounded 上下文中,因此在不同的数据库中。库存甚至可能位于不受您控制的外部系统中。这意味着您无法保留库存并以交易方式下订单,因此您可能会保留库存而不下订单,反之亦然。
之所以建议使用事件来处理这些场景,是因为使用事件可以可靠地开发这种类型的工作流,尽管这会带来新的复杂性。通过一些技术,可以可靠地保留库存并发布一个事件,另一方面,可靠地捕获付款并发布另一个事件,然后下订单并发布另一个事件等。这个工作流程可能涉及诸如发件箱模式、重试、sagas、补偿操作(在一个步骤失败的情况下回滚之前的步骤)等
命令应该更新单个聚合,否则您将违反“聚合”契约,只要这是真的,您可以在处理程序中执行您想要的读取次数。
事件是针对此类情况的最一致的解决方案,但您以这种方式编写软件需要付出复杂性的代价。
你应该使用存储库还是服务,这取决于你正在读取的数据是处理程序(存储库)的同一有界上下文的一部分还是在不同的处理程序(服务)中。
引入 ReserveProduct 命令,您正在定义域行为,并且在技术上如何做事是一个不同的问题,您可能想做或不想做,但这取决于域。
如其他答案中所述,您不希望 CommandHandler 维护多个聚合。这应该委托给一个 DomainService,通过 DomainEvents 来实现,或者将 Products 传递到 Order 聚合中来维护。解决方案还取决于预订流程和订单流程是否在同一个限界上下文中。
同一 BC 中的预订和订购,选项 1(域服务):
- CreateOrderCommand 已调度
- 基础设施开始交易
- CreateOrderCommandHandler 调用 CreateOrderDomainService
-
- CreateOrderDomainService 从存储库中检索产品并尝试保留,失败时抛出
-
- CreateOrderDomainService 尝试创建订单并添加到存储库,失败时抛出
-
- CreateOrderCommandHandler 调用 CreateOrderDomainService
- 如果没有错误,基础设施提交。
- 如果出现错误,基础架构将中止。
- 基础设施开始交易
同一 BC 中的预订和订单,选项 2(领域事件):
- CreateOrderCommand 已调度
- 基础设施开始交易
- CreateOrderCommandHandler 创建订单并添加到存储库
- 订单创建域事件“OrderCreated”
- OrderCreatedEventHandler 从存储库中检索产品并尝试保留
- 如果没有错误,基础设施提交。
- 如果出现错误,基础架构将中止。
- 基础设施开始交易
同一 BC 中的预订和订购,选项 3(插入产品):
- CreateOrderCommand 已调度
- 基础设施开始交易
- CreateOrderCommandHandler 检索订单中使用的产品
- CreateOrderCommandHandler 创建传入所有产品的订单
- 订单尝试使用产品域实体预订产品,失败
- 如果没有错误,基础设施提交。
- 如果出现错误,基础架构将中止。
- 基础设施开始交易
不同 BC 中的预订和订购,选项 1(域服务):
- CreateOrderCommand 已调度
- 基础设施开始交易
- CreateOrderCommandHandler 调用 CreateOrderDomainService
-
- CreateOrderDomainService 将 ReserveProduct 命令分派给 Reservations BC 并等待。
- Reservations BC 完成预订,失败时抛出
-
- CreateOrderDomainService 尝试创建订单并添加到存储库
- 如果失败,则通过将 UnreserveProduct 命令发送到 Reservations BC 进行补偿,然后抛出。
-
- CreateOrderCommandHandler 调用 CreateOrderDomainService
- 如果没有错误,基础设施提交。
- 如果出现错误,基础架构将中止。
- 基础设施开始交易
不同 BC 中的预订和订购,选项 2(领域事件):
- CreateOrderCommand 已调度
- 基础设施开始交易
- CreateOrderCommandHandler 创建订单并添加到存储库
- 订单创建域事件“OrderCreated”
- OrderCreatedEventHandler 将 ReserveProduct 命令分派给 Reservations BC 并等待。
- 如果失败,则抛出。
- 如果没有错误,基础设施提交。
- 如果出现错误,基础架构将中止。
- 基础设施开始交易
在所有情况下,在产品上使用并发令牌以防止并发 'over-reservation'。