DDD聚合设计的困境

为什么学了一堆DDD理论,但就是无法落地呢?很多人认为它只是个理论。

最近又看了一遍《IDDD》第十章聚合,结合已有的理论知识,来反思下这个问题。

DDD聚合是什么?

最容易与DDD聚合混淆的就是OO聚合关系。

由上图可以看出,OO聚合表示了一种关联关系;而DDD聚合表示了一种边界。

OO聚合关系(Aggregation) 表示一个整体与部分的关系。通常在定义一个整体类后,再去分析这个整体类的组成结构,从而找出一些成员类,该整体类和成员类之间就形成了聚合关系。

如上图中Question与Answer的关系。一个问题与很多的回答构成一个完整的问答关系。

在OO中还有一种比聚合关系更强的关联关系:

组合关系(Composition)也表示类之间整体和部分的关系,但是组合关系中部分和整体具有统一的生存周期。一旦整体对象不存在,部分对象也将不存在,部分对象与整体对象之间具有相同的生命周期

继续以上图为例,Answer都是因Question存在而存在,当Question消亡时,Answer也自然消亡。

但像知乎,就算Question没了,Answer也会留存。

OO聚合与DDD聚合是什么样的关系呢?

因为聚合有隐含的构建关系和级联生命周期,通常会把OO组合关系构建成DDD聚合,其实组合关系只是聚合的必要条件,而非充分条件

如果是聚合,那么聚合根与实体自然是组合,存在级联生命周期,但不代表存在级连生命周期都是聚合。

特别是,混淆了数据生命周期和对象生命周期,

例如在“获取客户订单”这一业务场景下,Customer 与 Order 之间也存在整体/部分的组合关系,但它们却不应该放在同一个 DDD 聚合内。

从数据生命周期看,一般如果数据库中顾客数据删除了,那么他对应的订单也会删除。

但不适合建模成聚合。

因为这两个类并没有共同体现一个完整的领域概念;同时,这两个类也不存在不变量的约束关系。

而且聚合后,订单就不再具有独立身份,每次查询订单时候,必须提供客户身份和订单身份,构成级连身份才可以。类似于当仅仅使用订单号查询订单时,是不行的,必须带上身份证和订单号一起,才能查询到订单。

聚合困境

看似把一堆实体和值对象放一起就组成聚合,在《IDDD》中提供了一个示例。使用过JIRA的人应该很容易理解。我简单按书中的思路叙述下。

第一次尝试

这次尝试,应该是完全按产品需求直接建模。

Product,BacklogItem,Release,Sprint的确是一组组合关系。

而且Product是聚合根。

1
2
3
4
5
6
7
8
public class Product {
private Set<BacklogItem> backlogItems;

private Set<Release> releases;

private Set<Sprint> sprints;

}

虽然这完整表达了模型,但实现很残酷。不得不考虑的问题:

1、持久化时,乐观并发,增加了操作失败概率

2、影响系统性能和可伸缩性

第二次尝试

将一个大的Product聚合拆分成了4个相对较小聚合。

虽然解决了事务问题,多个用户请求可以同时创建任何数量的BacklogItem、Release和Sprint实例。

但对客户端来说,这4个较小的聚合却多少会带来一些不便。

设计小聚合

一个完整的聚合

如果要加载一个完整的聚合,需要把所有这些实体与值对象都加载出来。那系统性能和可伸缩性大受影响。

为了解决这些问题,所有提出要设计小聚合。

小聚合不仅有性能和可伸缩性上的好处,它还有助于事务的成功执行,即它可以减少事务提交冲突。这样一来,系统的可用性也得到增强。在你的领域中,迫使你设计大聚合的不变条件约束并不多。当你遇到这样的情况时,可以考虑添加实体或者是集合,但无论如何,我们都应该将聚合设计得尽量小。

聚合之间不能直接加载到同一个边界之内,得通过唯一标识引用其他聚合。

通过标识引用并不意味着完全丧失了对象导航性。有时在聚合中使用Repository来定位其他聚合。这种作法也被称为失联领域模型(Disconnected Domain Model)。

这就是矛盾体,一方面希望保障模型的完整性,我们需要完整的聚合;另一方面又有各种实现限制条件。

这些原因正是《实现业务逻辑的本种方式》中提到的单体架构演变为分层架构的局限性。

总结越来,聚合有三点特性:

1、Global entities(aggregation root and entity) is the boundary of consistency

2、Global entities(aggregation root and entity) is the boundary of lifecycle

3、Object model assumes same lifecycle boundary within the global entity

DDD困境

由聚合的困境,管窥一斑,DDD落地的困境何尝不是类似原因:

1、Domain Driven Design,but technology may have a say

2、Using fundamentalism DDD,it only works for simple cases,so does transaction script,procedure of oriented programming

3、Smaller aggregation works as fine as a EJB2-refurbished architecture

由上面的聚合示例观察,第一点的确是现实。

我们使用DDD,就是想Domain Driven,可考虑了很多技术落地因素,打破了完整的模型。选择模型还是选择性能,是放在我们面前的第一道选择题。

而第二点在现如今多运行时分布式架构中,肯定不可能像在一个单休中加载完整的聚合对象。因此当要使用原味的DDD时,只能在简单的项目中,而DDD却说要在复杂场景下再去使用。不要简单问题复杂化。这又是个矛盾体。

这些问题怎么解决?

当前能想到的解决方案似乎只有在《DDD对象生命周期管理》提到的关联对象模式。既能保证模型的完整,又能兼顾性能。

总结

聚合设计时,尽量使用小聚合。这对吗?解决设计困境了吗?

如果使用小聚合,会造成一种现象。会出现很多的service。

只有使用service,才能在聚合间跨越对象生命周期,维持一致性。

这会慢慢演化成贫血模型,因为一部分逻辑在对象中,另一部分会放到service中。

所以我们得重新审视一些指导原则。或者时时提醒是不是过多的考虑了实现细节,破坏了模型。

怎么才能更好地保证模型完整性,而兼顾当前的技术限制。

公众号:码农戏码
欢迎关注微信公众号『码农戏码』