banq
2011-11-29 10:40
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设计

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

tangxuehua
2011-11-29 10:55
现在我也对如何设计聚合以及如何确定聚合边界有了一些看法:

一个聚合是由一些列相联的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修改过]

qiuriyuchen
2011-12-29 10:12
首先感谢楼主,昨天晚上我也在想聚合只所以是聚合是因为不变性,但不变性是太泛的概念了,今天看到了你的文章,同感,很有共鸣,因为业务不不变性才决定了聚合,如果业务范围变了,聚合也就会改变。

但有些观点和楼主不一样,自己也在思考怎样才是正确的。

就是在修改聚合根关联的实体时,楼主认为应该返回实体的不变对象,但如果是不变对象,聚合根不是也不能修改这个实体,还是在为这个实体建两个对象,一个可变的一个不变的,可变的根用,不变的给外面,这点没太听明白楼主的想法。

按照DDD书里说的,边界外不能持有聚合根导航到的实体,但聚合根可以返回一个副本,这个观点我也很怀疑,那修改时,得先修改了副本,再把副本传给根,让根来决定怎么保存。我觉得这样使用领域服务也太麻烦了,API不好用.再说根要怎么处理聚合实体的修改,我不太同意整个聚合都保存,没有必要为了聚合内一个小的变动就保存整个聚合啊,整个无非也就是想内存和数据库一致,不全部保存也可以做到,先把内存中的更新了,再保存变的地方就好了。更进一步,我觉得还不如让外界可以持有聚合内的实体,但必须从根得到,这样内存中只有一份和数据库的映射,更新聚合内的实体的时候,内存和数据库自然就是一致的。

总结下我的观点:聚合的首要目的是保持聚合内的不变性,通过聚合暴露的接口来保证,而不是通过必须整个保存聚合。

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

[该贴被qiuriyuchen于2011-12-29 10:26修改过]

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

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

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

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

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

[该贴被qiuriyuchen于2011-12-29 10:53修改过]

猜你喜欢