关于基于DDD+Event Sourcing设计的模型如何处理模型重构的问题的思考
阅读本文假设你已经对DDD和Event Sourcing比较了解。
基于DDD+Event Sourcing设计的模型如何处理模型重构?
问题背景:ddd的核心是聚合,一个聚合内包含一些实体,其中一个是根实体,这个大家都有共识;另外,如果将DDD与Event Sourcing结合,那就是一个聚合根会产生一些event;那么这里的问题是:如果一个领域对象,一开始是entity,后来升级为聚合根,但是该entity之前根本没有对应的event,因为它不是聚合根。因此它升级后我们如何通过event sourcing获取升级后的聚合根最新状态;同理,相反的例子是聚合根降级为实体,该如何处理。基于哲学方面的一些思考:
之前ORM时代,数据就是数据,我们直接存储数据,然后读取存储的数据即可,很简单;
现在Event Sourcing了,数据用事件表示,我们不在存储数据本身,而是存储与该数据相关的所有事件,包括数据被创建的事件在内;这种思维是好的,我们希望通过保存数据的“完整的历史”来达到任意时刻都能还原数据的目标。但是我们仅仅保存event就真的保存了“完整的历史”了吗?显然不是,我认为历史包含两部分信息:1)事件;2)逻辑;目前我们只保存事件而没有保存逻辑;但是我们又要希望通过事件溯源还原“完整的历史”,怎么可能?!
但是,我们为了确保能还原数据,所以代码重构都小心翼翼,比如确保尽量不改原来的事件,尽量用新事件实现业务变化或新业务功能。另外,对于处理事件的逻辑也尽量确保能兼容老的事件。之所以要这么别扭是因为我们没办法把历史的事件和历史的事件处理逻辑一同持久化。实际上我们总是在用老的事件与最新的代码逻辑相结合进行重演,这实际上是很危险的事情。
然后碰到我上面提出的尖锐问题,实际上很难有优雅的解决方案了。上面我提出的问题其实很难解决:无论是聚合根升级还是降级,都意味着新对象的事件我们无法获取或者说根本之前没有任何与新对象相关的事件,自然就无法再用事件溯源的方式得到该对象了。而实际上这个对象什么都没做,只是做了个升级或降级处理而已;
那么问题出在哪里呢?我认为是DDD的聚合导致的问题。我们之所以要设计出聚合,主要原因是为了通过聚合的手段确保业务上具有内聚关系具有数据一致性规则(Invariants)的领域对象之间方便的维护其一致性;而事件溯源从概念上来说并不针对整个aggregate,而是针对单个的entity.现在一旦将DDD与event sourcing结合,那势必会导致模型中一些对象没有与其相关的event,这就会给我们后期模型重构带来巨大的问题。
既然问题找到了,那我想解决方案也很容易了。就是如果要用event soucing,就必须抛弃聚合的概念,让一切对象回归平等,所有的entity都相互平等,当然value object还是保持不变,因为其只是一个值而已;然后让每个entity都能产生事件,这样就不会有因为某些entity没有事件而导致重构时遇到巨大问题的情况了。
自此,也许你会说,没有聚合那不就是贫血模型了吗?我不这么认为!聚合的意义有两个:1)更好的表达业务完整概念,因为有些对象却是在概念上就是内聚其他一些对象的,比如一辆汽车有四个轮子,汽车内聚轮子;2)为了维护对象之间的Invariants,这个不多解释了,我想大家都理解;那我认为第一点其实和功能无关,是概念上好理解才这样做;关于第二点维护对象之间的Invariants,我认为有很多方法,不必必须显式的定义聚合来实现,我们只要确保所有的entity都能很好的规定其自身哪些属性必须有,哪些属性不能变,哪些可以变,哪些可以在什么范围内变,等等规则约束。这样也同样能实现不变性约束;实际上这种方式和DDD看起来非常接近,但是绝不是贫血模型,因为贫血模型是所有entity的所有属性当然id除外都有get;set;然后所有逻辑全部在service中以transaction script的方式实现;而我上面说的方式实际上entity该有的职责和业务规则判断还是放在entity内部做掉,但是和经典DDD相比,经典DDD的大部分规则和一致性逻辑都在聚合根内完成,而我的方式则由各个entity合起来实现相同的规则和一致性约束;
到这里,其实event sourcing还是面临小范围(单个entity内部)的代码重构的压力,但这我们总能找到相对成本比较轻的解决方案,比如尽量不改原来事件,只新增事件属性,不删除事件属性。即总是采用与原事件兼容的修改方式来修改事件,这其实是可以接受的。
大家觉得怎么样呢?很希望能多听听大家的想法。
TAG: