关于DDD中聚合的思考

最近看了www.domaindrivendesign.org网站上的一篇关于如何设计聚合的文章,受益良多,让我对DDD中的聚合有了一些新的理解。

因为发现这篇文章中的观点和jdon上的关于如何设计聚合的观点有些出入,所以必须拿出来和大家讨论一下。
1)文章中介绍,聚合应该尽量小;
2)一些对象之所以成为一个聚合,不仅仅是简单的因为这些对象是具有紧密相关的关联关系,更加重要的是因为这些对象之间需要满足某个不变性(Invariants)规则;这单对理解聚合来说我认为实在是太重要了。

下面听我仔细分析一下:
首先为什么聚合应该尽量小?文中主要是从并发操作的角度来分析的。
大家都知道聚合代表一个完整的概念,是一个持久化存储的单元,聚合内的所有对象要么一起取出来,要么一起删除,一个聚合被保存时,内部的所有子对象的所有状态也要同时全部一起保存,只要其中任何一个子对象有改变,都应该视为整个聚合已被别人修改过,从而会引起整个聚合不能被保存。聚合也就是基于这样的前提才能确保其内部一致性和不变性的。那么假设一个聚合很大,包含了很多子对象,那么当该聚合被持久化时,很可能会保存不了,因为它在被持久化时在并发的情况下几乎总是会发现数据库中的状态比它新,也就是说该聚合已经被别人先修改过了。聚合越大,不能保存的概率越高。这种问题其实很常见,比如一个论坛中的帖子和回复,帖子和回复在一个聚合中,帖子是聚合根,一个帖子包含多个回复。这点大家应该都认同(我以前也这样觉得的,但现在有了新的看法)。在这个例子中,假设多个人同时对一个帖子进行回帖,就很容易产生很多人回不了帖子的情况。原因很简单,就是我们把帖子和回复看成了一个聚合,一个整体了。那么怎么解决这种问题呢?上面那篇文章中提到说,可以设计尽量小的聚合,那么怎么才算小,如何判断当前聚合的大小是否合适呢?为了回答这个问题,就要引起下面我要谈的了。

聚合设计的原则应该是聚合内各个有相互关联的对象之间要保持不变性!这点我们平时太忽视了,我们平时设计聚合时,一般只考虑到了对象之间的关系,比如看其是否能独立存在,是否必须依赖与某个其他对象而存在。举例来说,还是拿上面的帖子和回复的例子来说明。大家都知道回复离开帖子没有意义,因为我们都觉得回复是对帖子的回复,如果连帖子都不存在了,那回复还有什么存在的必要吗?另外,一个帖子有多个回复。鉴于以上的分析,我们很自然的会设计一个聚合,并把帖子作为聚合根,回复作为其子Entity。但这样的设计其实已经会导致我上面一段分析的问题了。那么我们错在那里呢?我认为根本原因就是我们在分析设计聚合时只是片面的分析对象的关系而完全忽视了对象之间的“不变性”规则!那么帖子和回复之间的不变性规则是什么呢?答案是没有不变性规则。如果一定要勉强的找出一个不变性规则,那就是任何一个回复都必须有一个对应的帖子,否则回复没有意义。但是反过来,帖子不会对其内部包含的回复有任何不变性要求。那么这样看来,从不变性的角度来说,回复是可以独立于帖子的。那么从关系的角度去理解是不是也是这样的呢?再举个例子,一个论坛一般有三个典型的元素(版块【Section】,帖子【Thread】,回复【Post】),这三个元素的关系是:一个Section关联多个Thread,一个Thread关联多个Post,奇怪的是,我们为什么常常认为Thread可以独立于Section而单独存在,而Post就不能独立于Thread单独存在了呢?原因你可能会说,因为Section只是对Thread的某个归类,而Post是因为Thread而产生的,虽然关系都是1:N,但是关系的强度不同。确实,从关系的强弱来看,确实如上面所说。但是关系的强弱就可以代表不变性了吗?我认为的不变性应该是这样的,比如a=b+3,假设我有一个规则说a不能大于100,那么就意味着b不能大于97,这就是不变性。所谓的不变性就是说某个规则不能被打破。试问帖子和回复之间有什么不变性吗?我认为,只要我能确保某个回复对应的帖子是一个不变的值对象,我就能确保帖子和回复之间永远保持不变性,因为回复的帖子永远是那个它当初回得那个帖子。

那么我的结论是什么呢,就是设计聚合时,除了要考虑聚合内对象之间的关系外,更重要的还要考虑这些对象之间的不变性是什么?如果没有不变性,对象何必聚合在一起呢?

恭听大家的意见和批评,其实我有时候知道自己可能有想法上的问题,但是我的确在动脑子,我从来不相信权威,只相信自己的思考结果。再贴一张jdon上的论坛的模型图,其实我看的比较费解,感觉和我上面的认为相差很大,尤其是我看不到不变性规则方面的描述。还请banq解释一下,我其实不太希望看到直接最后的结果模型图,而更喜欢看到分析的过程,这才是对大家最有帮助的。banq不要生气啊,呵呵。

jdon的登录功能很有问题啊为什么我都登录了,但是右上角还是显示为注册和登录?还有,为什么我不能编辑帖子了呢?这种这么明显的问题还希望尽早处理掉啊,否则对初次来这个论坛的人印象很差的,感觉非常业余。

楼主提出的不变性的设计很好,在面向对象设计中有两个基本原则:高内聚,松耦合,DDD中只讲了聚合的重要性,但是没有对如何设计聚合给出详细的描述。如果聚合设计的不恰当,聚合根聚合了不该聚合的东西那么久必然导致低内聚,高耦合。至于聚合的边界是什么,我目前想到的就是生命周期以及楼主所提及的不变性。聚合根一般生命周期是要长于被聚合的对象,不能说聚合根老大都挂啦,被聚合的小的们还活着,另外一方面聚合根内部的状态在整个聚合根的生命周期内都要保证不变性。

2011年11月29日 00:01 "@tangxuehua"的内容
从不变性的角度来说,回复是可以独立于帖子的。 ...

回复贴和帖子之间的结构关系一旦创建应该是不变的。另外,正如你所说,很多人都在回复,这种回复事件会影响帖子的状态ThreadState(当前贴的最新回复时间和最新贴被不断更新),因此ThreadState和ForumState是独立于聚合根外面存在的,这个案例类似DDD中订单和价格那个案例。


不变性的理解有多个角度:比如生命周期一致,或者业务规则约束,不变性是让数据成为有层次有边界的重要依据,这是区别于数据表扁平式数据结构的重要地方。

希望多多交流,共同进步。

按照banq的设计,ForumState和ThreadState是不包含在帖子聚合内的,那么它们看你的设计应该是值对象,因为只包含了一些关于版块或帖子的统计信息。
但是按照我的理解,值对象只是一个值,没有独立存在的意义,它必须用于描述某个Entity才有意义。那么为什么banq你能把ForumState,ThreadState放到聚合之外呢?
它们到底是以什么样的方式存在着呢?据我的理解,能独立存在具有独立生命周期的一定是聚合根实体。

另外,其实我对是否应该设计ForumState和ThreadState这两个对象也表示怀疑,这两个对象本质上只是一些附加信息,就是对Forum以及Thread的一些统计信息,其实这些信息即便不在
领域模型中维护,照样可以通过Forum,Thread,Post三者之间的1:N的关系而统计出来。所以,理论上这三个状态对象可以完全不用设计。所以我不会把这三个对象看作是设计的重点。

相反,我们要考虑的主要点是帖子和回复之间,如果banq也赞同帖子和回复是属于同一个聚合的话,并且帖子是聚合根的话。那么帖子和回复应该就是一个整体,是一个持久化的最小单元。
那么banq是如何解决我上面所说的多用户情况下的并发问题的呢?其实我的设计是,应该把帖子和回复都设计为独立的聚合,它们各自都是聚合根,即可。

2011年11月29日 10:32 "@tangxuehua"的内容
那么为什么banq你能把ForumState,ThreadState放到聚合之外呢?
它们到底是以什么样的方式存在着呢? ...

先回答这个问题吧,如同DDD中price独立于Order单独成为对象一样,因为订单价格price经常被修改,其生命周期和Order不一致,如果放入Order聚合边界中,那么修改Price就要通过Order进行,这时会将Order整个对象锁住,并发性能相当差,所以,锁住Price这个值对象并发性能好。

ThreadState对于Thread的关系类似price和Order关系,只要有事件发生,ThreadState就会发生改变,至于ThreadState为什么设计成值对象,也就是我为什么认为状态是值对象,可见这个帖子:不变性immutablity设计

下面图可能很好说明我的意思:

现在我也对如何设计聚合以及如何确定聚合边界有了一些看法:

一个聚合是由一些列相联的Entity和Value Object组成,一个聚合有一个聚合根,聚合根是Entity,整个聚合被看成是一个数据修改的单元,也就是说整个聚合内的所有对象要么同时被保存,要么都不能保存,即保存到数据持久层时必须以覆盖的方式来保存,而不是追加方式或合并的方式来保存,否则无法确保聚合内的对象的数据一致性。另外,整个聚合的不变性约束由聚合根负责维护。作为推导的一个结论:我们不能只保存一个聚合内的一部分对象;聚合内的所有实体和值对象应该总是一起被取出来一起被保存,因为一个聚合是一个数据持久化的单元,不需要考虑将整个聚合根取出来有性能问题,因为任何一个聚合根都有明确的边界。目前的内存缓存框架都已发展的比较成熟,性能已经不是问题;如MongoDb,MemCache,NoSQL,等等;

聚合内的对象之所以聚合在一起的关键原因不是因为它们具有一些关联关系或依赖关系,而是因为聚合内的对象之间具有某些不变性规则,在任何时候,聚合内的所有这些对象必须满足这些不变性规则。所以,如果一些对象之间看似有一些关联关系或依赖关系,但是他们之间不具有任何不变性约束,那么就不应该把这些对象放在一个聚合中,否则只会增加这些对象之间不必要的耦合性,增加对象维护的难度;(Remembering that aggregates are not about composition, but about managing invariants, we don't compose entities on an aggregate root only as a matter of convenience)。那么为什么一些对象之间有不变性约束后就一定非要聚合在一起不可呢?首先需要先明确一下什么是聚合,聚合是一个整体,是修改数据的一个最小单元,一个聚合有一个头,即聚合根,聚合根维护了整个聚合的不变性,所以整个聚合在外面看来就是一个对象,而不是多个对象的组合。另外一点非常重要,聚合在被持久化到数据库时,是以完全覆盖的且事务的方式保存。好了有了前面的共识之后,我们再想想为什么聚合能保证多个对象之间的不变性规则约束?其实很只要真正理解了前面的约束之后就很容易理解了。你想想不管一个聚合中有什么约束,所有的约束由该聚合自己维护,所以就可以确保数据在领域模型级别就是完全一致的,没有任何违反规则的错误数据,即内存中的数据都是正确的。再加上这些正确的数据被持久化时是以完全覆盖的且事务的方式保存,从而也确保了数据库里的数据不可能出现不一致。这里唯一让你可能担心的问题是,如果多个用户同时更新一个聚合时,会产生并发冲突,此时将会使系统变得不可用!其实我认为这不是个问题,因为现在的支持高并发写的分布式存储数据库已经非常成熟,比如淘宝的oceanbase(已经开源了),还有那些NoSQL也支持,或者用分布式缓存或MongoDB也效率不错。就算没这么好的存储机制支持,用传统的数据库来存储,我相信也不会有大问题,现在的数据库已经不是10年前的数据库了,在处理高并发写的能力上已经不是同日而语了。其实并发冲突并没有你想的那么严重,一般通过select before update,以及version乐观锁定,就没问题了。支付宝一天几千万比在线交易,全部是强一致性,不然不叫在线交易系统。聚合根的存储属于单点存储,不能用最终一致性。最终一致性是弱一致性的一种特殊方式,但是最终一致性往往用于处理分布式系统中同一份数据在多个地方有备份,然后可能会出现多个地方数据不一致的问题,但是最终都会一致即同步完成。具体大家可以看看CAP定理。

所谓的不变性约束是指:假设有一个采购订单Order,一个Order下有多个订单项OrderItem,假设有一个约束是,该采购订单的总额不能超过100元。那么订单的总额不能超过100元就是一个不变性约束;那么Order和OrderItem聚合在一起就显得很有意义。在这种情况下,有Order来维护这个规则,当整个订单被保存时,比如采用覆盖的方式保存到数据库。再举个例子,比如一个论坛中有帖子和回复,大家都知道一个帖子有多个回复,回复离开帖子没有意义。所以大家很自然会认为帖子和回复应该在一个聚合内,帖子是聚合根。但是这样其实很有问题,仔细想想会发现帖子和回复之间并没有不变性约束规则,回复和帖子之间只有一个简单的1:N的关系而已。如果每次在添加一个回复时,都把帖子先取出来,然后在帖子的回复列表中把新的回复添加进去,然后再保存整个帖子,那么不难想象,这样做无疑是小题大做,并且每次为了更新一个回复或新增一个回复,就要把整个帖子取出来,这样做无疑非常浪费内存,并且在多用户并发回同一个帖子的情况下则会更糟糕。实际上仔细分析一下,帖子和回复都应该是聚合,并且分别都是聚合根,我们要确保的仅仅是回复的帖子不能被修改即可。添加一个回复实际上和帖子无关,帖子根本不关心已经有多少个回复了。这点和之前的订单的例子不同,订单需要准确维护其包含的所有订单项以便能够计算出总价是否超出100元。其实这么多问题还是不足以详细说明什么样的对象该被聚合在一起,这里只是作为抛砖引玉,引发大家思考如何设计聚合。

一个聚合需要具备哪些更多的特征呢?1)需要具备前面说的基本特征;2)聚合内的子对象要么是值对象,要么是只读的实体,为什么需要只读,因为聚合的子实体是可以被临时传递到外部的,要是外面的对象调用子对象的某个方法修改了子对象的属性,那么就意味着绕过聚合根修改了聚合内的东西,这样就无法确保聚合内的不变性了;3)如果聚合根有集合类型的属性,那么该集合也必须是只读的,即不允许别人在外部添加或删除集合的元素,否则也同样无法确保聚合的不变性。总之,我们要避免任何可能从外部修改聚合的行为发生,所有修改聚合的行为必须通过聚合根来实现。所以,理论上我们推荐大家在聚合内尽量设计值对象,原因大家多想想吧!其实从逻辑哲学的角度去思考,值对象表示了不变性,值对象表示一个值,值可以用来描述事物,事物就是实体。要是实体是由其他实体来描述,而其它实体是可变的,那么如何确保被描述的实体是可控的?大家想想为什么DDD书中,为什么要在OrderItem中存放当时购买时的Price就知道了。要是直接引用Product对象,那么会导致OrderItem引用了一个可变的对象,就无法确保订单的不变性约束。而唯有持久一个不变的值对象,才能维持其不变性。

Evans关于聚合的两条推荐准则:1)聚合不要设计的过大,过大的聚合很难确保不变性,从而很难确保数据的强一致性;2)聚合与聚合之间不要通过引用的方式来关联,而应该通过ID关联,通过ID关联也同样能表示聚合之间的关系,并且具有更好的性能和可伸缩性,聚合根之间通过ID关联的好处是:不会因为Load一个聚合根而把其他关联的聚合根一起Load出来,这样也避免了Load一个聚合根会把整个数据库Load出来的风险;另外,对ORM的要求也很低,不需要ORM支持LazyLoad;聚合根与聚合根之间的关系不像聚合内的Entity之间这么强烈内聚,它们之间仅仅是某种比较弱的关联关系,每个聚合根都有其独立的生命周期;

[该贴被tangxuehua于2011-12-06 09:28修改过]

首先感谢楼主,昨天晚上我也在想聚合只所以是聚合是因为不变性,但不变性是太泛的概念了,今天看到了你的文章,同感,很有共鸣,因为业务不不变性才决定了聚合,如果业务范围变了,聚合也就会改变。
但有些观点和楼主不一样,自己也在思考怎样才是正确的。
就是在修改聚合根关联的实体时,楼主认为应该返回实体的不变对象,但如果是不变对象,聚合根不是也不能修改这个实体,还是在为这个实体建两个对象,一个可变的一个不变的,可变的根用,不变的给外面,这点没太听明白楼主的想法。
按照DDD书里说的,边界外不能持有聚合根导航到的实体,但聚合根可以返回一个副本,这个观点我也很怀疑,那修改时,得先修改了副本,再把副本传给根,让根来决定怎么保存。我觉得这样使用领域服务也太麻烦了,API不好用.再说根要怎么处理聚合实体的修改,我不太同意整个聚合都保存,没有必要为了聚合内一个小的变动就保存整个聚合啊,整个无非也就是想内存和数据库一致,不全部保存也可以做到,先把内存中的更新了,再保存变的地方就好了。更进一步,我觉得还不如让外界可以持有聚合内的实体,但必须从根得到,这样内存中只有一份和数据库的映射,更新聚合内的实体的时候,内存和数据库自然就是一致的。
总结下我的观点:聚合的首要目的是保持聚合内的不变性,通过聚合暴露的接口来保证,而不是通过必须整个保存聚合。
[该贴被qiuriyuchen于2011-12-29 10:12修改过]
[该贴被qiuriyuchen于2011-12-29 10:26修改过]

2011年12月29日 10:12 "@qiuriyuchen"的内容
不全部保存也可以做到,先把内存中的更新了,再保存变的地方就好了 ...

你这样就无法确保数据库是一致的了。比如两个人同时修改一个订单,都往订单中增加一个OrderItem,虽然他们每个人在内存中的订单都是符合不变性的,比如没有超出订单总额。但是如果只是保存变动的部分到数据库,即每个人都把新增的那个OrderItem保存到数据库了,那么数据库里的Order就是超出总额的订单了。

我觉得你既然明白聚合是的目的是因为不变性而存在,即为了数据一致性而存在,那么它在内存中所做的所有的一致性努力为什么可以被拆开来部分的保存聚合的状态到数据库?那在内存中所做的维护不变性的努力不是白做了?我觉得你还没明白什么叫“a aggregate is a unit of data changes”,聚合是一个修改数据的最小单元。既然是一个最小单元就是不能被拆分,就是在被持久化到数据库时不能被部分保存,否则无法确保数据库里的一致性;所以,这才引出了,为什么聚合不应该设计的太大,我们应该将那些真正拥有不变性的对象聚合在一起,而现实生活中真正拥有不变性要求的对象其实是很少的;

另外,关于外部如何访问聚合内的实体的问题,我认为聚合外部要访问某个聚合,原因有两个,要么要取数据,要么要更新它。Evans说过,聚合内部的实体可以被临时传出去,但是该传出去的实体不能被直接修改状态,如果要修改被传出去的实体,必须通过聚合根来做,一切要更改聚合内任何实体状态的操作都应该由聚合根来负责,因为聚合根负责不变性约束;那么我们如何确保被传出去的实体不会被外部修改呢?不能只是嘴巴上说说别人就不会改了吧!那只有两个办法,要么传递副本出去,要么传递状态只读的对象即值对象;

恩,同意,我再考虑下,原因其实是我现在正在做的是一个单机单用户的程序,所以感觉在这个程序中保持一致性,不如在内存中大家持有一份更简单,吃过饭,我再想想多用户,考虑好了,有了新想法再回,谢谢!
[该贴被qiuriyuchen于2011-12-29 10:53修改过]

2011年12月29日 10:33 "@tangxuehua"的内容
你这样就无法确保数据库是一致的了。比如两个人同时修改一个订单,都往订单中增加一个OrderItem,虽然他们每个人在内存中的订单都是符合不变性的,比如没有超出订单总额。但是如果只是保存变动的部分到数据库,即每个人都把新增的那个OrderIt ...

对于不同用户来说,一致性是通过锁来控制的呀。如果想两个人修改订单不冲突就得给这个订单加锁啊,这个和全部保存还是部分保存好像没有关系,只和锁的粒度有关吧?
以前我们公司在多用户问题上都是入库前判断,后面的不满足规则就不入了,提示发生错误。没有用锁,因为多用户同时编辑的机率比较小。

2011年12月29日 11:12 "@qiuriyuchen"的内容
对于不同用户来说,一致性是通过锁来控制的呀。如果想两个人修改订单不冲突就得给这个订单加锁啊,这个和全部保存还是部分保存好像没有关系,只和锁的粒度有关吧?
以前我们公司在多用户问题上都是入库前判断,后面的不满足规则就不入了,提示发生错误。没有 ...

内存中根本不是同一个内存对象啊。两次HttpRequest请求,会导致同一个订单在内存中有两份。用所解决不了问题。

总之,我觉得你是在逃避聚合的定义,纯粹为了方便而只保存聚合的部分。与其这样,还不如不设计聚合,让所有实体都能单独保存即可。在想想Aggregate is a unit of data changes吧。

谢谢你的回复。

锁怎么会解决不了呢,这和request请求没有关系,和数据库有关系啊,第一个用户修改时用select for update nowait来锁定订单,然后第二个用户进来想修改这个订单时,用select for update nowait就会不能修改,订单只所以能被两个人同时修改,和锁当然有关系了。
我不是逃避聚合,是在考虑它帮编程中解决了什么问题,我也是不相信权威,知道了它的好处我才会信,现在也正在学习,学习的过程就是要疑问并证实的过程,我的意思是两个人不能同时修改一个订单是要靠锁来解决的,和一起保存没关系,就算是我用部分保存,也可以先锁定这个订单。
我的观点是聚合是用来保持业务不变性,但不应该是一定每次更新部分都要从根全部再保存一遍。我同意边界内的对象要通过根导航来找到。

还有一个怪我没说清楚,我现在在做一个小项目来验证领域建模能提供什么好处,用的是javafx做的桌面程序,所以在内存中是一份数据,不好意思上次回没表达清楚。

[该贴被qiuriyuchen于2011-12-29 17:44修改过]

2011年12月29日 17:43 "@qiuriyuchen"的内容
锁怎么会解决不了呢,这和request请求没有关系,和数据库有关系啊,第一个用户修改时用select for update nowait来锁定订单,然后第二个用户进来想修改这个订单时,用select for update nowait就会不 ...

你说的锁是锁数据库吧?我以为是锁内存对象。如果是锁数据库,那是可以确保数据库的数据不会违反不变性的;

但是你是如何只保存聚合的部分的呢?在DDD中,都是通过仓储Repository来保存聚合的,比如orderRepository.SaveOrder(order);那么,对于OrderRepository来说,又如何知道当前只要保存order的一部分呢?我觉得对于OrderRepository来说,当它接收到一个Order后,要么全部将Order最新状态直接覆盖到数据库,要么先检查Order内部有哪些改动点,然后再保存;

难不成你会提供orderRepository.SaveNewOrderItem(newOrderItem)这样的方法?那这个orderRepository还是一个repository吗?

终于知道我在说什么了,哈哈,这正是我想和你讨论的呀,我的意思就是没有必要全部一起保存,部分保存就可以了。先说在单机程序中吧,如果客户端持有的是副本,我觉得可以像这样order.EditOrderItem(orderItem),因为orderItem是副本,所以应该先执行逻辑判断,再更新order聚合中的orderItem,然后再调用orderRepository.EditOrderItem(orderItem)这样的方法保存到数据库。 保存方法放在哪以及实体可否调用repository可以再讨论,先把这个讨论清了啊。这样内存和数据库也是一致的,也没破坏不变性啊。所有我才有还不如直接持有一份,这样就可以省了更新聚合内实体这一步,因为更新的就是聚合内的实体。我做的是单机版程序,所以聚合的整个生命周期比较长,整个交互过程只加载了一遍整个聚合。部分更改后,我就是更新变更部分的内存对象,再同步这部分到数据库(部分更新);
引用你的话:难不成你会提供orderRepository.SaveNewOrderItem(newOrderItem)这样的方法?那这个orderRepository还是一个repository吗?
没有人说Repository不可以修改里面的实体啊,只说了不能直接加载非根实体给客户端。
如果是Web应用的话,客户端持有一个实体的副本,这时向服务器端提交修改,要传回第一个是修改后的实体副本,还有一个参数要传回就是它所属的Order的Id,这时服务器端你应该不会把原来加载的聚合放Session吧,这时还得加载整个Order的聚合,然后再把里面的OrderItem按照传回的副本更新成新的(当然中间要加入逻辑),如果按照全部保存的说法,然后再保存整个Order聚合,我的意思是最后的一步完全可以只保存更新的部分就可以了,没有必要全部保存。结果和全部保存是完全一致的,不管是内存里的对象还是数据库里的记录。
可以再讨论一下这个方法放在哪,我觉得最合适放的地方就是Order类中加一个editOrderItem(OrderItem orderItem)的方法。它完成逻辑判断后再调用OrderRepository,关于实体可不可以调用Repository或服务可以参考http://www.jdon.com/jivejdon/thread/37289Domain Events – 救世主的文章,可以用领域事件来做,对那个观点我也是有些不一样的看法的,已经发在那个的评论里了。
里面说的实体,指的是聚合内的实体。
[该贴被qiuriyuchen于2011-12-29 19:00修改过]
[该贴被qiuriyuchen于2011-12-29 19:05修改过]