我研究 DDD 一段时间了,并偶然发现了 CQRS 和事件溯源 (ES) 等设计模式。这些模式可用于帮助以更少的努力实现 DDD 的某些概念。
\n\n然后我开始开发一个简单的软件来实现所有这些概念。并开始想象可能的失败路径。
\n\n为了阐明我的架构,下图描述了来自前端并到达后端控制器的一个请求(为简单起见,我忽略了所有过滤器、绑定器)。
\n\n\n\n可以添加很多层,例如:聚合缓存、事件缓存、快照等。
\n\n有时ES可以与关系数据库并行使用。这样,当 UOW 保存已发生的新事件时,它还会将聚合持久保存到关系数据库中。
\n\nES 的好处之一是它拥有一个中心事实来源,即事件存储。因此,即使内存中甚至关系数据库中的模型被损坏,我们也可以从事件中重建模型。
\n\n有了这个事实来源,我们就可以构建其他系统,这些系统可以以不同的方式使用事件来形成不同的模型。
\n\n然而,要实现这一点,我们需要真相来源干净且未被损坏。否则所有这些好处都不会存在。
\n\n也就是说,如果我们考虑图中描述的架构中的并发性,可能会出现一些问题:
\n\n这个问题可以在很多不同的地方处理:
\n\n前端可以控制哪个用户/演员可以执行什么操作以及执行多少次。
调度程序可以拥有正在处理的所有命令的一个缓存,如果存在引用同一聚合(帐户)的命令,则会抛出异常。
存储库可以创建聚合的新实例,并在保存之前运行事件存储中的所有事件,以检查版本是否仍与步骤 7 中获取的版本相同。
每个解决方案的问题:
\n\n前端
\n\n我一直在研究DDD,偶然发现了CQRS和事件采购(ES)等设计模式.这些模式可用于帮助实现DDD的一些概念,而不需要花费太多精力.在下面举例说明的体系结构中,聚合知道如何处理与其自身相关的命令和事件.换句话说,事件处理程序和命令处理程序是聚合.
然后,我开始建模一个示例域,只是为了理解实现如何遵循业务逻辑.对于这个问题,这里是我的域名(基于此):
我知道这是一个错误的建模示例,但我只是作为一个例子使用它.因此,使用ES,在操作结束时,我们会将所有事件(绿色箭头)保存到事件存储中(如果没有异常),将每个事件保存到其给定的事件流(聚合类型+聚合ID)中:
一切似乎都是正确的.因此,如果我们想要重建任何此Aggregate实例的内部状态,我们只需要新建它(new())并以正确的顺序应用保存在各自事件流中的所有事件.
我的问题与模型的变化有关.因为,软件开发是一个我们永远不会停止了解我们的领域的过程,我们总是带来新的想法.那么,让我们分析一些变化情景:
我们假装现在,如果预订聚合检查座位不可用,它应该发送一个事件(座位未保留),这个事件应由一个新的聚合处理,该聚合将存储所有未获得座位的人:
在旧系统已正确处理初始命令(放置顺序)并将所有事件保存到其各自事件流的假设中:
让我们假装现在,当付款被接受时,我们将在新的Aggregate(财务汇总)中处理此事件(已接受付款),而不再在订单汇总中处理.它会向订单总计发送一个新事件(已收到付款).我知道这种情况结构不合理,但这种情况可能会发生.
在旧系统已正确处理初始命令(放置顺序)并将所有事件保存到其各自事件流的假设中:
现在,订单不再知道如何处理付款接受事件.
因此,如示例所示,每当系统更改反映在由不同事件处理程序(Aggregate)处理的事件中时,存在一些主要问题.因为,我们不能再重建内部状态了.所以,这个问题可以有一些解决方案:
当事件未由存储事件流的聚合处理时,我们可以找到新处理程序并创建新实例并将事件发送给它.但是为了保持内部状态正确,我们需要最后一个事件(Payment Received)由Order Aggregate处理.所以,我们让它调度事件(以及可能的命令):
该解决方案可能存在一些问题.让我们假设一个新命令(Place Order)到了,它必须创建这个订单实例并保存新状态.现在我们有:
灰色是当系统尚未完成模型更改时已在上次调用中保存的事件.我们可以看到为新聚合(Finance W)创建了一个新的事件流.我们可以看到Event Streams仅附加,因此订单Y事件流中的付款接受事件仍然存在.Finance W Event Stream中的第一个Payment Accepted事件是应该由Order处理但必须找到新处理程序的事件.订单的事件流中的黄色付款收到事件是当订单的事件流中的付款已接受事件由财务处理时由接受的付款的新处理程序生成 …