我目前正在参与一个涉及域驱动设计(DDD)和多个域的集成场景的研究项目。
我在我的一个有限上下文中有一个用例,我需要联系另一个BC来验证聚合。实际上,未来可能会有几个BC要求验证数据(但现在没有)。
现在,我患有DDD强迫症神经衰弱,我无法找到正确应用模式的方法(笑)。我真的很感激人们对此的一些反馈。
关于2个有界的背景。
- 用例发生的第一个(BC_A)将包含与用户相关的元素列表。
- 外部的(BC_B)对这些元素有一些了解
*因此,从BC_A到BC_B的验证请求将要求从BC_A审查聚合的所有元素,并返回包含有关如何处理这些元素的一些规范的报告(如果我们应该保留或不保留,以及为什么) 。
*汇总的状态将通过(比方说)"草案"然后"验证"在请求之后,然后根据发回的报告,它将是"有效"或" has_error"万一有。如果用户以后选择不遵循规范,则可以将聚合的状态传递给"控制"意思是有一些错误,但我们不会照顾它。
该命令是 ValidateMyAggregateCommand
用例是:
它包含8个步骤,可能是1到3个或更多个事务。
我需要保持localy的验证报告(在UI中访问它),我想我可以做到:
我更喜欢第一个选项(步骤5),因为它更加分离 - 即使我们可以认为这里存在一个不变量(???) - 因此报告的持久性与报告之间存在一致性延迟由汇总人员知晓。
我实际上正在努力解决这个问题(步骤4)。
我想我可以通过多种方式做到这一点:
// 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实现的域服务(也可以是本地验证,但不在我的场景中)。
呼叫需要立即响应,并且调用者(命令处理程序)被阻止,直到返回响应为止。因此它引入了一个高时间耦合。
如果由于技术原因导致验证调用失败,我们会采取异常并且我们必须回滚所有内容。该命令必须稍后重播。
在这个版本中,命令处理程序将保持"验证"聚合的状态,并将触发(并忘记)验证请求。
// code_fragment_b0
// = ValidateMyAggregateCommandHandler
// ---
myAggregate = myAggregateRepository.find(command.myAggregateId()); // #1
myAggregate.changeStateTo(VALIDATING); // #2
myAggregateRepository.save(myAggregate); // #3
validationRequestService.requestValidation(myAggregate); // #4
// ---
此处,报告的确认可能以同步或异步方式发生,在初始交易内部或外部。
在专用事务中使用上面的代码可以使验证调用中的失败无害(如果我们在impl中有重试机制)。
此解决方案允许快速轻松地开始同步通信,稍后切换到异步通信。所以它很灵活。
在这种情况下,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);
}
// ---
所以在这里,我们有一个从infra层到app层的事件。
异步版本将更改ValidationRequestService impl中的先前解决方案(code_fragment_b1_a)。使用JMS / AMQP bean将允许第一次发送消息,并在以后单独接收响应。
我猜消息传递侦听器会触发相同的ValidationReportReceived事件,其余代码对于code_fragment_b1_b也是相同的。
在写这篇文章时,我意识到这个解决方案(B2)在交换中提供了更好的对称性和更好的技术点,因为它在网络通信方面更加分离和可靠。在这一点上,它没有引入如此多的复杂性。
最后一次实现,我会提出像MyAggregateValidationRequested这样的域事件,而不是使用域服务来请求来自其他BC的验证。我意识到这是一个强迫"域事件,确定用户请求它但它从未真正出现在对话中但仍然是域事件。
问题是,我不知道如何以及在何处放置事件处理程序。基础设施处理人员应该直接接受吗?
我应该在将域事件发送到目的地之前将其转换为技术事件吗?
技术事件,如某种DTO,如果它是一个数据结构
我猜所有与消息传递相关的代码都属于基础设施层(端口/适配器插槽),因为它们仅用于系统之间的通信。
在这些管道中传输的技术事件及其提升/处理代码应属于应用层,因为类似命令,它们最终会导致系统状态的突变。它们协调域,并由infra触发(如控制器触发应用程序服务)。
我阅读了一些关于在命令中翻译事件的解决方案,但我认为这会使系统变得更加复杂而没有任何好处。
所以我的应用程序外观会暴露3种类型的交互: - 命令 - 查询 - 活动
通过这种分离,我认为我们可以更清楚地从UI和其他BC的事件中隔离命令。
好的,我知道帖子很长,也许有点乱,但这就是我被困住的地方,所以如果你能说一些可以帮助我的话,我会提前感谢你。
所以我的问题是我在与BC省的整合中挣扎。
不同的解决方案:
- 服务RPC(#A)很简单,但限制了规模,
- 带消息的服务(#B)似乎正确,但我仍然需要反馈, - 和领域事件(#C)我真的不知道如何跨越boudaries。
再次感谢你!
答案 0 :(得分:4)
我在我的一个有限上下文中有一个用例,我需要联系另一个BC来验证聚合。
这是一个非常奇怪的问题。通常,聚合是有效的,或者是无效的,完全取决于它们自己的内部状态 - 这就是它们是聚合的原因,而不仅仅是某些较大网站中的实体。
换句话说,您可能无法应用DDD模式,因为您对要解决的实际问题的理解不完整。
顺便说一句:在ddd中寻求帮助时,您应该尽可能地坚持实际问题,而不是试图将其抽象化。< / em>的
也就是说,有一些模式可以帮助你。 Udi Dahan在他关于reliable messaging的演讲中详细介绍了他们,但我将在这里介绍高点。
当您针对聚合运行命令时,需要考虑两个不同的方面
&#34;副作用&#34;可以包含针对其他聚合运行的命令。
在您的示例中,我们会在快乐路径中看到三个不同的交易。
第一个事务会将聚合的状态更新为Validating,并安排任务以获取验证报告。
该任务以异步方式运行,查询远程域上下文,然后在此BC中启动事务#2,这将持久保存验证报告并安排第二个任务。
第二个任务 - 根据复制到验证报告中的数据构建 - 启动事务#3,对聚合运行命令以更新其状态。完成此命令后,将无法再安排任何命令,一切都会变得安静。
这样可行,但它可能会使您的聚合过程与您的过程过于紧密。此外,您的流程是不相交的 - 分散在您的汇总代码中,而不是真正被认为是一等公民。
所以你更有可能看到这个实现了两个额外的想法。首先,引入域事件。域事件是描述状态的变化,具有特殊意义。因此聚合描述了更改(ValidationExpired?)以及理解它所需的本地状态,异步发布事件。 (换句话说,我们不是异步运行任意任务,而是异步调度PublishEvent任务,并将任意域事件作为有效负载。)
第二,引入了一个&#34;流程经理&#34;。进程管理器订阅事件,更新其内部状态机,并调度(异步)任务以运行。 (这些任务与聚合之前安排的任务相同)。请注意,流程经理没有任何业务规则;那些属于聚合。但是他们知道如何将命令与他们生成的域事件相匹配(参见Gregor Hohpe的企业集成模式中的消息传递章节),以安排超时任务,帮助检测哪些计划任务在他们的SLA中没有完成等等
从根本上说,流程经理类似于聚合;它们本身是域模型的一部分,但应用程序组件会向它们提供对它们的访问。使用聚合,命令处理程序是应用程序的一部分;当聚合处理命令时,它是调度异步任务的应用程序。域事件发布到事件总线(基础结构),应用程序的事件处理程序订阅该总线,通过持久性加载进程管理器,传递要处理的域事件,再次使用持久性组件来保存更新的流程管理器,然后应用程序安排挂起的任务。
我意识到这是一个强迫的&#34;域事件,确定用户请求它但它从未真正出现在对话中但仍然是域事件。
我不会把它形容为强迫;如果对此验证过程的要求确实来自业务,则域事件属于无处不在的语言。
我应该将域事件转换为技术事件,然后再将其发送到目的地
我不知道你认为这意味着什么。事件是描述发生事件的消息。 &#34;域名事件&#34;意味着域内发生的事情。它仍然是要发布的信息。