我认为,如何执行它(或什至是什么可能)在很大程度上取决于您将如何坚持它。例如,您正在使用 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 年,但我希望它对那里的人有所帮助。