领域驱动设计学习成果分享

  1. 创建领域对象采用构造函数或者工厂,如果用工厂时需要依赖于领域服务或仓储,则通过构造函数注入到工厂;
  2. 一个聚合是由一些列相联的Entity和Value Object组成,一个聚合有一个聚合根,聚合根是Entity,整个聚合被看成是一个数据修改的单元,也就是说整个聚合内的所有对象要么同时被保存,要么都不能保存,即保存到数据持久层时必须以覆盖的方式来保存,而不是追加方式或合并的方式来保存,否则无法确保聚合内的对象的数据一致性。另外,整个聚合的不变性约束由聚合根负责维护。作为推导的一个结论:我们不能只保存一个聚合内的一部分对象;聚合内的所有实体和值对象应该总是一起被取出来一起被保存,因为一个聚合是一个数据持久化的单元,不需要考虑将整个聚合根取出来有性能问题,因为任何一个聚合根都有明确的边界。目前的内存缓存框架都已发展的比较成熟,性能已经不是问题;如MongoDb,MemCache,NoSQL,等等;
  3. 聚合内的对象之所以聚合在一起的关键原因不是因为它们具有一些关联关系或依赖关系,而是因为聚合内的对象之间具有某些不变性规则,在任何时候,聚合内的所有这些对象必须满足这些不变性规则。所以,如果一些对象之间看似有一些关联关系或依赖关系,但是他们之间不具有任何不变性约束,那么就不应该把这些对象放在一个聚合中,否则只会增加这些对象之间不必要的耦合性,增加对象维护的难度;(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定理。
  4. 所谓的不变性约束是指:假设有一个采购订单Order,一个Order下有多个订单项OrderItem,假设有一个约束是,该采购订单的总额不能超过100元。那么订单的总额不能超过100元就是一个不变性约束;那么Order和OrderItem聚合在一起就显得很有意义。在这种情况下,有Order来维护这个规则,当整个订单被保存时,比如采用覆盖的方式保存到数据库。再举个例子,比如一个论坛中有帖子和回复,大家都知道一个帖子有多个回复,回复离开帖子没有意义。所以大家很自然会认为帖子和回复应该在一个聚合内,帖子是聚合根。但是这样其实很有问题,仔细想想会发现帖子和回复之间并没有不变性约束规则,回复和帖子之间只有一个简单的1:N的关系而已。如果每次在添加一个回复时,都把帖子先取出来,然后在帖子的回复列表中把新的回复添加进去,然后再保存整个帖子,那么不难想象,这样做无疑是小题大做,并且每次为了更新一个回复或新增一个回复,就要把整个帖子取出来,这样做无疑非常浪费内存,并且在多用户并发回同一个帖子的情况下则会更糟糕。实际上仔细分析一下,帖子和回复都应该是聚合,并且分别都是聚合根,我们要确保的仅仅是回复的帖子不能被修改即可。添加一个回复实际上和帖子无关,帖子根本不关心已经有多少个回复了。这点和之前的订单的例子不同,订单需要准确维护其包含的所有订单项以便能够计算出总价是否超出100元。其实这么多问题还是不足以详细说明什么样的对象该被聚合在一起,这里只是作为抛砖引玉,引发大家思考如何设计聚合。
  5. 一个聚合需要具备哪些更多的特征呢?1)需要具备前面说的基本特征;2)聚合内的子对象要么是值对象,要么是只读的实体,为什么需要只读,因为聚合的子实体是可以被临时传递到外部的,要是外面的对象调用子对象的某个方法修改了子对象的属性,那么就意味着绕过聚合根修改了聚合内的东西,这样就无法确保聚合内的不变性了;3)如果聚合根有集合类型的属性,那么该集合也必须是只读的,即不允许别人在外部添加或删除集合的元素,否则也同样无法确保聚合的不变性。总之,我们要避免任何可能从外部修改聚合的行为发生,所有修改聚合的行为必须通过聚合根来实现。所以,理论上我们推荐大家在聚合内尽量设计值对象,原因大家多想想吧!其实从逻辑哲学的角度去思考,值对象表示了不变性,值对象表示一个值,值可以用来描述事物,事物就是实体。要是实体是由其他实体来描述,而其它实体是可变的,那么如何确保被描述的实体是可控的?大家想想为什么DDD书中,为什么要在OrderItem中存放当时购买时的Price就知道了。要是直接引用Product对象,那么会导致OrderItem引用了一个可变的对象,就无法确保订单的不变性约束。而唯有持久一个不变的值对象,才能维持其不变性。
  6. Evans关于聚合的两条推荐准则:1)聚合不要设计的过大,过大的聚合很难确保不变性,从而很难确保数据的强一致性;2)聚合与聚合之间不要通过引用的方式来关联,而应该通过ID关联,通过ID关联也同样能表示聚合之间的关系,并且具有更好的性能和可伸缩性,聚合根之间通过ID关联的好处是:不会因为Load一个聚合根而把其他关联的聚合根一起Load出来,这样也避免了Load一个聚合根会把整个数据库Load出来的风险;另外,对ORM的要求也很低,不需要ORM支持LazyLoad;聚合根与聚合根之间的关系不像聚合内的Entity之间这么强烈内聚,它们之间仅仅是某种比较弱的关联关系,每个聚合根都有其独立的生命周期;
  7. 聚合内的非跟的Entity以及Value Object之间不要相互引用,聚合内的所有Child可以对根Entity持有引用,如果一个Child Entity需要和另外一个Child Entity交互,则因该通过聚合根完成;
  8. 我们应该尽量减少聚合之间关联,尽量做到单向关联,只保留确实需要处理的经常需要用到的遍历方向的关联;
  9. 仓储应理解为一个在内存中维护一系列聚合根的集合;
  10. 一个聚合根配备一个仓储;
  11. 仓储提供的接口应该总是接受聚合根或返回聚合根,不能返回聚合内的其他Entity或Value Object;
  12. 不要把仓储理解为DAO,仓储属于领域模型的一部分,代表了领域模型向外提供接口的一部分,而DAO是表示数据库向上层提供的接口表示;
  13. 仓储的目的不是为了支持界面查询,不要给仓储中设计一些目的是为了为界面提供显示数据的接口,仓储提供的所有接口应该仅为领域模型使用;基本的仓储接口只需要三个:Add,Remove,GetById,其他的扩展接口可以根据业务需要扩展接口声明;
  14. 如果一个操作仅由一个聚合根就可以完成,那么直接调用该聚合根完成即可;
  15. 领域服务表示领域模型中的一些业务操作,这些操作通常由多个聚合根或仓储或其他领域服务相互协作完成,那么需要为这些操作建立领域服务,在领域服务中以过程化的方式来一步步首先根据各个聚合的ID获取到操作的相关聚合根,然后调用聚合根完成整个业务操作;比如资金转帐,这是经典的领域服务的例子;再比如在调用某个聚合根做一个数据更新之前需要先判断一些业务规则,但是这些判断规则不能在该聚合根内做,因为这样做可能会导致聚合根依赖于外部的领域服务或仓储,此时,应该交给领域服务来完成规则校验和聚合根数据更新的整个过程。领域服务可以依赖仓储或聚合根;
  16. 领域服务依赖仓储时,工厂依赖于领域服务或仓储时,都因该采用构造函数注入的方式,这样可以避免领域模型中不会出现DependencyResolver.Resolve<T>()这样的语句;
  17. 切忌不要因为领域服务的引入让聚合根变得贫血,聚合根应该有的职责还是必须要由聚合根来承担;
  18. 聚合根内不要依赖领域服务或仓储,如果你发现一个聚合根的职责需要依赖于某个领域服务或仓储来帮忙完成一些其他的逻辑(像判断业务规则之类),那么通常你要考虑这个职责不应该由该聚合根来承担,而应该建立合适的领域服务来承担;聚合根的主要职责是管理其内聚的所有Child Entity或Value Object的业务完整性;
  19. 领域驱动设计时,为对象分配职责时,可以参考信息专家模式:将职责分配给拥有执行该职责所需信息的人;如果一个聚合根看起来拥有执行某个职责所需的信息,但没包含全部所需信息,此时则不应该将该职责分配给该聚合根,因为强行分配给它,会导致该聚合根没有内聚性,因为势必会依赖于其它的领域对象或领域服务或仓储;
  20. 要学习CQRS架构,要知道我们应该将应用程序的业务逻辑处理部分(即用户命令响应部分)和查询部分分离;我们应该用两个不同的技术来实现这两个部分的实现;用DDD领域模型来实现命令部分;用最快的查询引擎来实现查询部分;
  21. 如果要采用CQRS架构,我们需要考虑一个成熟可靠的底层框架,否则很容易导致命令端产生的领域对象的状态无法同步(后者丢失)到查询端的存储中;
  22. 领域对象上的属性可以具有get和set,因为我们平时所理解的对象不是真正的对象,而是某个事实的描述,比如图书管理系统中的一个Book对象,表示图书管中放着一本书,然后该书可能有一个入库时间。现实生活正的话,书本的入库时间绝对不可能变化,但是软件中的Book因为不是真正的现实生活中的书本,而只是表示图书馆中有一本书这个事实的描述,我们当然可以修改这个事实,因为我们可能因为之前在书本入库时所输入的入库时间是错的,需要修改该入库时间,此时就有提供set的必要了。所以,理论上任何一个Entity,除了ID之外,其他所有属性都可以更改,因为这些属性并不表示现实生活中的真正对象的特征,而仅仅只是对一个事实的描述;刚开始Book对象对书本入库这个事实的描述可能有问题,此时我们就需要修改该Book的属性;我想这个例子已经充分说明为什么可以提供get和set了;
  23. 不要总是零散的不加任何分组的设计Entity的属性,因为有些属性在逻辑上或业务上就是内聚的,代表一个完整的概念,比如Country,Province,City,Town,Street,等这些属性表示一个地址的信息,此时我们应该设计一个Address对象来表示该地址信息,此时该Address就是一个值对象。所以我们在设计Entity的属性时,要好好想想,哪些子属性其实在业务上是一个完整的概念,此时我们就需要考虑将这些相关的属性设计为一个值对象;
  24. 切忌值对象必须是只读的,值对象之所以叫值对象最主要的是因为它表示一个值,而不是一个对象;值是不会变化的,是一个明确含义的不变的事物,比如3表示一个值,表述数量是3,3永远不能变化;所以说,世界之所以存在,是因为有这些永恒不变的值对象的存在;我们只要把值对象理解为3,“abcd”这样的永恒不变的值就行了;
  25. 不要让领域模型去模拟现实,模拟用户(软件使用者)与领域模型交互的过程;领域模型要实现的应该是用户的需求,领域模型中不应该包含用户的成分,想想只有空杯子才能装水的道理,即无为以之用的道理就明白了;所以,我们在设计领域模型时首先要明白领域模型要完成的事情是什么;这方面,多看看用例图,就知道软件该做的事情了,推荐大家看的书是:Craig Larman写的《UML和模式应用》一书,非常经典;
  26. DDD中强调“领域对象是拥有行为的”。这句话我觉得说法是正确的,但是其做法难道就是“在领域对象里写方法”这么简单吗?我们常说“类应该具有生命的”,但我不认为“把方法写到类里就会让类具有生命了”,因为"把简单地把方法写到类里,其最终也只是让类变成了一潭死水,是经不起风浪的,是无法变成湖泊和海洋的”。类不会无缘无故产生行为,类能够产生行为一定是在一定的场景下发生的。在我看来,“简单地把方法写进类里是无法描述多(复杂)场景下的类的行为的”。如果有人说在他的项目里那样做没问题,那我只能说他的项目(场景)还不够复杂。其实“贫血对象”和“充血对象”都是极端的做法,而问题的关键是“类如何合理而自然地拥有行为”。在我看来,我们只能在“贫血对象”和“充血对象”之间达成一种平衡。如何把“场景”更好地融入DDD还有没公论,但我想“类合理而自然地拥有行为”应该是一条准则。

你可能感兴趣的:(领域驱动设计学习成果分享)