我被要求将 CQRS/事件溯源模式实现到旧版 Web 应用程序中,以便准备将其从整体/面向状态的模型迁移到面向服务的分布式应用。
我有一些关于如何设计面向域的代码包的问题,该代码包将使用新的事件源模型将与数据库强耦合的旧实体连接起来。
我做的第一件事是:
- 为 CQRS/ES 编写一个小的"框架",其中包含 AggregateRoot、DomainEvent、Command、Handlers、Messaging、Eventstore、AggregateId 等类。
- 尝试将旧实体分组并"迁移"到一些聚合中,以将应用的所有历史记录和状态重建为事件聚合
- 在旧控制器中插入一些调度命令,以便让应用按原样工作,同时也为新的 CQRS/ES 系统提供信息。
上下文:
旧版应用程序包含多个映射到数据库的实体,用于保存模型图层。(我们的领域是人力资源(人力)。 假设我们有这些现有实体:
- 工作人员,具有各种字段和相关实体(一对一、一对一多),例如
- 名字
- 地址 1-1
- 能力 1-N
- 社会,工人在其中工作,涉及各个领域和相关实体(一对一,一对一多),例如
- 名字
- 地址 1-1
- 小时
- >合约,包含各种字段和相关实体(一对一、一对一多),如
- 地址 1-1
- 工人 1-1
- 社会 1-1
- 文档 1-N
- 第 1-N 天
- 小时
- 等。
从这个遗留模型中,我设计了一个包含以下内容的 MissionAggregation:
- 独立于数据库的 ID,如 UUID
- 一些值对象:地址、天数(它们是旧模型中的实体,在这里它们变成了 VO)
我还设计了一个 WorkerAggregate 和一个 SocietyAggregate,其中包含字段和 UUID,在 MissionAggregate 中,我添加了:
- 对 WorkerAggregate 的 UUID 的引用
- 对SocietyAggregate的UUID的引用
如前所述,我的目标是保持旧版应用不变,但只需在 CRUD 控制器的方法中引入一些调用,以将命令调度到新的 CQRS 系统。
例如:
在 bdd 中刷新新创建的合约后,我想向新的命令总线调度一个"创建任务命令"。
它以适当的命令处理程序为目标,该处理程序处理命令的所有数据,将其传递给具有新 UUID 的新创建的聚合,并将"MissionCreatedDomainEvent"存储在 EventStore 中。
DomainEvent 使用 AggregateId(播放头)编制索引,并具有有效负载,其中包含应用于和构建 MissionAgregate 所需的字段。
与往常一样,在应用中创建的新合约现在具有其以前的生命周期,其中包含旧版应用在其上执行的所有更新。但我还需要将所有这些更改反映到相应的 EventSourcedAgregate 中,因此每次应用程序中的数据库中出现刷新时,我都会调度一个命令,该命令将旧应用程序的"类似 crud 的操作"转换为面向域/面向命令的模式。
总结一下工作流程是:
- 发生 Crud 遗留操作并刷新合同实体上的一些更改
- 在控制器中的一行代码中,我调度了一个使用必要字段构建的命令(MissionAggregate...我需要存储在某个地方...见下文问题)到域命令总线,这样对现有代码库的影响就很小了。
- 总线将命令传递给相应的命令处理程序
- 处理程序加载聚合并通过调用适当的 Aggregate 方法来应用更改
- 然后经过一些验证后,聚合将引发并存储相应的事件
我的问题和疑问(至少其中一些)是:
-
我觉得我正在重写旧版应用程序的所有大部分,聚合之间的关系与实体之间的关系相同,并且具有相同类型的验证、检查等。
-
在 MissionAggregate 中同时引用 WorkerAggregate 和 SocietyAggregate UUID 意味着我还必须构建这些聚合(因此在刷新工人和社会实体时从旧版应用程序调度命令)。我不能只引用工人的实体ID和社会的实体ID吗?
-
我怎样才能避免有一个永恒增长的使命聚合?合同实体非常庞大,它有很多不断更新的字段(小时、天、文档等)如果我想存储所有这些事件,我需要有一个大型的任务聚合来反映所有这些更改;所以我需要大量的命令处理程序来响应我将从旧版应用程序调度的所有添加、更新等命令。
-
它应该引用的根实体的聚合有多"自由"?例如,合约实体需要在某处与其相关的任务聚合相关联,例如,当我想从应用程序调度命令时,就在旧代码刷新实体上的某些内容之后。在哪里存储此关系?在实体本身中,在聚合 ID 字段中?在聚合中,我应该有一个合同 ID 字段吗?或者我应该在某个地方有某种映射表来保存合同 ID 和任务聚合 ID 之间的关系?
-
过去怎么办?我是否应该通过在所有历史数据上生成聚合和事件的脚本迁移所有现有数据?
提前感谢您的时间。
你面前有一个巨大的任务,让我们试着分解一下。
最好在与遗留代码库隔离的情况下构建系统的这一新部分,否则您将在每一个转折点上束手无策。
在工程中创建单独的图层以满足这些新要求。从现在开始,我们将称之为"泡沫"。这个泡沫就像一个绿地项目,有自己的结构、依赖关系等。泡沫和遗产之间不会有直接的沟通;通信将通过另一个专用的转换层进行,我们称之为"反腐败层"(ACL)。
前交叉韧带
它就像两个系统之间的API。
它将来自气泡的调用转换为旧调用,反之亦然。其目的是防止一个系统破坏或影响另一个系统。通过这种方式,您可以继续独立地构建/维护每个系统。
同时,ACL 允许一个系统使用另一个系统,并重用逻辑、验证、规则等。
直接回答您的问题:
- 我觉得我正在重写旧版应用程序的所有大部分,聚合之间的关系与实体之间的关系相同,并且具有相同类型的验证、检查等。
使用 ACL,您可以求助于调用验证并从旧代码重用实现。这将使您有时间根据需要或尽可能重写内容。
但是,您可能不需要重写整个系统。如果你的目标是实现 CQRS 和事件溯源,并且可以通过保留大部分或部分旧系统来实现此目标,我会说你做到了。当然,除非目标之一是完全取代旧系统。否则,保留它;尽可能少地编写代码。
建议的工作流程:
- 保持 CQRS 和事件溯源系统处于气泡中
- 不要将这些新框架纳入旧框架
- 对 ACL 进行滞后控制器问题方法调用
- ACL 会将这些调用转换为命令并调度它们
- 事件溯源框架将捕获任何事件
- 结果将保存到气泡的数据库中
气泡的数据库可以是同一数据库中的不同架构,也可以是完全不同的数据库。但是你必须考虑同步,这本身就是一个话题。为了降低复杂性,我建议在同一数据库中使用不同的架构。
在
MissionAggregate 中同时引用 WorkerAggregate 和 SocietyAggregate UUID 意味着我还必须构建这些聚合(因此在刷新工人和社会实体时从旧版应用程序调度命令)。我不能只引用工人的实体 ID 和社会的实体 ID 吗?
我怎样才能避免拥有永恒增长的任务聚合?合同实体非常庞大,它有大量不断更新的字段(小时、天、文档等)。如果我想存储所有这些事件,我需要有一个大型的任务聚合来反映所有这些更改;所以我需要大量的命令处理程序来响应我将从旧版应用程序调度的所有添加、更新等命令。
您应该以小聚合为目标。大型聚合可能会降低性能并导致并发问题。
如果您预计会有一个巨大的聚合,最好重新考虑它并尝试分解它。询问哪些字段/属性一起更改 - 这些可能是不同的聚合。
此外,当您谈论 CQRS 时,您通常倾向于在系统中采用基于任务的方式。
想想一个传统的 Web 应用程序,其中有一个巨大的页面,其中包含许多字段,当用户保存时,这些字段都会批量发送到服务器。
现在,将其与现代 Web 应用程序进行对比,在该应用程序中,用户在每一步都会更改一小部分数据。如果您以这种方式考虑您的系统,您会发现那些较小的聚合。
PS. 您不需要为此重建界面。如果您的旧系统具有这些大页面,则可以在控制器中使用逻辑来检测哪些字段已更改并发出适当的命令。
它
- 应该引用的根实体的聚合有多"自由"?例如,合约实体需要在某处与其相关的任务聚合相关联,例如,当我想从应用程序调度命令时,就在旧代码刷新实体上的某些内容之后。在哪里存储此关系?在实体本身中,在 AggregateId 字段中?在聚合中,我应该有一个 ContratId 字段吗?或者我应该在某个地方有某种映射表来保存合同 ID 和任务聚合 ID 之间的关系?
聚合表示一个概念整体。它们就像原子,不可分割的东西。应始终按聚合的根实体 ID 引用聚合,而不应引用子实体 ID:从外部看,没有子实体。
聚合应作为一个整体加载并作为一个整体持久化。拥有小聚合的另一个原因。
聚合可以由单个实体组成。或者它可以有更多的实体和值对象,形成一个图形,但一个实体将被选为根,并将保存对其子项的引用。子实体和值对象不应包含对其父实体和值对象的引用。依赖关系不是双向的。
如果合同是任务聚合中的一个实体,则合同不应引用其父级。
但是,如果您的合同和任务是不同的聚合,那么它们可以通过其 ID 相互引用。
- 如何处理过去?我是否应该通过在所有历史数据上生成聚合和事件的脚本迁移所有现有数据?
这是业务专家的问题。他们需要它吗?如果他们不这样做,那么不要仅仅为了这样做而实施它。您做出的每个决策都应该着眼于满足业务需求并为其创造真正的价值,同时考虑成本和权衡。
有人说代码是一种负债,而不是资产,我在某种程度上总结:你写的每一行代码都需要经过测试和支持。不要编写任何不是真正必要的代码。
另外,请查看这篇关于 Strangler 模式的文章,它展示了如何通过逐渐用新的应用程序和服务替换特定功能来迁移旧系统。
如果有机会,请在Pluralsight(付费)观看本课程:领域驱动设计:使用遗留项目。作者提出了处理此类任务的实用方法。
我希望这能给你一些见解。
我不想破坏你的游戏。每个人都知道从头开始重写某些内容有多酷。这是一个挑战,很有趣,很令人兴奋。然而。。。
将其从整体式/面向状态的模型迁移到面向服务的分布式应用
CQRS/事件溯源无法解决你的任何问题,也不会帮助你以任何合理的方式分发应用。如果您只是在 CRUD 操作上生成事件,则每个部分之间会有一大堆混乱的依赖关系。每个需要数据的部分都必须调用几个"服务"(即表)来获取数据,而不是将数据推送到其他地方,生成其他一些部分将做出反应的事件1。这将是一团糟。通常这称为分布式单体。
这也是您已经看到它存在问题的原因。这些问题不会消失,因为你基本上是以相同的方式构建相同的系统,但这次它会更复杂。
从这里去哪里
第一件事永远是:有一个明确的目标。你想要一个你说的面向服务的体系结构。为什么?是否有需要不同缩放、不同资源的部件?它们是否由具有不同生命周期的不同团队管理?等。?也许你已经拥有了这一切,我不知道,但如果没有,那是你的第一个任务。
然后。你想拉出的部分不能只是 CRUD 的东西。这些不会是独立的,所以无论你的目标(见上一点!)是扩展还是不同的团队,你都不会达到你的目标!要保持独立,您必须提取数据的行为,并且以一种服务可以自行运行的方式。
你不能只是抛出流行语,然后希望最好。我建议忽略所有的炒作和流行语,想想你想要达到的目标。
例如:我需要 100 万工人在总共 10 分钟内记录他们的时间。因此,这意味着我需要一个"服务"来使工人能够使用Web界面记录他们的时间。因此,让我们将其创建为一个具有自己的数据库的完整独立部分,以便在需要时可以将其扩展到 100 个节点。每小时左右自动将数据导出到计费。