聚合根中子实体的目的是什么?

时间:2018-08-29 05:17:59

标签: domain-driven-design

[跟踪此问题和评论:Should entity have methods and if so how to prevent them from being called outside aggregate]

正如标题所述:我不清楚实体作为子实体的实际/精确目的是什么?

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

  1. 它具有本地身份以进行汇总
  2. 不能直接访问,只能通过聚合根访问
  3. 应该有方法
  4. 不应从聚合中暴露

在我看来,这转化为几个问题:

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

那么,为什么我们根本有一个实体而不是只有价值对象?似乎只有一个值对象,所有方法都聚合并公开值对象(我们已经在复制实体信息了),这看起来更加方便。

PS。 我想将重点放在子实体上,而不是实体集合上。


[根据 Constantin Galbenu 的回答和评论进行更新]

那么,有效的话,您会有类似的东西吗?

public class Aggregate {
    ...
    private _someNestedEntity;

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

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

5 个答案:

答案 0 :(得分:1)

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

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

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

不。使您的聚合返回在聚合的每种方法上都需要保留和/或引发的数据。

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

  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构建适当的输出。这样,您甚至不需要将具有实体ID的公共只读属性公开给AR,并且AR既不向应用程序服务公开其内部数据。这是摆脱Getter / Setters包对象的方法。

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

有时,要检查和应用的业务规则仅属于一个实体,其内部状态和AR只是网关。可以,但是如果您发现此模式太多,则表明存在错误的AR设计。也许内部实体应该是AR,而不是内部实体,也许您需要将AR拆分为服务器AR(并且其中一个是旧的ner实体),等等。。。不要为拥有仅一个类的类而感到恐惧或两种方法。

回应dee zg评论:

  

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

答案 1 :(得分:1)

  
      
  1. 它具有本地身份以进行汇总
  2.   

从逻辑上讲,也许是可行的,但是通过持久性来具体实现这一点意味着我们通常会变得不必要地复杂。

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

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

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

  

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

您可能会急于将确实有点不同的问题放在同一个篮子中

  • 操作封装
  • 总体水平不变执法
  • 读取权限
  • 写访问权限
  • 实体或VO数据完整性

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

答案 2 :(得分:1)

您正在考虑数据。别搞了。 :)实体和值对象不是数据。它们是可用于对问题域进行建模的对象。实体和值对象只是对问题建模时自然产生的事物的分类。

  

实体应该是私有的才能聚合

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

  

我们需要一个只读的复制值对象,以暴露来自实体的信息(例如,至少一个存储库能够读取它以便保存到db)

不。我们不会公开已经可用的信息。如果信息已经可用,则意味着已经有人对此负责。因此,联系该对象为您做事情,您不需要数据!实际上,Law of Demeter就是这样。

经常执行 do 的“存储库”需要访问数据,这是正确的。他们是一个坏模式。它们经常与ORM结合使用,在这种情况下甚至更糟,因为您失去了对数据的所有控制。

  

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

诀窍是,您不必这样做。您创建的每个对象(类)都有一定的原因。如前所述,要创建其他抽象,请对域的一部分进行建模。如果这样做,存在更高抽象级别的“聚合”对象将永远不会希望提供与以下对象相同的方法。那意味着没有任何抽象。

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

答案 3 :(得分:0)

您所遇到的这些问题在CQRS体系结构中不存在,在该体系结构中Write模型(聚合)与Read模型不同。在平面架构中,聚合必须公开读取/查询方法,否则将毫无意义。

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

是的,通过这种方式,您可以清楚地表达它们不是供外部使用的事实。

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

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

可以将实体视为/实现为具有不变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上处理实体的方法在实体上重复)
  2.   

是的,这保护了聚合的封装,也允许聚合保护其不变式。

答案 4 :(得分:0)

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