调用其他限界上下文的策略

Strategies to call other bounded context

我目前正在研究一个涉及领域驱动设计 (DDD) 和多领域集成场景的研究项目。

我的一个限界上下文中有一个用例,我需要联系另一个 BC 来验证聚合。事实上,将来可能会有多个BC要求验证数据(但现在没有)。

现在,我患有DDD强迫症神经衰弱,无法找到正确应用模式的方法(笑)。我非常感谢人们对此的一些反馈。


About the 2 bounded contexts.
- The first one (BC_A) where the use case is taking place would contain a list of elements that are related to the user.
- The external one (BC_B) has some knowledge about those elements

* So, a validation request from BC_A to BC_B would ask a review of all elements of the aggregate from BC_A, and would return a report containing some specifications about what to do with those elements (if we should keep it or not, and why).
*The state of the aggregate would pass through (let say) "draft" then "validating" after a request, and then depending on the report sent back, it would be "valid" or "has_error" in case there is one. If the user later choose to not follow the spec, it could pass the state of the aggregate to "controlled" meaning there is some error but we do not taking care of it.

命令是ValidateMyAggregateCommand

用例是:

  1. 通过id获取目标聚合
  2. 将其状态更改为 "validating"
  3. 坚持聚合
  4. 进行验证调用(到另一个 BC)
  5. 保留验证报告
  6. 确认目标聚合的验证报告(将根据结果再次改变其状态,应该是"OK"或"HAS_ERROR")
  7. 再次持久化聚合
  8. 根据验证结果生成领域事件

它包含 8 个步骤,可能有 1 到 3 个交易或更多。


我需要在本地保留验证报告(以便在 UI 中访问它),我想我可以做到:

我更喜欢第一个选项(第 5 步),因为它更解耦 - 即使我们可以争辩说这里有一个不变量(???) - 因此报告的持久性之间存在一致性延迟以及集体的认可。


我实际上正在为调用本身而苦苦挣扎(第 4 步)。

我想我可以通过几种方式做到这一点:

一个。同步 RPC 调用

// code_fragment_a
// = ValidateMyAggregateCommandHandler
// ---
myAggregate = myAggregateRepository.find(command.myAggregateId());  // #1
myAggregate.changeStateTo(VALIDATING);                              // #2
myAggregateRepository.save(myAggregate);                            // #3

ValidationReport report = validationService.validate(myAggregate);  // #4
validationReportRepository.save(report);                            // #5

myAggregate.acknowledge(report);                                    // #6
myAggregateRepository.save(myAggregate);                            // #7
// ---

validationService 是在基础设施层中使用 REST 服务 bean 实现的域服务(也可以是本地验证,但在我的场景中不是)。

调用需要立即响应并且调用者(命令处理程序)被阻塞,直到返回响应。所以它引入了高时间耦合

如果由于技术原因导致验证调用失败,我们将采取例外措施并且我们必须回滚所有内容。该命令必须稍后重播。

乙。调用无响应(同步或异步)

在此版本中,命令处理程序将保留聚合的 "validating" 状态,并触发(并忘记)验证请求。

// code_fragment_b0
// = ValidateMyAggregateCommandHandler
// ---
myAggregate = myAggregateRepository.find(command.myAggregateId());  // #1
myAggregate.changeStateTo(VALIDATING);                              // #2
myAggregateRepository.save(myAggregate);                            // #3

validationRequestService.requestValidation(myAggregate);            // #4
// ---

这里,报告的确认可以同步或异步方式发生,在初始事务内部或外部[=]。

在专用事务中使用上述代码允许验证调用失败是无害的(如果我们在 impl 中有重试机制)。

此解决方案将允许快速轻松地开始同步通信,然后切换到异步通信。所以它很灵活。

B.1。同步实现

在这种情况下,validationRequestService 的实现(在基础设施层)直接 request/response。

// code_fragment_b1_a
// = SynchronousValidationRequestService
// ---
private ValidationCaller validationCaller;

public void requestValidation(MyAggregate myAggregate) {
        ValidationReport report = validationCaller.validate(myAggregate);

        validationReportRepository.save(report);
        DomainEventPublisher.publish(new ValidationReportReceived(report))
}
// ---

报告保存在专用事务中,事件的发布激活第三个代码片段(在应用程序层中),该代码片段对聚合进行实际确认工作。

// code_fragment_b1_b
// = ValidationReportReceivedEventHandler
// ---
public void when(ValidationReportReceived event) {
        MyAggregate myAggregate = myAggregateRepository.find(event.targetAggregateId());
        ValidationReport report = ValidationReportRepository.find(event.reportId());

        myAggregate.acknowledge(report);                                        
        myAggregateRepository.save(myAggregate);
}
// ---

所以在这里,我们有一个从基础层到应用层的事件。

B.2。异步

异步版本将更改之前在 ValidationRequestService 实现中的解决方案 (code_fragment_b1_a)。使用 JMS/AMQP bean 将允许在第一时间发送消息,然后独立接收响应。

我猜消息监听器会触发相同的 ValidationReportReceived 事件,其余代码与 code_fragment_b1_b.

相同

在我写这篇文章时 post,我意识到这个解决方案 (B2) 在交换中呈现出更好的对称性和更好的技术要点,因为它在网络通信方面更加解耦和可靠。在这一点上,它并没有引入太多的复杂性。

C。 BC之间的域事件和总线

最后一个实现,我不会使用域服务请求其他 BC 的验证,而是引发一个域事件,如 MyAggregateValidationRequested。我意识到这是一个 "forced" 领域事件,好吧,用户请求了它,但它从未真正出现在对话中,但它仍然是一个领域事件。

问题是,我还不知道如何以及在何处放置事件处理程序。基础架构处理程序应该直接接受它吗?

在将域事件发送到目的地之前,我是否应该将其转换为技术事件???

technical event like some kind of DTO if it was a data structure


我想所有与消息传递相关的代码都属于基础设施层(port/adapter 插槽),因为它们仅用于系统之间的通信。

并且在这些管道内传输的技术事件及其 raising/handling 代码应该属于应用程序层,因为像命令一样,它们最终会发生系统状态的突变。它们协调域,并由基础设施触发(就像控制器触发应用程序服务)。

我阅读了一些有关在命令中翻译事件的解决方案,但我认为这会使系统变得更加复杂,但没有任何好处。

所以我的应用程序外观会公开 3 种类型的交互: - 命令 - 查询 - 活动

通过这种分离,我认为我们可以更清楚地隔离来自 UI 的命令和来自其他 BC 的事件。


好的,我知道 post 很长而且可能有点乱,但这就是我卡住的地方,所以如果你能说一些可以帮助我的话,我提前谢谢你。

So my problem is that I'm struggling with the integration of the 2 BC.
Different solutions:
- The service RPC (#A) is simple but limit the scale,
- the service with messaging (#B) seems right but I still need feedback,
- and the domain events (#C) I don't know really how to cross boudaries with that.

再次感谢!

I have a use case in one of my bounded context where I need to contact another BC to validate an aggregate.

这真是一个奇怪的问题。通常,聚合是有效的还是无效的,完全取决于它们自己的内部状态——这就是为什么它们是聚合,而不仅仅是一些更大网络中的实体。

换句话说,您可能无法应用 DDD 模式,因为您对要解决的实际问题的理解不完整。

顺便说一句:在 中寻求帮助时,您应该尽可能地坚持 实际 问题,而不是试图使它抽象。

也就是说,有些模式可以帮助您。 Udi Dahan 在他关于 reliable messaging 的演讲中详细介绍了它们,但我将在此处介绍要点。

当您 运行 针对聚合的命令时,需要考虑两个不同的方面

  • 持久化状态变化
  • 安排副作用

"Side effects" 可以包含针对其他聚合 运行 的命令。

在您的示例中,我们会在快乐路径中看到三个不同的事务。

第一个事务会将您的聚合状态更新为正在验证,并安排任务以获取验证报告。

该任务 运行 是异步的,查询远程域上下文,然后在此 BC 中启动事务 #2,它保留验证报告并安排第二个任务。

第二个任务 - 根据复制到验证报告中的数据构建 - 启动事务 #3,运行对您的聚合执行命令以更新其状态。当这个命令完成时,没有更多的命令可以安排,一切都变得安静。

这行得通,但它可能会将您的聚合与您的流程过于紧密地耦合在一起。此外,您的流程是不相交的 - 分散在您的聚合代码中,并未真正被认为是第一个 class 公民。

因此,您更有可能看到通过两个额外的想法实现这一点。首先,引入领域事件。领域事件是对具有特殊意义的状态变化的描述。因此,聚合描述了更改(ValidationExpired?)以及理解它所需的本地状态,异步发布事件。 (换句话说,我们 运行 异步调度 PublishEvent 任务,而不是异步 运行 任意任务,将任意域事件作为有效负载)。

二、介绍一个"process manager"。流程管理器订阅事件,更新其内部状态机,并将(异步)任务调度到 运行。 (这些任务与聚合之前调度的任务相同)。请注意,流程管理器没有任何业务规则;那些属于聚合体。但是他们知道如何将命令与它们生成的域事件相匹配(请参阅 Gregor Hohpe 撰写的企业集成模式中的消息传递章节),以安排超时任务以帮助检测哪些计划任务未在其 SLA 内完成等等。

从根本上说,流程管理器类似于聚合;它们本身是领域模型的一部分,但对它们的访问是由应用程序组件提供给它们的。对于聚合,命令处理程序是应用程序的一部分;当命令已被聚合处理后,它是调度异步任务的应用程序。域事件发布到事件总线(基础设施),应用程序的事件处理程序订阅该总线,通过持久化加载流程管理器,传递要处理的域事件,再次使用持久化组件保存更新的流程管理器,然后应用程序安排挂起的任务。

I realize it is a "forced" domain event, ok the user requested it but it never really emerge in conversation but still it is a domain event.

我不会说这是被迫的;如果这个验证过程的需求真的来自业务,那么领域事件就是属于通用语言的东西。

Should I translate the domain event into a technical event before sending it to its destination

我不知道你认为那是什么意思。事件是描述发生的事情的消息。 "Domain event" 表示某事发生在域内。还是要发布的消息