如何实施聚合根的规则?

时间:2010-03-17 23:58:36

标签: domain-driven-design

在网上搜索时,我遇到了埃里克埃文斯的书中的一系列规则,这些规则应该针对聚合实施:

  1. 根实体具有全局标识,并且最终负责检查不变量
  2. 根实体具有全局标识。边界内的实体具有本地标识,仅在Aggregate中唯一。
  3. 除了根实体之外,聚合边界之外的任何内容都不能包含对内部任何内容的引用。根实体可以将对内部实体的引用传递给其他对象,但它们只能瞬时使用它们(在单个方法或块中)。
  4. 只能使用数据库查询直接获取聚合根。其他一切都必须通过遍历来完成。
  5. Aggregate中的对象可以保存对其他Aggregate根的引用。
  6. 删除操作必须一次性删除聚合边界内的所有内容
  7. 当提交对聚合边界内任何对象的更改时,必须满足整个聚合的所有不变量。
  8. 理论上这一切似乎都很好,但我不知道这些规则在现实世界中如何强制执行

    以规则3为例。一旦根实体为外部对象提供了对内部实体的引用,那么什么是使该外部对象保持超出单个方法或块的引用?

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

5 个答案:

答案 0 :(得分:6)

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

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

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

我们问:我们有汽油吗?是。不是:给我坦克物品,这样我就可以检查一下我们是否有汽油。

答案 1 :(得分:0)

从技术上讲,我不认为有一种方法可以防止外部对象超出单个方法或块之外的参考。我想你只需在你的设计中强制执行这条规则。

答案 2 :(得分:0)

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

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

::我使用GUID作为身份。永远不要使用PK。永远。我也碰巧对任何非root实体使用GUID,但你也可以使用字符串。

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

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

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

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

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

的下一条评论
  • Aggregate中的对象可以保存对其他Aggregate根的引用。

::关键词是"引用"。这真的只是一个ID。例如,当你"添加" RootB的RootB实例,然后RootA应该只捕获RootB的ID,并以这种方式保存。所以现在如果你需要从RootA取回RootB,那么你需要让RootA给你ID,然后你用它在后续查询中查找RootB。

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

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

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

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

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

我知道这已经晚了8年,但我希望它可以帮助那些人。

答案 3 :(得分:0)

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

答案 4 :(得分:-1)

我最喜欢的实施DDD模式和实践的方法是不断教育人们他们的价值。然而,有些时候我和我有一个更严格的工具。

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

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

它有意义吗?