软件设计 2017-07-13
[blockquote]
DDD理论学习系列——案例及目录
[/blockquote]
聚合,最初是UML类图中的概念,表示一种强的关联关系,是一种整体与部分的关系,且部分能够离开整体而独立存在,如车和轮胎。
在DDD中,聚合也可以用来表示整体与部分的关系,但不再强调部分与整体的独立性。聚合是将相关联的领域对象进行显示分组,来表达整体的概念(也可以是单一的领域对象)。比如将表示订单与订单项的领域对象进行组合,来表达领域中订单这个整体概念。
我们知道,领域模型是由一系列反映问题域概念的领域对象(实体和值对像)组成,聚合正是应用在领域对象之上。如果要正确应用聚合,我们首先得理清领域对象间的关联关系。
在设计领域模型的初期,我们习惯专注于领域中的实体和值对象,而忽略领域对象之间的关联关系,以至于我们会基于现实业务场景或数据模型来建立关联关系。这样就会引入大量不必要的关联,比如下图:
然而图中的关联关系都是必要的吗?我想未必。这样的关联关系,加大了实现领域模型的技术难度。
当我们建立对象的关联关系时,思考以下问题:
而如何简化关联呢?
如果遵从这个原则,那我们的领域模型将会是这样的:
领域对象间清晰的关联关系,能够清晰反映领域概念,便于我们设计出比较理想的领域模型。理清了领域对象间的关联关系,我们下面来应用聚合。
领域对象不是孤立存在的,往往几个对象的组合才能表示一个完整的概念,如上文所说的订单和订单项。那如何组合对象呢?也就是我们本文的主题。 聚合是领域对象的显示分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。 这句话涉及到几个概念,我们来拆解一下:
其中我们需要澄清下领域不变性:
[blockquote]
Domain invariants are statements or rules that must always be adhered to. 领域不变性指的是必须遵守的陈述或规则。换句话说,就是领域内我们关注的业务规则。比如,订单必须具有唯一订单编号、订单日期;订单必须冗余商品的基本信息(名称、价格、折扣);订单至少有一个商品,删除商品时,订单项需要一并删除;等等。
[/blockquote]
前两句话综合来说,就是聚合通过对领域对象的封装来体现领域中的业务规则。 而边界的目的是分离聚合内外,聚合内通过事物来保证强一致性。
总而言之,聚合不仅仅是简单的对象组合,其主要的目的是用来封装业务和保证聚合内领域对象的数据一致性。
一致性和事务性边界,又如何理解呢? 一致性是指数据一致性,事务性指的数据库的ACID原则。 下面我们来着重介绍下。
为了确保系统的可用性和可靠性,我们必须保证数据的一致性。
[blockquote]
订单支付成功后,订单状态要更新为已支付状态,且现有库存要根据订单中商品实际销售数量进行扣减。
[/blockquote]
下面我们就以这个案例,来分析说明。
针对这个用例,传统的做法就是,在一个事务中,去更新订单状态和扣减库存。这样似乎满足了业务场景需求,但是我们不得不考虑另外一个问题——并发冲突。比如,在更新订单的同时,商城来了一批货,要进行库存更新,这个时候就存在潜在的冲突,而问题可能表现为数据库级别的阻塞或更新失败(由于悲观并发),如下图:
这个并发问题我们该如何解决呢? 首先我们要分析问题的原因,这个用例陈述了具体的业务规则。我们错误的将业务涉及到的所有领域对象都放到了一个事务性边界中去了。其实这个用例涉及到三个子域,销售、商品、库存子域。从领域不变性的角度来看,我们应该维护各自子域内业务规则的不变性,而不是为了业务场景实现一概而论。按照这个思想,我们把订单、商品、库存拆分成三个独立的聚合,如下图所示。
从图中我们可以看出,每个聚合都有自己的事务一致性边界。也就是说这三个聚合分别在不同的事务中维持自己的不变性,也就是说聚合是用来维护内部事务一致性。那针对以上用例,明显需要跨域多个聚合,我们又该如何保证一致性呢?因为我们不能在一个事务中更新多个聚合,所以我们只能实现最终一致性。
最终一致性的实现原理是借助领域事件来完成事务的拆分,如下图所示。
而针对我们的用例,在更新订单支付状态时,发布一个订单已支付的领域事件,库存聚合订阅处理这个事件,即可完成库存的更新。事务拆分如下图:
凡事没有绝对,在一个聚合中仅修改一个聚合是最佳方法。但有时候,在一个事务中更新多个聚合也是可行的,这需要结合具体场景区别对待。另外还有一点需要澄清,以上使用一致性的目的,主要是针对聚合的修改。在一个事务中加载和创建多个聚合是没有问题的,因为并不会导致并发冲突。
根据上面的阐述:聚合不仅仅是简单的对象组合,其主要的目的是用来封装业务和保证聚合内领域对象的数据一致性。
那聚合设计时要遵循怎样的原则呢?
聚合是一个复杂的概念,其正确应用的关键是领域对象间关联关系的把握和领域不变性的理解。其实现的难点在于一致性的维护上:聚合内实现事务一致性,聚合外实现最终一致性。聚合的设计是一个持续性的活动,不可能在初始阶段就能设计出完美的聚合,我们应该根据对领域知识的深入和经验的积累持续改进聚合的设计。