我目前正在研究一个涉及域驱动设计(DDD)和多个域集成方案的项目。
我在一个受限环境中有一个用例,需要联系另一位BC来验证聚合。实际上,将来可能会有多个BC要求验证数据(但现在没有)。
现在,我患有DDD强迫症神经衰弱,无法找到正确应用模式的方法(笑)。我非常感谢人们对此的一些反馈。
关于2个有界上下文。
-发生用例的第一个(BC_A)将包含与用户相关的元素列表。
-外部(BC_B)对这些元素有一些了解
*因此,从BC_A到BC_B的验证请求将要求审查BC_A中汇总的所有元素,并将返回包含有关如何处理这些元素的规范的报告(如果我们保留或不保留它,以及为什么) 。
*聚合的状态将通过(例如)“草稿”传递,然后在请求后进行“验证”,然后根据发回的报告,如果有一个,则为“有效”或“has_error”。如果用户以后选择不遵循规范,则可能会将聚合的状态传递给“受控”,这意味着存在一些错误,但我们不会对此进行处理。
该命令为 ValidateMyAggregateCommand
用例是:
它包含8个步骤,可能从1到3个事务或更多。
我需要持久保存验证报告的本地设置(以便在用户界面中访问它),我认为我可以做到:
验证调用之后的
我更喜欢第一个选项(第5步),因为它更加解耦-即使我们可以认为这里有一个不变式(???)-因此报表的持久性与用户的认可之间存在一致性延迟。骨料。
我实际上在为呼叫本身做苦事(第4步)。
我想我可以通过几种方式做到这一点:
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,如果它是数据结构
我猜所有与消息传递有关的代码都属于基础结构层(端口/适配器插槽),因为它们仅用于系统之间的通信。
并且在这些管道内部以引发/处理代码传递的技术事件应属于应用程序层,因为像命令一样,它们最终会导致系统状态发生变化。它们协调域,并由基础设施触发(例如控制器触发应用程序服务)。
我阅读了一些有关在命令中转换事件的解决方案,但我认为这会使系统变得更加复杂,没有任何好处。
因此,我的应用程序外观将公开三种交互方式:
-命令
-查询
-事件
通过这种分离,我认为我们可以更清楚地将UI和其他BC中的事件隔离开。
好的,我知道帖子很长,可能有点混乱,但这就是我遇到的问题,因此如果您能说些什么对我有所帮助,我先谢谢您。
所以我的问题是,我在努力与2 BC的整合。
不同的解决方案:-服务RPC(#A)很简单,但限制了规模,-带有消息传递的服务(#B)看起来不错,但是我仍然需要反馈,以及-我确实不知道的域事件(#C)如何跨界。
再次感谢你!
最佳答案
我在一个受限环境中有一个用例,需要联系另一位BC来验证聚合。
这是一个非常奇怪的问题。通常,聚合有效或无效,完全取决于它们自己的内部状态-这就是为什么它们是聚合的原因,而不仅仅是某些较大网络中的实体。
换句话说,您可能无法应用DDD模式,因为您对要解决的实际问题的理解不完整。
顺便说一句:在ddd中寻求帮助时,您应该尽可能地坚持自己的实际问题,而不是尝试使其抽象。
也就是说,有些模式可以帮助您。乌迪·达汉(Udi Dahan)在关于reliable messaging的演讲中详细介绍了它们,但我将在此处介绍要点。
对聚合运行命令时,需要考虑两个不同方面
“副作用”可以包括要针对其他聚合运行的命令。
在您的示例中,我们将在幸福的道路上看到三个不同的交易。
第一个事务会将聚合的状态更新为“正在验证”,并计划任务以获取验证报告。
该任务异步运行,查询远程域上下文,然后在该BC中启动事务#2,该事务将持久保存验证报告并计划第二个任务。
第二项任务-根据复制到验证报告中的数据构建-启动事务#3,对聚合运行命令以更新其状态。该命令完成后,不再需要安排任何命令,一切变得安静。
这行得通,但是可能会使您的聚集体与您的过程紧密结合。此外,您的过程是不相交的-分散在您的汇总代码中,并未真正被认为是头等公民。
因此,您更有可能看到通过另外两个想法实现的方法。首先,引入域事件。域事件是对状态变化的描述,具有特殊意义。因此,聚合描述了更改(ValidationExpired?)以及实现该更改所需的本地状态,从而异步发布了该事件。 (换句话说,不是异步运行任意任务,而是异步调度以任意域事件作为有效负载的PublishEvent Task)。
第二,引入“流程管理器”。流程管理器订阅事件,更新其内部状态机,并安排(异步)任务运行。 (这些任务与聚合之前计划的任务相同)。注意,流程管理器没有任何业务规则。这些属于集合。但是他们知道如何将命令与其生成的域事件进行匹配(请参阅Gregor Hohpe的Enterprise Integration Patterns中的消息传递章节),以安排超时任务,以帮助检测在其SLA中尚未完成的计划任务,等等。
从根本上说,过程管理器类似于聚合。它们本身是域模型的一部分,但是应用程序组件向它们提供了对其的访问。对于聚合,命令处理程序是应用程序的一部分;当命令由聚合处理后,调度异步任务的是应用程序。域事件将发布到事件总线(基础结构),应用程序的事件处理程序订阅该总线,通过持久性加载流程管理器,传递要处理的域事件,再次使用持久性组件保存更新的流程管理器,然后,应用程序安排待处理的任务。
我意识到这是一个“强制”域事件,好吧用户请求了它,但它从未真正出现在对话中,但仍然是域事件。
我不会形容为强迫性的。如果此验证过程的要求确实来自业务,则域事件属于普遍使用的语言。
我应该先将域名事件转换为技术事件,然后再将其发送到目的地
我不知道你是什么意思。事件是描述发生的事情的消息。 “域事件”表示域内发生了某些事情。它仍然是要发布的消息。
关于java - 调用其他有界上下文的策略,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38234410/