我目前正在进行一个涉及领域驱动设计 (DDD) 和多个领域集成方案的研究项目。
我在一个有界上下文中有一个用例,我需要联系另一个 BC 来验证聚合。事实上,未来可能会有几个 BC 要求验证数据(但现在没有)。
现在,我患有 DDD 强迫症神经衰弱,我无法找到正确应用模式的方法(笑)。我真的很感谢人们对此的一些反馈。
关于 2 个有界上下文。
- 发生用例的第一个(BC_A)将包含与用户相关的元素列表。
- 外部的 (BC_B) 对这些元素有一些了解
* 因此,从 BC_A 到 BC_B 的验证请求将要求对来自 BC_A 的聚合的所有元素进行审查,并返回一份报告,其中包含有关如何处理这些元素的一些规范元素(我们是否应该保留它,以及为什么)。
*聚合的状态将通过(比如说)“草稿”,然后在请求后“验证”,然后根据发回的报告,如果有的话,它将是“有效”或“has_error”。如果用户后来选择不遵循规范,它可以将聚合的状态传递给“受控”,这意味着存在一些错误,但我们不处理它。
该命令是ValidateMyAggregateCommand
用例是:
- 通过 id 获取目标聚合
- 将其状态更改为“正在验证”
- 坚持聚合
- 拨打验证电话(到另一个 BC)
- 持久化验证报告
- 使用目标聚合确认验证报告(它将根据结果再次更改其状态,应为“OK”或“HAS_ERROR”)
- 再次持久化聚合
- 根据验证结果生成域事件
它包含 8 个步骤,可能从 1 到 3 笔交易或更多。
我需要在本地保存验证报告(在 UI 中访问它),我想我可以做到:
- 在独立验证调用之后(报告是它自己的汇总)
- 当我坚持目标聚合时(它会在里面)
我更喜欢第一个选项(第 5 步),因为它更加解耦——即使我们可以说这里有一个不变量(???)——因此在报告的持久性和由总计的。
我实际上正在为通话本身而苦苦挣扎(第 4 步)。
我想我可以通过几种方式做到这一点:
- A. 使用 REST 实现的同步 RPC 调用
- B. 没有响应的调用(void)(即发即弃)让几个实现选项放在表上(同步/异步)
- C. 领域事件翻译成技术事件到达其他 BC
A. 同步 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(也可以是本地验证,但在我的场景中不是)。
调用需要立即响应,调用者(命令处理程序)被阻塞,直到返回响应。因此它引入了高时间耦合。
如果由于技术原因验证调用失败,我们会处理异常,我们必须回滚所有内容。该命令必须稍后重播。
B. 无响应调用(同步或异步)
在这个版本中,命令处理程序将保持聚合的“验证”状态,并触发(并忘记)验证请求。
// 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(在基础设施层中)的实现会直接请求/响应。
// 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 impl (code_fragment_b1_a) 中的先前解决方案。使用 JMS/AMQP bean 将允许第一次发送消息,然后独立接收响应。
我猜消息侦听器会触发相同的 ValidationReportReceived 事件,而 code_fragment_b1_b 的其余代码将是相同的。
当我写这篇文章时,我意识到这个解决方案(B2)在交换中呈现出更好的对称性和更好的技术点,因为它在网络通信方面更加解耦和可靠。在这一点上,它并没有引入这么多的复杂性。
C. 领域事件和 BC 之间的总线
最后一个实现,而不是使用域服务从其他 BC 请求验证,我会引发一个域事件,如 MyAggregateValidationRequested。我意识到这是一个“强制”域事件,用户请求它,但它从未真正出现在对话中,但它仍然是一个域事件。
问题是,我还不知道如何以及在哪里放置事件处理程序。基础设施处理程序是否应该直接接受它?
我应该在将域事件发送到目的地之前将其转换为技术事件吗???
如果它是一种数据结构,则像某种 DTO 之类的技术事件
我猜所有与消息传递相关的代码都属于基础设施层(端口/适配器插槽),因为它们仅用于系统之间的通信。
在这些管道中传输的技术事件及其引发/处理代码应该属于应用程序层,因为就像命令一样,它们最终会导致系统状态的突变。它们协调域,并由基础设施触发(如控制器触发应用程序服务)。
我阅读了一些关于在命令中翻译事件的解决方案,但我认为它使系统更加复杂而没有任何好处。
所以我的应用程序外观将公开 3 种类型的交互:-命令-查询-事件
通过这种分离,我认为我们可以更清楚地将命令与 UI 和事件与其他 BC 隔离开来。
好的,我意识到这篇文章很长而且可能有点混乱,但这就是我卡住的地方,所以如果你能说一些可以帮助我的话,我提前感谢你。
所以我的问题是我正在努力整合 2 BC。
不同
的解决方案: - 服务 RPC (#A) 很简单,但限制了规模,
- 带有消息传递的服务 (#B) 似乎正确,但我仍然需要反馈,
- 和域事件 (#C) 我真的不知道如何跨越国界。
再次感谢你!