DDD 实践手册(4. Aggregate — 聚合)

2020 年 05 月 06 日 阅读数: 25
DDD 实践手册(4. Aggregate — 聚合)

上一篇中介绍了 DDD 中的核心概念,Entity 实体与 Value Object 值对象的概念,以及如何在项目中实现它们。而本篇文章我会介绍 DDD 中另一个核心概念,Aggregate 聚合。

什么是 Aggregate ?

其实 Aggregate 是一种模式,在代码中实现的具体形式很简单,分为两部分,首先是定义一个 Entity,作为 Aggregate Root,一般称之为聚合根。第二部分则是遵循 Aggregate 的完整性规则对领域数据进行操作。

在开始介绍具体的实现之前,我们先思考一下为什么要使用 Aggregate 这样的模式,它到底能为我们解决什么样的问题。

设想有这样一个业务场景,「客户增加保额」。从代码实现的角度来说,你可以这么做: 通过「保险单号」获取客户的保单信息,进而获取对应的保险产品信息,然后修改对应的「保额」,接着修改需要缴纳的「保费」,最后需要更新「被保险人」的信息。这样的需求实现起来其实不难,如果考虑使用 DDD 的方式,我们会设计 3 个不同的 Entity 对象,即保单(Policy),保险产品(Product),以及被保人(Insured)。然后类似的代码可能如同下面那样:

policy.increaseInsuredAmount(xxxx);
product.changePremium(yyyy);
insured.updateBill();

这样的代码在功能上并没有什么不妥,但是从设计角度出发确是值得探讨的。从「客户增加保额」的实现来看,需要牵涉到多个 Entity 的数据更新,而上面代码的问题在于将这些数据更新的逻辑零散的暴露在代码中,当后续业务需求发生变化时,开发人员很难从代码上理解业务,从而造成遗漏与错误。

在设计方法或是 API 上,我们知道方法或是 API 的颗粒度不能太细,有时候需要设计一个粗粒度的方法,将实现的逻辑隐藏在这个方法之下,而不是暴露给客户端。当 Entity 的数据发生变化时,同样应该遵循这样的理念。在许多业务场景下,Entity 之间的数据都需要遵循一致性,在上面的示例中,当进行增加保额这项业务操作后,保单,产品,被保人这些 Entity 的数据状态应该是按照业务规则保持一致的,不应该出现保额增加,但是保费不变的情况。

那么 Aggregate 又是如何解决这个问题的呢?这就需要了解一下 Aggregate 的完整性规则了。

Aggregate 的完整性规则

所谓的完整性规则又由下面两点组成:

  • 所有的代码只能通过 Aggregate Root,即聚合根这个特殊的 Entity 访问系统的 Entity,而不能随便的操作任一的 Entity。

  • 每个「事务」范围只能只能更新一个 Aggregate Root 及它所关联的 Entity 状态。

接下来让我们逐一解释这两项规则。

首先看第一条,这点很容易理解,单纯实现的话也很简单。参考之前的示例,我们可以把「保单」对象作为 Aggregate Root,而「产品」与「被保人」都作为这个 Aggregate Root 内部的成员变量。对外暴露的也只有「保单」对象上的方法。修改后的类图如下所示:

而代码也变为:

policy.increaseInsuredAmount(xxxx);

policyincreaseInsuredAmount 方法的内部实现则是:

public void increaseInsuredAmount(BigDecimal insuredAmount) {
this.product.changePremium(yyyy);
this.insured.updateBill();
}

从代码中可以看到,我们不再逐个操作不同的 Entity 对象,而是只能通过 policy 对象完成整个业务逻辑,与业务规则相关的数据完整性则由作为 Aggregate Root 的 policy 对象保证。

在理解第一条规则的基础上,我们再来看一下第二条规则。第二条规则其实从字面意义上来说很好理解,就是在一个事务范围内,我们只能更新一个 Aggregate Root 以及和它相关的数据。为了简化问题,这里的事务特指是关系型数据库的事务。

但是这两条完整性规则会引出一些设计上的取舍,你必须在实际项目上想好如何解决这些设计问题。

Aggregate 的设计

当需要实现 Aggregate 模式时,你需要解决的第一个问题就是找个一个合适的 Aggregate Root。在这个问题上无论是 Eric Evans 还是其他有关 DDD 的书籍都没有给出一个明确的答案,它们都举了一些例子,但是缺乏一个清晰的方法论来帮助架构师设计 Aggregate Root。书中的建议是「既不能太大,也不能太小」,这其实说了和没说一样。如果 Aggregate Root 设计的过大,那么无论实现什么业务规则都要拼装相同的 Aggregate Root 对象,必然有很对代码是冗余无用的。但是如果设计的很小,例如每个 Entity 都是一个 Aggregate Root,那么就很难做到每个事务只能更新一个 Aggregate Root 的要求。

我在项目上的经验是设计初期,尽量控制 Aggregate Root 的大小,不要关联过多的 Entity,造成出现「上帝类」这样的 Entity。当发现业务逻辑发生变化,需要更新额外的 Entity 状态时再丰富 Aggregate Root 的关联关系。如果项目中将 Domain 与 PO 分离,在设计 Aggregate Root 时的优势就很明显,不需要和持久层的关系型数据结构相耦合,能够在 Repository 进行自由的装配。

而另一种设计 Aggregate Root 的方法则是最近几年兴起的「事件风暴」,作为一种方法论,它可以帮助架构师与业务人员一起从业务流程中找到那些适合作为 Aggregate Root 的对象。具体如何使用「事件风暴」我会在之后的文章中讲解。

第二个在设计上需要考虑的是在分层架构的哪个部分定义事务范围。按照之前我介绍的分层架构,建议将事务控制放在 application service 那一层,与一个业务用例的粒度保持一致。

实际项目中「每次事务只能更新一个 Aggregate」的限制会比较严苛,因为当你将事务控制放在 application service 那层时也就意味着每个用例只能更新一个 Aggregate,在这种限制下需要设计一个合理的 Aggregate 就很难了,有时甚至是不可能的。如果一定要在一个事务内更新多个 Aggregate 该怎么办呢?一般我建议有两种选择。

领域事件 — 最终一致性

这种是 DDD 书籍上推荐的一种方式,使用领域事件的方式将单个 Aggregate 更新的事件广播出去,有其他对应的 hadler 收到后更新自己负责的 Aggregate。由于打破了事务一致性,因此需要某种机制来保证多个 Aggregate 的数据一致性。

使用这种解决方案的问题在于需要引入事务最终一致性的解决方案,这无疑会增加系统的复杂性。其次如果单纯为了满足单个事务与 Aggregate 的限制而脱离业务规则写了很多处理事件的 handler,那么无疑有点舍本逐末,为了 Aggregate 而 Aggregate 了。

打破规则

第二种并不能算是什么解决方案,实现起来很简单,就是打破每个事务只能更新 Aggregate 的限制,在 application service 中的单个事务中可以更新多个 Aggregate。

但是这也不是完全没有限制的,我们依然要遵循只能通过 Aggregate Root 引用 Entity 的规则,并且控制 application service 中能够访问 Aggregate Root 的数量,按照项目经验 3 个以下是可以接受的。

小结

Aggregate 是 DDD 中非常重要且特有的概念,它对外封装了 Entity 数据一致性,由此也是系统代码层面对业务规则的最直接的体现。而从 Aggregate 开始,业务知识在分析中的价值也逐渐开始体现。如何设计一个粒度合理的 Aggregate 需要丰富的业务知识与系统分析经验,而且随着业务的发展 Aggregate 也应该不断的重构。

下一篇将会是 DDD 中有关领域对象的最后一篇,我会介绍如何在项目中如何使用 Factory 与 Repository 实现 Entity 生命周期管理,希望你不会错过。

往期推荐

https://xie.infoq.cn/article/bc2245284b73ebca7b14308dc DDD 实践手册(1.Get Started)

https://xie.infoq.cn/article/2bc22422a8ace939e40125882 DDD 实践手册(2. 实现分层架构)

https://xie.infoq.cn/article/8fedd10940281f69bd228e17c DDD 实践手册(3. Entity, Value Object)

用户头像

Joshua

关注

FIND YOUR RHYTHM ENJOY YOUR RUN 2018.05.27 加入

花旗银行/360/ThoughtWorks

评论

发布
暂无评论
DDD 实践手册(4. Aggregate — 聚合)