领域中会包含很多实体对象,他们之间有关联关系,比如用户、订单、联系方式、商品...这些实体管理关系有一对一、一对多、多对多,如果把所有的这些实体组成一个大的对象会非常复杂,虽然现实中他们存在管理关系,但是不一定在领域模型中完全关联在一起,领域模型非数据模型,目的在于支持不变条件,而非用户界面,所以需要对已经存在的关联关系精简,得到合适力度的对象(聚合)
不变条件:是领域模型中强制实现一致性的规则,无论和是对实体或者聚合变更都要遵循业务规则。比如规定用户必须总是有一个完整地址的规则,为了遵循这个不变条件不能为用户提供单独编辑地址行的功能,从而是完整地址遭到破坏,可能这个地址需要做值对象来处理,确保不变条件有效。
这个完整地址应该包含省、市、县,小区、门牌号多部分组成的一个字符串,意思是说不能提供一个功能让用户可以直接修改由这几部分组成的字符串的功能,编辑地址功能只能是通过包含省市县、小区门牌号的UI界面提交并验证后保存。
1.管理复杂对象的关联图
1.选用单一便利方向
image.png这图反映了供应商、订单、采购产品之间的关联关系,关联关系必然是双向的,很多ORM框架都在这方面提供了很好的支持,比如一个供应商可能有许多订单,那单个订单必然属于一个供应商,但是可以根据业务实现场景去精简关联关系:
比如如果Supplier
实体需要修改联系方式其实是没有必要加载所有的Product
是没有影响的,
一个Product
实体没有必要反向关联供应商列表
在创建关联关系时要考虑关联关系实现的是什么行为,谁需要该关系才能发挥作用。
2.合格的关联关系
如果管理关系是通过对象引用来实现的,被引用的关联应该具备降低融合对象数量的资格。
比如:用户和订单之间是一对多的关系,如果在用户关联订单属性是一个Order对象数组,
表示用户所有的订单,但是业务场景可能需要的是最近一段时间的订单,这个必须有条件实现,
比如在thinkphp的orm中可以定义一个关联方法recentOrders
, 关联的查询条件根据业务需求写时间就可以了。
3.使用ID关联
现实业务逻辑,一个用户会有很多订单,但是在应用程序的解空间可能不存在要求user对order列表的引用的不变条件,这个时候使用对象引用建模会造成不必要的复杂性。这个时候可以使用ID关联,实际开发很多也都是这样做的,比如创建订单的时候没有必要完全加载用户所有信息,引用一个userid就可以了。
2.聚合的设计
2.1.围绕不变条件进行设计
不变条件必须是一直坚持的陈述和规则,比如拍卖中标必须发生在拍卖结束之前,外卖中菜品必须属于一个店铺,一个用户拥有自己的购物车,其实本质上就是业务中的一些不变性的规则,一旦打破这些不变规则,模型就开始失效了,但是为了支持业务就需要重新建模,比如现在很多外卖支持多人一起下单,这明显和普通的电商的购物车有区别。
2.2.高层次领域抽象
站在更高层次上观察关联关系进行分组,比如订单和订购项可以关联在一起。
2.3.一致性边界
确保一个系统是有用并且可靠的,需要确定哪些数据应该保持一致以及事务一致性的边界在哪里,这个时候就需要对领域中对象进行业务分组,分组对象就是聚合。
- 内部事务一致性
- 单个事务边界封装整个模型,拥有单一聚合来保持一致性,会在协作变更时出现毫不相干的冲突,表现就是数据库级别的阻塞或者更新失败。
顾客虽然和订单有关联,但是顾客添加一个地址和另一个人修改此用户的订单状态没有关系,这样实现方式必然需要同时锁定用户和订单,同时修改地址和订单状态时引发阻塞或者更新失败,取决于用乐观锁还是悲观锁。
- 没有事务边界,某些场景下会产生数据不一致性。
一个人修改订购项目,一个人修改使用的优惠策略,如果没有事务边界会导致优惠计算错误。
- 所以要根据不变条件划分事务边界
通过聚合根实现内部强一致性,比如订购项在订单的聚合根内部,只能通过聚合根访问,
聚合作为一个整体通过存储库来提取并提交到数据库存储的,存储时可以采用数据库事务
保持一致性。
2.聚合之间采用最终一致性
如果用户下单去年超过了一定金额,则可以享受优惠,订单聚合不包含忠诚度聚合,订单在创建后可以发送领域事件,忠诚度聚合去消费并更新忠诚度,最终一致性方案要得到业务方认可,不同步的聚合会造成用户体验缺失或者bug。
image.png3.特殊情况
最终一致性实现成本过高时,可以考虑在一个事务中修改两个对象。
2.4.选用小聚合
1.小聚合性能好
聚合的数据要从数据库读取、保存,聚合过大会导致性能问题,实际中应该从小的聚合开始,并逐步开始在证明添加属性到聚合是正确的时候在逐步添加。
2.大聚合容易受并发冲突的影响
大聚合往往有很多职责,聚合内部要保持一致性,并发冲突的概率会增加。
3.大聚合无法很好扩展
聚合的数据来源于存储、并需要写入,如果聚合的一部分数据要迁移到不同的数据库时比较复杂,聚合内部的依赖关系越多实现成本越高。
3.定义聚合的边界
-
与不变条件保持一致
-
与事务和一致性保持一致
-
忽略用户界面影响
不能围绕UI界面设计聚合,否则会导致聚合非常大,可以考虑用多个聚合填充UI
-
避免无用的集合和容器,需要满足不变条件时才需要聚合
-
不要专注于HAS-A关系
聚合是不应该收数据模型影响,数据之间的关联关系不一定非要在领域对象之间完全体现,还是要
参考存在的不变条件和业务场景,而不是完全实现现实环境。 -
重构聚合
聚合的重构可能是不变条件发生了变化、或者性能影响。
4.实现聚合
聚合设计是收到来自反馈所影响的持续过程,持久化、一致性和并发性都是重要的实现细节。
4.1 选择一个聚合根
要让一个聚合保持一致性,起组成部门不应该在整个领域模型中共享或者可以访问服务层,聚合选择一个实体作为聚合根,通过该聚合根
对外提供行为,聚合根协调聚合的所有变更,确保客户端不会将聚合置位不一致的状态。
1.公开行为接口
聚合根要对外提供方法供外部访问内部的对象,聚合根内部不能直接从根访问到所有对象是合适的。
2.保护内部的状态
谨慎的设置set、get方法,这样意味着内部被公开了,可能会造成其他领域对象耦合,其他调用方能够修改聚合为不一致的状态,损坏聚合的一致性原则。
3.只允许根有全局标识
聚合根拥有全局标识、可以从外部访问,聚合内的其他对象是内部标识,无法从外部访问。
4.2 引用其它聚合
-
聚合根和聚合根之间要通过ID引用,而不是用对象本身引用
-
聚合外部没有任何事物可以保留内部对象的引用。
外部保留对内部对象的临时引用是可以的,用于单个方法内部,但是也应该小心分发临时引用,可能会变的耦合,必要时最后返回共用对象的副本。
- 聚合内部的对象可以保留对其他聚合根的引用。
4.3 实现持久化
只有聚合根可以直接使用数据库直接查询获得。
image-20220824235917531.png有时认为聚合根一次加载其他所有的对象内容是不必要的,但是这种情况很少,比如定购项在没有订单的聚合根存在的情况下查出来没有意义。
这个有的时候会把订单的状态冗余到订购项表,不用查询订单表了,但是按照DDD的聚合设计订购项是依赖订单的,订单是聚合根的入口。
-
领域状态报告可以直接从数据库读取数据,无须融合领域对象。
-
删除操作必须一次性移除聚合边界的所有内容。
删除聚合根的时候,下面关联的都要全部删除,比如订单删除订购项就没有意义了。 -
尽量避免是用ORM的延迟加载功能。
因为延迟加载有时具有不可预测性,循环中有时会造成N+1查询问题,可以将聚合设置小一些,就不需要通过延迟加载提高性能了。
4.4 其他特性实现
4.4.1 实现一致性
聚合的数据保存在一个表或者多个表没有关系,重要的是聚合信息保存的时候要处于一个事务中。
4.4.2 实现最终一致性
是不同的聚合之间通过分发数据副本实现的,一般都是异步消息。
4.3 实现并发性
聚合可以通过封装非聚合根的实体来保护不变条件,但是可能有多个用户都在操作同一个聚合根,聚合根的数据可能会并发写入。
- 乐观锁:给每个聚合根增加version字段,保存数据时数据库的version和当前聚合根不一致说明中间有人改过,提示保存失败。
- 悲观锁:可以采用数据库的redis的setnx,获取失败可以根据业务场景自旋或者返回失败
这里除了锁还涉及一个幂等键的问题,获取到锁后必须有幂等键检查。可以自旋的场景:两个请求同时更新一个订单状态为退款,更新完以后以后给用户退钱,这个时候在自旋的后可以检查订单状态如果是已经处于退款中或者退款完成则直接返回。不能自旋的场景:两个人同时修改一个文章内容,如果自旋后没有直接保存会直接覆盖第一个人提交的修改,类似于git push --force,没有使用git pull拉取远程代码,所以一般在保存的时候都会通过sql的update的where额外的条件做进一步断言,另外保存文章这个场景需要第一个人编辑的时候就提前锁定会更好。
网友评论