3

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

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

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


关于 2 个有界上下文。
- 发生用例的第一个(BC_A)将包含与用户相关的元素列表。
- 外部的 (BC_B) 对这些元素有一些了解

* 因此,从 BC_A 到 BC_B 的验证请求将要求对来自 BC_A 的聚合的所有元素进行审查,并返回一份报告,其中包含有关如何处理这些元素的一些规范元素(我们是否应该保留它,以及为什么)。
*聚合的状态将通过(比如说)“草稿”,然后在请求后“验证”,然后根据发回的报告,如果有的话,它将是“有效”或“has_error”。如果用户后来选择不遵循规范,它可以将聚合的状态传递给“受控”,这意味着存在一些错误,但我们不处理它。

该命令是ValidateMyAggregateCommand

用例是:

  1. 通过 id 获取目标聚合
  2. 将其状态更改为“正在验证”
  3. 坚持聚合
  4. 拨打验证电话(到另一个 BC)
  5. 持久化验证报告
  6. 使用目标聚合确认验证报告(它将根据结果再次更改其状态,应为“OK”或“HAS_ERROR”)
  7. 再次持久化聚合
  8. 根据验证结果生成域事件

它包含 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) 我真的不知道如何跨越国界。

再次感谢你!

4

1 回答 1

4

我在一个有界上下文中有一个用例,我需要联系另一个 BC 来验证聚合。

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

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

中寻求帮助时,您应该尽可能地关注您的实际问题,而不是试图将其抽象化。

也就是说,有一些模式可以帮助你。Udi Dahan 在他关于可靠消息传递的演讲中详细介绍了它们,但我将在这里介绍重点。

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

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

“副作用”可以包括针对其他聚合运行的命令。

在您的示例中,我们将在快乐路径中看到三个不同的交易。

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

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

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

这行得通,但它可能将您的聚合与您的流程耦合得太紧。此外,您的流程是不相交的——分散在您的聚合代码中,并没有真正被认为是一等公民。

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

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

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

我意识到这是一个“强制”域事件,用户请求它,但它从未真正出现在对话中,但它仍然是一个域事件。

我不会将其描述为强迫。如果这个验证过程的需求真的来自业务,那么领域事件是属于普适语言的东西。

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

我不知道你认为这意味着什么。事件是描述发生的事情的消息。“领域事件”是指在领域内发生的事情。它仍然是要发布的消息。

于 2016-07-07T05:47:50.843 回答