【问题标题】:How should rules for Aggregate Roots be enforced?应该如何执行聚合根的规则?
【发布时间】:2010-03-17 23:58:35
【问题描述】:

在搜索网络时,我从 Eric Evans 的书中发现了一个应该对聚合执行的规则列表:

  1. 根实体具有全局身份,并最终负责检查不变量
  2. 根实体具有全局标识。边界内的实体具有本地标识,仅在聚合中是唯一的。
  3. Aggregate 边界之外的任何内容都不能包含对内部任何内容的引用,除了根实体。根实体可以将对内部实体的引用传递给其他对象,但它们只能临时使用它们(在单个方法或块内)。
  4. 只能通过数据库查询直接获取聚合根。其他一切都必须通过遍历来完成。
  5. 聚合中的对象可以保存对其他聚合根的引用。
  6. 删除操作必须一次性删除聚合边界内的所有内容
  7. 提交对聚合边界内的任何对象的更改时,必须满足整个聚合的所有不变量。

理论上这一切似乎都很好,但我看不出这些规则在现实世界中会如何执行

以规则 3 为例。一旦根实体为外部对象提供了对内部实体的引用,如何防止外部对象在单个方法或块之外保持引用?

(如果执行此操作是特定于平台的,我很想知道如何在 C#/.NET/NHibernate 环境中执行此操作。)

【问题讨论】:

  • 另一个与规则#3 相同的问题是,这些规则是仅适用于域层还是也适用于应用程序层或表示层。一个示例是使用 MVVM 范例中的 ViewModel 将内部项目包装到聚合中以用于演示目的;这会违反规则#3 吗?
  • @MylesRip 当根实体向外部实体提供对其内部子实体的引用时,它不会向其提供对象引用。它给出了内部子实体的 ID,并且只有 ID。 External 不能直接在该内部子实体上调用方法。它必须去存储库,获取根实体并调用根实体上的方法。

标签: domain-driven-design


【解决方案1】:

我认为您不应该让聚合让您的外部代码访问它的实体。

你告诉你的聚合你想要发生什么,它会处理它。

如果我们有一个聚合:汽车。我们不关心汽油和车轮,我们只是开车。我们向汽车询问事情,它会在不提及内部结构的情况下回答。

我们问:我们有汽油吗?是的。不是:把油箱给我,我看看有没有汽油。

【讨论】:

  • 那么,如果您只是检索从数据库中出来后必须应用一些规则的数据,该怎么办。如果您要隐藏实体,那么剩下的唯一一件事就是通过根目录一一公开所需的属性。如果有多个实体,这听起来很混乱。我并不反对,我正在尝试在学习这个概念时找到最佳实践。
  • 我首先想到的不仅仅是获取数据并应用规则,而是通过将使用数据的对象获取数据。然后该对象将应用规则。我认为您必须自上而下考虑每个操作。目标是什么,您将如何包装该请求。示例:假设我们有一个系统来检查一个人是否达到一定年龄,如果是这样就打开一扇门。我们将调用 Door.open(Person) 该方法然后将处理所有子查询和调用而不暴露数据。
【解决方案2】:

从技术上讲,我认为没有办法阻止外部对象在单个方法或块之外保持引用。我想您只需要在设计中强制执行此规则即可。

【讨论】:

    【解决方案3】:

    我认为,如何执行它(或什至是什么可能)在很大程度上取决于您将如何坚持它。例如,您正在使用 NHibernate,我认为这意味着域对象中的所有内容都必须是可访问的,以便映射它与事件源相比,重建对象状态唯一重要的是事件本身,这可以更轻松地重建无法通过公共接口访问的内部对象。

    • 根实体具有全局身份,并最终负责检查不变量 根实体具有全局标识。边界内的实体具有本地标识,仅在聚合中是唯一的。

    :: 我使用 GUID 作为标识。永远不要使用PK。曾经。我也碰巧对任何非根实体使用 GUID,但您也可以使用字符串。

    • 聚合边界之外的任何内容都不能引用内部任何内容,根实体除外。根实体可以将对内部实体的引用传递给其他对象,但它们只能临时使用它们(在单个方法或块内)。

    :: 这就是持久性实现的重要性。我是事件源,我可以使用这些事件在根目录中重建根目录的公共接口无法访问的对象。因此,在 C# 中,我只是将所有非根实体标记为内部实体,并且对这些实体的所有访问都通过根的公共接口进行代理。由于我的域在它自己的程序集中,因此任何客户端都无法获得对非根实体的引用,编译器也不会允许我意外地这样做。如果我需要公开属性,我只需确保它们是只读/只获取的。如果您使用的是 ORM,那么这可能是不可能的,我不确定。如果您可以向 NHibernate 授予对 internal 的访问权限,那么这可能会打开一些大门,但它仍然会在许多方面限制您。在这种情况下,我的解决方案是创建一对模仿事件快照的方法(如果你是事件源,你会拥有什么),它基本上吐出一个包含 NHibernate 可以使用的状态的 DTO,并且还接受相同的 DTO恢复对象的状态。如果可能,请确保它们只能由存储库访问。

    从域内部(域对象引用其他域对象),非根实体应该只存在于根中成为一种规则(代码审查)。如果您正确设置命名空间,则可以使用 Visual Studio 的依赖项验证来防止项目在违反此规则时生成。

    • 只能通过数据库查询直接获取聚合根。其他一切都必须通过遍历来完成。

    :: 我用 IEntity 标记我的非根实体,它只是将 ID 作为接口的一部分。然后我创建了一个实现 IEntity 的 AggregateRoot 抽象类。这符合“聚合根是聚合中的实体”的特征。然后我的存储库只接受或返回 AggregateRoot 的实例。这是由存储库抽象强制执行的,使用泛型作为约束,因此如果没有一些明显的恶作剧,基本上不能违反它。请参阅下一条关于“遍历”的评论

    • Aggregate 中的对象可以保存对其他 Aggregate 根的引用。

    :: 关键词是“引用”。这实际上只是一个ID。因此,例如,当您将 RootB 的实例“添加”到 RootA 时,RootA 应该只捕获 RootB 的 ID,并以这种方式保存。因此,现在如果您需要从 RootA 取回 RootB 的 ID,那么您需要让 RootA 给您 ID,然后在后续查询中使用它来查找 RootB。

    • 删除操作必须一次性删除聚合边界内的所有内容

    :: 这很简单,但也非常依赖于业务案例。例如,假设通过root,我创建了一个配置。作为配置的结果,创建了几个资源文件。如果我通过root删除配置,那么这些资源文件也应该被删除。在大多数情况下,如果您的 root-persistence 设置正确,这将自行解决。但是,就不变量而言,您可能会遇到更复杂的情况。例如,如果您有一个作为根的经理实体,并且该经理有许多员工向其报告,则通过删除该经理,可能需要许多操作才能完成业务流程中的流程。例如,也许这些员工需要取消“报告至”字段。这是一个更复杂的话题,因为涉及到很多系统设计因素。例如,你是事件源的,它是事件驱动的系统还是同步的等等。可能有一百种不同的方法来解决这个问题。我认为这里的重点是聚合根负责确保它发生,或者至少确保进程开始。

    • 提交对聚合边界内的任何对象的更改时,必须满足整个聚合的所有不变量。

    :: 参见前面关于经理和员工的评论。这基本上只是意味着在保存根之前,必须执行所有业务规则。我通过确保在运行 ActionA() 时执行此操作,如果任何业务规则在聚合或其非根实体、值对象或沿线的任何内容中失败,那么我会抛出异常。这可以防止最终提交的发生,因为原始的 Action() 永远不会完成。为此,您必须确保您的处理程序(无论启动此操作)不会尝试过早保存。为了模拟事务,我通常会等到操作(或操作链)的最后,然后再尝试保存任何内容。如果您的有界上下文非常整洁,那么您实际上应该只需要在操作结束时保存一个实体(根),因为它是根。

    在某些情况下,您可能需要保存一些根,但您必须弄清楚如何回滚该事务。我提到的那些快照可以使这变得微不足道。例如,您获得根 A 和 B 并保存他们的快照(纪念品),然后执行操作。然后你尝试保存 RootA 并且它成功了。您尝试保存 RootB 但抛出异常(可能连接失败或其他原因)。在某些重试逻辑失败后,您使用快照恢复 RootB,然后重新保存它,然后重新抛出异常,使其在日志中显示为致命异常。如果由于某种原因,您无法恢复和保存 RootA(数据库现在已关闭 - 糟糕的时机),那么您只需将备忘录与您的日志一起记录下来,以便以后可以手动恢复(例如,将其排队等待恢复)。有些人不喜欢在域中抛出异常以违反业务规则的想法,并认为您应该为此使用事件(请参阅:异常应该是异常的),我不反对。我现在对这种方法更满意。有上百万种方法可以做到这一点,但这并不是真正的 DDD 问题,我只是提供一些关于如何利用该结构来解决那些不可避免的问题/问题的想法。

    我知道这已经晚了 8 年,但我希望它对那里的人有所帮助。

    【讨论】:

      【解决方案4】:

      您可以做的一件事就是将内部状态的副本提供给外部世界。

      【讨论】:

        【解决方案5】:

        我最喜欢执行 DDD 模式和实践的方式是不断地教育人们了解它们的价值。然而,有时我有一个更严格的工具。

        我自己还没有这样做,但在我看来,FluentNHibernate 可能是强制执行聚合属性的好工具。

        您的示例可以通过使用“IAggregateRoot”标记接口标记所有聚合根和使用“IEntity”标记接口标记非根实体来实现。然后,您的自定义 FNH 约定将检查标记为 IEntity 引用实体 IEntity 的实体,并在找到时发出错误信号(例如抛出异常)。

        这有意义吗?

        【讨论】:

        • 用 IAggregateRoot 标记聚合根实体和用 IEntity 标记其他实体可能有助于控制哪些实体可以与存储库模式一起使用,但我不确定这是否能有效地在这种情况有两个原因。 1) 允许聚合实体在聚合边界之外保存对其他对象的引用(但不能反过来)。这将是引用 IEntity 的 IEntity 的有效使用。 2) 如果在不同的用例中使用不同的实体作为聚合根,执行可能不准确。
        • 看来,聚合根的“强制执行规则”与其说是定义实体的问题,不如说是必须遵循规则,而更像是一个教育和永恒的警惕。这会是一个公平的说法吗?
        猜你喜欢
        • 1970-01-01
        • 2021-12-22
        • 2020-10-24
        • 2014-01-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-05-29
        • 1970-01-01
        相关资源
        最近更新 更多