检查命令对来自其他聚合的数据的有效性



我目前正在开发我的第一个更大的DDD应用程序。目前它运行良好,但我们从早期就遇到了一个问题,我无法停止思考:

在我们的一些聚合中,我们保留了对另一个聚合根的引用,这对整个应用程序非常重要(基于它们的 ID,因此没有硬引用 - 删除也基于事件/最终一致性)。现在,当我们创建一个新的实体"Entity1"时,我们发送一个新的CreateEntity1命令,其中包含引用的聚合根的ID。

现在如何检查此引用的 ID 是否有效?现在我们通过从另一个聚合中读取来检查它(不修改那里的任何内容) - 但这种方法不知何故感觉很脏。我只想"信任"命令,因为 ID 无法手动输入,但必须选择。问题是,我们的应用程序是一个Web应用程序,信任您到达那里的用户输入并不真正安全(即使公众无法访问它)。

我是否忽略了这个问题的任何可能的解决方案,或者我应该忽略需要更好的解决方案的感觉?

验证另一个引用的聚合是否存在不是聚合的责任。这将打破单一责任原则。当CreateEntity1Command到达聚合时,应认为另一个引用的聚合处于有效状态,即它存在

由于在聚合边界之外,此检查最终是一致的。这意味着,即使它最初通过,它也可能在那之后变得无效(即它是deletedunpublished或任何其他无效的域状态)。您需要确保:

  • 如果引用的聚合尚不存在,则拒绝该命令。在使用域服务将命令分派到聚合之前,您可以在负责用例的应用程序服务中执行此检查。

  • 如果引用的聚合之后进入无效状态,则会执行更正操作。您应该在 Saga/Process Manager 中执行此操作。如果使用 CQRS,则订阅相关事件;如果没有,则使用cron.正确的操作是什么取决于您的域,但主要思想是它应该被建模为一个过程。

因此,长话短说,聚合的责任不会超出其一致性边界

附言抵制将服务(域与否)注入聚合(通过构造函数或方法参数)的诱惑。

直接聚合到聚合交互是 DDD 中的一种反模式。聚合 A 不应直接向聚合 B 发送命令或查询。聚合是严格的一致性边界。

我可以为您的问题想到 2 种解决方案:假设您有 2 个聚合根 (AR) - A 和 B。每个 AR 都有一堆命令处理程序,其中每个命令引发 1 个或多个事件。A 中的命令处理程序依赖于 B 中的某些数据。

  1. 您可以订阅 B 引发的事件,并在 A 中维护 B 的状态。您只能订阅决定有效性的事件。

  2. 您可以在 A 和 B 之间有一个完全独立的服务 (S) 进行协调。不要直接将您的请求发送给 A,而是将您的请求发送给 S,S将负责来自 B 的查询(以检查引用 ID 的有效性),然后将请求转发给 A。这有时称为进程管理器 (PM)。

例如,当您创建新的实体"Entity1"时,请将此请求发送给 PM,该 PM 的工作是验证请求中的数据是否有效,然后将请求路由到负责创建"实体 1"的聚合。将包含引用的聚合根 ID 的新 CreateEntity1Command 发送到此 PM,该 PM 使用引用的 AR 的 ID 来确保它是有效的,如果它是有效的,那么只有它会转发您的请求。

有用的链接: http://microservices.io/patterns/data/saga.html

我是否忽略了此问题的任何可能解决方案

你做到了。 "域名服务"为您提供了一个可能的漏洞。

聚合是一致性边界;它们的行为受以下约束

  • 聚合的当前状态
  • 它们被传递的论点。

如果聚合需要与其边界之外的内容进行交互,则将域服务传递给聚合根以封装该交互。 聚合可以自行决定调用域服务提供的方法来实现工作。

通常,域服务只是应用程序或基础结构服务的包装器。 例如,如果聚合需要知道某些外部数据是否可用,则可以传入支持该查询的域服务,检查某些数据缓存。

但是 - 这里有一个诀窍:您需要注意这样一个事实,即来自聚合边界之外的数据必然是过时的。 即使您正在查询过时的副本,也可能有另一个进程更改数据。

问题是,我们的应用程序是一个Web应用程序,信任您到达那里的用户输入并不真正安全(即使公众无法访问它)。

这是真的,但这通常不是域问题。 例如,我们可以指定 API 中的端点需要某些命令消息的 JSON 表示形式——但这并不意味着域模型负责获取原始字节数组并为其创建 DOM。 应用程序层将承担该责任;聚合的责任是域问题。

可能需要仔细思考才能区分不同关注点之间的界限。 此字节序列是否为聚合的有效标识符?显然是一个应用程序问题。 另一个聚合是否处于允许某些行为的状态?显然是一个领域问题。 聚合是否存在...?可以走任何一条路。

最新更新