domain-driven-design - 如何设计从面向CRUD的旧版应用程序到CQRS和事件源系统的桥梁?

标签 domain-driven-design cqrs event-sourcing

我被要求在旧版Web应用程序中实施CQRS / Event来源模式,以准备将其从单块/面向状态的模型迁移到面向服务的分布式应用程序。

我对如何设计一个面向域的代码束有一些疑问,该束将使用新的事件源模型将紧密耦合到数据库的旧实体连接起来。

我做的第一件事是:


用诸如AggregateRoot,DomainEvent,Command,Handlers,Messaging,Eventstore,AggregateIds等类为CQRS / ES编写一个小的“框架”。
尝试将旧实体分组并“迁移”到一些聚合中,以将应用程序的所有历史和状态重构为EventSoourced聚合
在旧控制器中插入一些Commands调度程序,以使应用程序按原样工作,同时还可以在侧面提供新的CQRS / ES系统。


上下文:

旧版应用程序包含映射到数据库的多个实体,这些实体保存模型层。 (我们的领域是人力资源(人力)。
假设我们有这些现有实体:


具有各种字段和相关实体(OneToOne,OneToMany)的工作者,例如


名称
地址1-1
能力1-N

工人工作的社会,涉及各个领域和相关实体(OneToOne,OneToMany),例如


名称
地址1-1
小时

具有各种字段和相关实体(OneToOne,OneToMany)的合同,例如


地址1-1
1-1
社会1-1
文件1-N
第1-N天
小时
等等



从这个旧模型中,我设计了一个MissionAggregate,它具有:


db独立ID,例如UUID
一些值对象:地址,日期(它们在旧模型中是一个实体,在这里成为VO)


我还设计了一个具有字段和UUIDS的WorkerAggregate和SocietyAggregate,并在MissionAggregate中添加了:


对WorkerAggregate的UUID的引用
对SocietyAggregate的UUID的引用


就像我之前说的,我的目的是使旧版应用程序保持不变,而只是在CRUD控制器的方法中引入一些将Command调度到新CQRS系统的调用。

例如:

在bdd中刷新新创建的Contrat之后,我想向新的命令总线分派“ CreateMissionCommand”。

它针对适当的命令处理程序,该命令处理程序处理所有命令数据,将其传递给具有新UUID的新创建的Aggregate,并将“ MissionCreatedDomainEvent”存储在EventStore中。

DomainEvent用一个播放头AggregateId进行索引,并具有一个有效负载,该有效负载包含要应用于和构建MissionAggregate所需的字段。

与往常一样,在应用程序中创建的新Contract现在具有以前的生命周期,其中包含旧版应用程序对其执行的所有更新。但是我还需要将所有这些更改反映到相应的EventSourcedAggregate上,因此,每次应用程序中的数据库出现刷新时,我都会调度一个Command,该命令将旧版应用程序的“类似操作”转换为面向域/面向命令的模式。

总结工作流程是:


发生Crud旧操作并刷新合同实体上的某些更改
在控制器中的一行代码中,我将构建有必要字段(MissionAggregate的AggregateId ...我需要将其存储在某处,请参阅下一个问题)的命令调度到Domain命令总线,以便对现有的代码库非常低。
总线将命令传递给相应的命令处理程序
处理程序通过调用适当的Aggregate方法来加载聚合并应用更改
然后经过一些验证,聚合引发并存储适当的事件


我的问题和疑问(至少其中一些;)是:


我觉得我正在重写旧版应用程序的所有重要部分,它们之间具有实体之间相同的聚合关系,并且具有相同类型的验证,检查等。
在MissionAggregate中同时引用了WorkerAggregate和SocietyAggregate UUID意味着我也必须构建那些聚合(因此在刷新Worker和Society实体时,从旧版应用程序中调度命令)。我是否只能引用工人的实体ID和协会的实体ID?
我如何避免拥有一个不断增长的MissionAggregate?合同实体非常庞大,它具有大量不断更新的字段(小时,天,文档等)。如果我想存储所有这些事件,则需要有一个较大的MissionAggregate来反映所有这些更改;因此,我需要拥有大量的CommandHandlers,它们可以对我将要从旧版应用程序分派的所有add,update等命令做出反应。
根实体应该引用的聚集体有多“免费”?例如,合同实体需要在某个地方与其相关的任务集合相关联,例如当我想在遗留代码已刷新实体上的某些内容之后从应用程序分发Command时。在哪里存储这种关系?在实体本身中,在AggregateId字段中?在汇总中,我应该有一个ContratId字段吗?还是我应该在某处具有某种映射表,其中包含合同ID和MissionAggregate ID之间的关系?
过去该怎么办?我应该通过一个脚本来迁移所有现有数据,该脚本在所有历史数据上生成汇总和事件吗?


在此先感谢您的时间。

最佳答案

您面前还有一项艰巨的任务,让我们尝试分解一下。
最好将系统的这一新部分与传统代码库隔离开来,否则您将无所适从。
在项目中为这些新需求创建一个单独的层。从现在开始,我们将其称为“气泡”。这个气泡就像一个未开发的项目,具有自己的结构,依赖关系等。气泡和遗留物之间不会直接通信;通信将通过另一个专用的翻译层进行,我们将其称为“反腐败层”(ACL)。
访问控制列表
就像两个系统之间的API。
它将呼叫从气泡转换为遗留,反之亦然。其目的是防止一个系统损坏或影响另一个系统。这样,您可以保持彼此独立地构建/维护每个系统。
同时,ACL允许一个系统使用另一个系统,并重用逻辑,验证,规则等。

要直接回答您的问题:


我觉得我正在重写旧版应用程序的所有重要部分,它们之间具有实体之间相同的聚合关系,并且具有相同类型的验证,检查等。


使用ACL,您可以求助于调用验证并从旧代码中重用实现。这将使您有时间根据需要或尽可能多地重写内容。
但是,您可能不需要重写整个系统。如果您的目标是实施CQRS和事件源,并且可以通过保留大部分或部分旧系统来实现此目标,那么我会说您做到了。当然,除非目标之一是完全替代旧系统。否则,请保留;编写尽可能少的代码。
建议的工作流程:

将CQRS和事件源系统保持在泡沫中
不要将这些新框架带入传统
对ACL进行滞后控制器发出方法调用
ACL会将这些调用转换为命令并分派它们
任何事件都将被您的事件来源框架捕获
结果将保存到气泡数据库中

气泡的数据库可以是同一数据库中的不同架构,也可以是完全不同的数据库。但是您必须考虑同步,这是它自己的主题。为了降低复杂性,我建议在同一数据库中使用不同的架构。


在MissionAggregate中同时引用了WorkerAggregate和SocietyAggregate UUID意味着我也必须构建那些聚合(因此在刷新Worker和Society实体时,从旧版应用程序中调度命令)。我不能仅引用工人的实体ID和协会的实体ID吗?

我如何避免拥有一个不断增长的MissionAggregate?合同实体非常庞大,它具有大量不断更新的字段(小时,天,文档等)。如果我想存储所有这些事件,则需要有一个较大的MissionAggregate来反映所有这些更改;因此,我需要拥有大量的CommandHandlers,它们可以对我将要从旧版应用程序分派的所有add,update等命令做出反应。



您应该针对小型骨料。巨大的聚合可能会降低性能并导致并发问题。
如果您预计会有一个巨大的总量,那么最好重新考虑一下并尝试将其分解。询问哪些字段/属性一起更改-这些可能是不同的集合。
另外,在谈到CQRS时,通常会倾向于在系统中采用基于任务的处理方式。
考虑一下传统的Web应用程序,其中有一个巨大的页面,其中包含许多字段,当用户保存时,这些字段将全部发送到服务器。
现在,将它与现代Web应用程序进行对比,在Web应用程序中,用户在每个步骤中都更改一小部分数据。如果以这种方式考虑系统,则会发现那些较小的聚合。
PS。您无需为此重建接口。如果您的旧系统有那么大的页面,则可以在控制器中包含逻辑以检测更改了哪些字段并发出适当的命令。


根实体应该引用的聚集体有多“免费”?例如,合同实体需要在某个地方与其相关的任务集合相关联,例如当我想在遗留代码已刷新实体上的某些内容之后从应用程序分发Command时。在哪里存储这种关系?在实体本身中,在AggregateId字段中?在汇总中,我应该有一个ContratId字段吗?还是我应该在某处具有某种映射表,其中包含合同ID和MissionAggregate ID之间的关系?


集合表示概念整体。他们就像原子,不可分割的事物。您应该始终通过根实体ID引用聚合,而永远不要通过子实体ID引用聚合:从外部看,没有子代。
聚合应作为一个整体加载并作为一个整体持久存在。骨料少的另一个原因。
聚集可以由单个实体组成。或者它可以具有更多的实体和值对象,从而形成一个图,但是一个实体将被选作Root,并将保留对其子代的引用。子实体和值对象不应包含对其父代的引用。依赖性不是双向的。
如果合同是任务汇总中的一个实体,则合同不应引用其上级。
但是,如果您的合同和任务是不同的汇总,则它们可以通过其ID相互引用。


与过去有什么关系?我是否应该通过一个脚本迁移所有现有数据,该脚本会在所有历史数据上生成聚合和事件?


这是业务专家的问题。他们需要吗?如果他们不这样做,则不要仅仅为了实现它而实施它。考虑到成本和权衡因素,您所做的每个决定都应着眼于满足业务需求并为其产生实际价值。
有人说代码是一种责任,而不是资产,我在某种程度上表示赞同:您编写的每一行代码都需要测试和支持。不要编写任何不必要的代码。

另外,请查看此article about the Strangler Pattern,它显示了如何通过逐渐用新的应用程序和服务替换特定功能来迁移旧系统。
如果有机会,请在Pluralsight观看此课程(收费):Domain-Driven Design: Working with Legacy Projects。作者提出了处理此类任务的实用方法。
我希望这能给您一些见识。

关于domain-driven-design - 如何设计从面向CRUD的旧版应用程序到CQRS和事件源系统的桥梁?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57326853/

相关文章:

domain-driven-design - CQRS/ES 去规范化器

c# - 泛型类型参数与参数实例的类型不同

event-handling - 到底应该在哪里实现端口?

domain-driven-design - 外部 id 作为域标识

sql - 获取自上次检查后插入的行?

message-queue - CQRS/上下文之间的通信/事件存储/推送或拉取?

javascript - 为复杂的关系域模型构建 Redux 状态

c# - 使用 MediatR 编辑操作 CQRS 模式

concurrency - CQRS 读取端、多个事件流主题、并发/竞争条件

design-patterns - 在 CQRS/ES 系统中存储命令有什么好处?