【问题标题】:What is the purpose of child entity in Aggregate root?聚合根中子实体的目的是什么?
【发布时间】:2018-08-29 05:17:58
【问题描述】:

[从这个问题和 cmets 跟进:Should entity have methods and if so how to prevent them from being called outside aggregate]

正如标题所说:我不清楚实体作为一个孩子的实际/确切目的是什么?

根据我在很多地方读到的内容,这些是作为聚合子实体的属性:

  1. 它具有本地聚合标识
  2. 不能直接访问,只能通过聚合根访问
  3. 应该有方法
  4. 不应从聚合中暴露出来

在我看来,这意味着几个问题:

  1. 实体应该是私有的聚合
  2. 我们需要一个只读副本 Value-Object 来公开来自实体的信息(例如,至少让存储库能够读取它以便保存到数据库)
  3. 我们在实体上拥有的方法在 Aggregate 上重复(反之亦然,我们必须在 Aggregate 上拥有的处理实体的方法在实体上重复)

那么,为什么我们有一个实体而不是只有值对象呢?只拥有值对象、聚合和公开值对象的所有方法(我们已经复制实体信息)似乎更方便。

PS。 我想关注子实体的聚合,而不是实体的集合。


[更新以响应 Constantin Galbenu 回答和 cmets]

那么,实际上,你会有这样的东西吗?

public class Aggregate {
    ...
    private _someNestedEntity;

    public SomeNestedEntityImmutableState EntityState {
       get {
          return this._someNestedEntity.getState();
       }
    }

    public ChangeSomethingOnNestedEntity(params) {
       this._someNestedEntity.someCommandMethod(params);
    }
}

【问题讨论】:

    标签: domain-driven-design


    【解决方案1】:

    您正在考虑数据。停下。 :) 实体和值对象不是数据。它们是您可以用来为问题域建模的对象。实体和值对象只是对问题进行建模时自然出现的事物的分类。

    实体应该是私有的聚合

    是的。此外,对象中的所有状态都应该是私有的,并且不能从外部访问。

    我们需要一个只读副本 Value-Object 来公开来自实体的信息(例如,至少让存储库能够读取它以便保存到数据库)

    没有。我们不会公开已经可用的信息。如果信息已经可用,这意味着有人已经对此负责。所以联系那个对象为你做事,你不需要数据!这基本上就是Law of Demeter 告诉我们的内容。

    经常实现的“存储库”确实需要访问数据,你是对的。他们是一个糟糕的模式。它们通常与 ORM 结合使用,在这种情况下情况更糟,因为您失去了对数据的所有控制权。

    我们在实体上拥有的方法在 Aggregate 上重复(反之亦然,我们在 Aggregate 上必须拥有的处理实体的方法在实体上重复)

    诀窍是,您不必这样做。您创建的每个对象(类)都是有原因的。如前所述,创建一个额外的抽象,为领域的一部分建模。如果你这样做,一个存在于更高抽象级别的“聚合”对象将永远不想提供与下面的对象相同的方法。这意味着没有任何抽象。

    这种用例仅在创建面向数据的对象时出现,这些对象除了保存数据之外几乎没有其他作用。显然,如果您无法获取数据,您会想知道如何处理这些数据。然而,这很好地表明您的设计尚未完成。

    【讨论】:

    • 非常感谢您的回答!首先,果然我的模型还远未完成:)。顺便说一句,我不使用任何 orm。 /关于您输入的问题: 1. 存储库的替代方案是什么? 2. 如果实体是私有的聚合并且我们仍然需要聚合之外的实体状态(在事件中,在某些域服务中,无论如何)除了将它作为值对象复制到聚合上之外,我们还能如何公开它? 3. 我很可能正在考虑数据,我愿意停下来。我应该怎么想?:) 4. 你能展示子实体实现的真实例子吗?
    • 实际上,让我扩大评论中的最后一个问题:您能否展示不能/不应该使用值对象实现的子实体的真实示例,为什么会这样?我认为这对我有很大帮助
    • 我们不会公开已经可用的信息。” - 我想这对我来说是一个完全未知的:如果实体是私有的聚合并且我们需要实体状态除了在聚合级别复制实体的状态以公开它之外,我们还有哪些选择?
    • 请问How to make an entity private to aggregate, 这个规则也应该考虑到客户端代码,或者只是我们谈论其他聚合?
    • @deezg 存储库的替代方案:首先,尝试将其混合到域中。我在一家银行看到了AccountEntryAccountEntryRepository,可能分别是:TransactionLedgerLedger 是一个自然存储库。如果找不到任何内容,则可能必须将其“隐藏”在业务方法下方。例如。 Account.transfer(...) 在没有“存储库”的情况下在内部进行持久化。
    【解决方案2】:
    1. 实体应该是私有的聚合

    是的。我不认为这是一个问题。继续阅读以了解原因。

    1. 我们需要一个只读副本值对象来公开来自实体的信息(至少让存储库能够读取它以便 例如,保存到数据库)

    没有。使您的聚合返回需要持久化和/或需要在聚合的每个方法的事件中引发的数据。

    原始示例。现实世界需要更细粒度的响应,也许performMove 函数需要使用game.performMove 的输出来为persistence 和eventPublisher 构建propper 结构:

      public void performMove(String gameId, String playerId, Move move) {
        Game game = this.gameRepository.load(gameId); //Game is the AR
        List<event> events = game.performMove(playerId, move); //Do something
        persistence.apply(events) //events contains ID's of entities so the persistence is able to apply the event and save changes usign the ID's and changed data wich comes in the event too.
        this.eventPublisher.publish(events); //notify that something happens to the rest of the system
      }
    

    对内部实体执行相同操作。让实体返回由于其方法调用(包括其 ID)而更改的数据,在 AR 中捕获此数据并为持久性和 eventPublisher 构建适当的输出。这样,您甚至不需要向 AR 公开带有实体 ID 的公共只读属性,而 AR 也不需要向应用程序服务公开其内部数据。这是摆脱 Getter/Setters 包对象的方法。

    1. 我们在实体上拥有的方法在 Aggregate 上重复(反之亦然,我们在 Aggregate 上必须拥有的处理实体的方法 在实体上重复)

    有时,要检查和应用的业务规则仅属于一个实体,其内部状态和 AR 只是充当网关。没关系,但如果你发现这种模式太多,那么这表明 AR 设计有误。也许内部实体应该是 AR 而不是内部实体,也许您需要将 AR 拆分为多个 AR(其中一个是旧的 ner 实体),等等......不要害怕拥有只有一个的类或两种方法。

    回应dee zg cmets:

    persistance.apply(events) 究竟做了什么?它能拯救整个 仅聚合还是实体?

    两者都没有。聚合和实体是领域概念,而不是持久性概念;您可以拥有不需要一对一匹配您的域概念的文档存储、列存储、关系等。您不会从持久性中读取聚合和实体;您使用从持久性中读取的数据在内存中构建聚合和实体。聚合本身不需要持久化,这只是一个可能的实现细节。请记住,聚合只是一种组织业务规则的构造,它并不意味着是状态的表示。

    您的事件具有上下文(用户意图)和已更改的数据(以及在持久性中识别事物所需的 ID),因此在知道的持久层中编写 apply 函数非常容易,即在关系数据库的情况下执行什么 sql 指令,执行什么以应用事件并持久化更改。

    您能否提供示例何时以及为什么更好(甚至 不可避免?)使用子实体而不是单独的 AR 引用 其 Id 作为值对象?

    您为什么要设计一个具有状态和行为的类并对其建模?

    进行抽象、封装、复用等基本SOLID设计。如果实体具有确保操作的域规则和不变量所需的一切,那么该实体就是该操作的 AR。如果您需要实体无法完成的额外域规则检查(即实体没有足够的内部状态来完成检查或不自然地适合实体和代表什么),那么您必须重新设计;有时可能是对执行额外域规则检查的聚合建模并将其他域规则检查委托给内部实体,有时可能会更改实体以包含新事物。它过于依赖领域上下文,所以我不能说有一个固定的重新设计策略。

    请记住,您不会在代码中对聚合和实体进行建模。您只需对具有行为的类进行建模,以检查域规则以及执行该检查和响应更改所需的状态。这些类可以充当不同操作的聚合或实体。这些术语仅用于帮助沟通和理解类在每个操作上下文中的作用。当然,您可能会遇到操作不适合实体的情况,您可以使用 V.O. 对聚合进行建模。持久性 ID 并且没问题(遗憾的是,在 DDD 中,在不了解域上下文的情况下,默认情况下几乎所有内容都可以)。

    你想从比我解释得更好的人那里得到更多启发吗? (不是以英语为母语是这些复杂问题的障碍)看看这里:

    https://blog.sapiensworks.com/post/2016/07/14/DDD-Aggregate-Decoded-1 http://blog.sapiensworks.com/post/2016/07/14/DDD-Aggregate-Decoded-2 http://blog.sapiensworks.com/post/2016/07/14/DDD-Aggregate-Decoded-3

    【讨论】:

    • 感谢您的意见!我有 2 个问题: 1. persistance.apply(events) 究竟是做什么的?它是否只保存整个聚合或实体? 2. 您能否展示一些真实世界的示例,其中使用子实体而不是聚合上的值对象来建模显然更好? (请,对于单个子实体,而不是它们的集合)。谢谢!
    • 我正在再次阅读您的答案,您解决了我的另一个难题:我只是无法发现 AR 上的价值对象和单独的 AR 之间的中间立场。我看不到现实世界对子实体的需求。您能否提供示例以及何时以及为什么更好(甚至不可避免?)使用子实体而不是其 Id 引用的单独 AR 作为值对象?
    【解决方案3】:
    1. 它具有本地聚合标识

    从逻辑上讲,可能,但是用我们所拥有的持久性手段来具体实现这一点通常是不必要的复杂。

    1. 我们需要一个只读副本值对象来公开来自 实体(至少让存储库能够读取它以便 例如,保存到数据库)

    不一定,例如,您可以拥有只读实体。

    问题的存储库部分已在another question 中得到解决。读取不是问题,有多种技术可以防止来自外部世界的写入访问,但仍然允许持久层直接或间接填充实体。

    那么,为什么我们有一个实体而不是只有值对象呢?

    您可能有些仓促地将关注点放在同一个篮子中,实际上略有不同

    • 操作封装
    • 聚合级别不变实施
    • 读取权限
    • 写入权限
    • 实体或 VO 数据完整性

    仅仅因为值对象最好是不可变的并且不强制执行聚合级别的不变量(尽管它们确实强制执行自己的数据完整性)并不意味着实体不能具有某些相同特征的微调组合.

    【讨论】:

    • 我知道您已经在其他问题上解决了其中的一些问题,如果我在同一点上盘旋了几次,但我想获得全面和完整的理解,请接受我的道歉。你能解释一下你怎么会有read-only entities吗?他们的行为/方法会发生什么?还是您在谈论实体数据字段的 VO 表示?
    • 感谢您的链接!我已经阅读了它,但是如果我们将他的最终结果类 (Customer) 作为公开的只读实体放在某个聚合上,则可以从 AR 外部访问它的方法。还是我错过了什么?
    • 是的,默认情况下它们会。 Internal(或 Java 中的 protected)在这里会有所帮助。
    • 这意味着每个包/组件都有一个 AR,对吧?
    【解决方案4】:

    这些问题在 CQRS 架构中不存在,其中写入模型(聚合)不同于读取模型。在扁平化架构中,Aggregate 必须公开读取/查询方法,否则将毫无意义。

    1. 实体应该是私有的聚合

    是的,这样你就清楚地表达了它们不是供外部使用的事实。

    1. 我们需要一个只读副本 Value-Object 来公开来自实体的信息(例如,至少让存储库能够读取它以便保存到数据库)

    存储库是一种特殊情况,不应以与应用程序/演示代码相同的方式查看。它们可以是同一个包/模块的一部分,换句话说,它们应该能够访问嵌套实体。

    实体可以被视为/实现为具有不可变 ID 和表示其状态的 Value 对象的对象,如下所示(在伪代码中):

    class SomeNestedEntity
    {
        private readonly ID;
        private SomeNestedEntityImmutableState state;
    
        public getState(){ return state; }
        public someCommandMethod(){ state = state.mutateSomehow(); }
    }
    

    所以你看到了吗?您可以安全地返回嵌套实体的state,因为它是不可变的。 Law of Demeter 会有一些问题,但这是您必须做出的决定;如果你通过返回状态来打破它,你会让代码第一次更容易编写,但耦合会增加。

    1. 我们在实体上拥有的方法在 Aggregate 上重复(反之亦然,我们必须在 Aggregate 上拥有的处理实体的方法在实体上重复)

    是的,这保护了 Aggregate 的封装,也允许 Aggregate 保护它的不变量。

    【讨论】:

    • 感谢您的回答!首先让我说,我所说的只是写方面。读取端是完全不同的部分。 /我不清楚的是你将如何防止调用SomeNestedEntity.someCommandMethod() 外部聚合?如果我们将这个问题扩展到更广泛的问题:如果实体永远不应该从任何其他地方访问,而不是从聚合中访问,为什么还要对实体进行行为?为什么不把它放在聚合上并将实体变成简单的 POCO 值对象?
    • @deezg “如果永远不应该从聚合之外的任何地方访问实体,为什么还要对实体进行行为” - 因为你想释放这种行为的聚合,你想保持聚合简单,您想将行为与它使用的数据等放在一起;通常,与您首先创建类(实体)的原因相同
    • @RobertBräutigam 在理论上如何隐藏聚合状态(知识)并同时共享它?除非我们想让域持久性感知和依赖,这听起来像是更糟糕的选择(比暴露状态)。领域事件、json 表示等……所有这些都只是共享状态/知识或使持久性影响您的领域设计的另一种形式。感觉有些规则简直是相互排斥的。
    • @deezg 你还在思考数据,以及如何移动而不是分解业务功能。 :) 行。所以“数据”当然可以四处移动。关键的区别是不要移动裸数据。例如Account.getLedgerItems() 是不正确的,因为该数据属于Account。那么,为什么我们仍然想要这些数据?制作余额报告。好的,那就请Account 制作吧,因为他负责所有数据:Account.createBalanceReport()。所以我将数据“取出”出来,它现在只是在另一个对象内部,并且意味着不同的东西。
    • @RobertBräutigam 是的,当然,这很清楚:)。但是Account.PutOnHold() 呢(您的 ML 模块检测到潜在的欺诈性帐户使用情况并需要阻止它进行审计)?现在你有一些内部标志hold=true,你需要坚持。如果您不公开信息/状态/知识 - 有哪些选择? (我很抱歉在那个周围来回走动,但这让我很困扰,看不到任何巧妙的方法)它几乎感觉封装和完全持久性不可知域是正交的(?!)跨度>
    【解决方案5】:

    我不会写太多。只是一个例子。一辆车和一个齿轮。车是聚合根。齿轮是一个子实体

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-06-26
      • 1970-01-01
      • 2011-11-19
      • 1970-01-01
      • 2010-12-29
      • 1970-01-01
      相关资源
      最近更新 更多