分享我的:领域驱动设计(DDD)学习成果精简总结

11-12-03 tangxuehua

1. 创建领域对象采用构造函数或者工厂,如果用工厂时需要依赖于领域服务或仓储,则通过构造函数注入到工厂;

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

3. 聚合内的对象之所以聚合在一起的关键原因不是因为它们具有一些关联关系或依赖关系,而是因为聚合内的对象之间具有某些不变性规则,在任何时候,聚合内的所有这些对象必须满足这些不变性规则。所以,如果一些对象之间看似有一些关联关系或依赖关系,但是他们之间不具有任何不变性约束,那么就不应该把这些对象放在一个聚合中,否则只是增加对象之间不必要的耦合性,增加对象维护的难度;

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

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

6. 聚合内的非跟的Entity以及Value Object之间不要相互引用,聚合内的所有Child可以对根Entity持有引用,如果一个Child Entity需要和另外一个Child Entity交互,则因该通过聚合根完成;

7. 我们应该尽量减少聚合之间关联,尽量做到单向关联,只保留确实需要处理的经常需要用到的遍历方向的关联;

8. 仓储应理解为一个在内存中维护一系列聚合根的集合;

9. 一个聚合根配备一个仓储;

10.仓储提供的接口应该总是接受聚合根或返回聚合根,不能返回聚合内的其他Entity或Value Object;

11.不要把仓储理解为DAO,仓储属于领域模型的一部分,代表了领域模型向外提供接口的一部分,而DAO是表示数据库向上层提供的接口表示;

12.仓储的目的不是为了支持界面查询,不要给仓储中设计一些目的是为了为界面提供显示数据的接口,仓储提供的所有接口应该仅为领域模型使用;基本的仓储接口只需要三个:Add,Remove,GetById,其他的扩展接口可以根据业务需要扩展接口声明;

13.如果一个操作仅由一个聚合根就可以完成,那么直接调用该聚合根完成即可;

14.领域服务表示领域模型中的一些业务操作,这些操作通常由多个聚合根或仓储或其他领域服务相互协作完成,那么需要为这些操作建立领域服务,在领域服务中以过程化的方式来一步步首先根据各个聚合的ID获取到操作的相关聚合根,然后调用聚合根完成整个业务操作;比如资金转帐,这是经典的领域服务的例子;再比如在调用某个聚合根做一个数据更新之前需要先判断一些业务规则,但是这些判断规则不能在该聚合根内做,因为这样做可能会导致聚合根依赖于外部的领域服务或仓储,此时,应该交给领域服务来完成规则校验和聚合根数据更新的整个过程。领域服务可以依赖仓储或聚合根;

15.领域服务依赖仓储时,工厂依赖于领域服务或仓储时,都因该采用构造函数注入的方式,这样可以避免领域模型中不会出现DependencyResolver.Resolve<T>()这样的语句;

16.切忌不要因为领域服务的引入让聚合根变得贫血,聚合根应该有的职责还是必须要由聚合根来承担;

17.聚合根内不要依赖领域服务或仓储,如果你发现一个聚合根的职责需要依赖于某个领域服务或仓储来帮忙完成一些其他的逻辑(像判断业务规则之类),那么通常你要考虑这个职责不应该由该聚合根来承担,而应该建立合适的领域服务来承担;聚合根的主要职责是管理其内聚的所有Child Entity或Value Object的业务完整性;

18.领域驱动设计时,为对象分配职责时,可以参考信息专家模式:将职责分配给拥有执行该职责所需信息的人;如果一个聚合根看起来拥有执行某个职责所需的信息,但没包含全部所需信息,此时则不应该将该职责分配给该聚合根,因为强行分配给它,会导致该聚合根没有内聚性,因为势必会依赖于其它的领域对象或领域服务或仓储;

19.要学习CQRS架构,要知道我们应该将应用程序的业务逻辑处理部分(即用户命令响应部分)和查询部分分离;我们应该用两个不同的技术来实现这两个部分的实现;用DDD领域模型来实现命令部分;用最快的查询引擎来实现查询部分;

20.如果要采用CQRS架构,我们需要考虑一个成熟可靠的底层框架,否则很容易导致命令端产生的领域对象的状态无法同步(后者丢失)到查询端的存储中;

21.领域对象上的属性可以具有get和set,因为我们平时所理解的对象不是真正的对象,而是某个事实的描述,比如图书管理系统中的一个Book对象,表示图书管中放着一本书,然后该书可能有一个入库时间。现实生活正的话,书本的入库时间绝对不可能变化,但是软件中的Book因为不是真正的现实生活中的书本,而只是表示图书馆中有一本书这个事实的描述,我们当然可以修改这个事实,因为我们可能因为之前在书本入库时所输入的入库时间是错的,需要修改该入库时间,此时就有提供set的必要了。所以,理论上任何一个Entity,除了ID之外,其他所有属性都可以更改,因为这些属性并不表示现实生活中的真正对象的特征,而仅仅只是对一个事实的描述;刚开始Book对象对书本入库这个事实的描述可能有问题,此时我们就需要修改该Book的属性;我想这个例子已经充分说明为什么可以提供get和set了;

22.不要总是零散的不加任何分组的设计Entity的属性,因为有些属性在逻辑上或业务上就是内聚的,代表一个完整的概念,比如Country,Province,City,Town,Street,等这些属性表示一个地址的信息,此时我们应该设计一个Address对象来表示该地址信息,此时该Address就是一个值对象。所以我们在设计Entity的属性时,要好好想想,哪些子属性其实在业务上是一个完整的概念,此时我们就需要考虑将这些相关的属性设计为一个值对象;

23.切忌值对象必须是只读的,值对象之所以叫值对象最主要的是因为它表示一个值,而不是一个对象;值是不会变化的,是一个明确含义的不变的事物,比如3表示一个值,表述数量是3,3永远不能变化;所以说,世界之所以存在,是因为有这些永恒不变的值对象的存在;我们只要把值对象理解为3,“abcd”这样的永恒不变的值就行了;

24.不要让领域模型去模拟现实,模拟用户(软件使用者)与领域模型交互的过程;领域模型要实现的应该是用户的需求,领域模型中不应该包含用户的成分,想想只有空杯子才能装水的道理,即无为以之用的道理就明白了;所以,我们在设计领域模型时首先要明白领域模型要完成的事情是什么;这方面,多看看用例图,就知道软件该做的事情了,推荐大家看的书是:Craig Larman写的《UML和模式应用》一书,非常经典;

25.是该学习一下什么是强一致性,什么是最终一致性的时候了,必须了解什么是CAP定理;

    

47
lileiqx
2011-12-05 12:22

总结的很好,受教了,

顶起来!

bowwow94
2011-12-06 12:27

请教个问题,一般的WEB应用的话都有分页逻辑,那分页逻辑属于怎么设计

tangxuehua
2011-12-06 13:13

页面展示统一采用CQRS思想的Q端实现,和DDD无关的实现方式。怎么快怎么来。

alexyhe
2012-03-27 16:34

作者是不是把“切记”打成“切忌”呢