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

原文链接: 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);

而 policy 的 increaseInsuredAmount 方法的内部实现则是:

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 生命周期管理,希望你不会错过。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: mongotemplate.aggregate是Spring Data MongoDB提供的一种聚合操作方法,用于对MongoDB数据库中的数据进行聚合操作,可以实现类似于SQL中的GROUP BY、SUM、COUNT等操作。通过mongotemplate.aggregate方法,可以使用MongoDB的聚合管道对数据进行处理,包括筛选、分组、排序、计算等操作,从而得到需要的结果。 ### 回答2: MongoTemplate.aggregate是Spring Data MongoDB提供的用于执行聚合操作的方法。在MongoDB中,聚合操作用于在集合中进行数据处理和分析,以获得需要的结果。 使用MongoTemplate.aggregate,我们可以使用聚合管道来指定一系列的聚合阶段,以处理从集合中检索到的文档。聚合管道是由一系列阶段组成的,每个阶段都会对输入文档进行处理,并将处理结果传递给下一个阶段,最终生成最终的聚合结果。 聚合阶段可以包括多种操作,如过滤、分组、投影、排序、限制等。我们可以根据具体的需求选择不同的操作来构建聚合管道。 使用MongoTemplate.aggregate方法,我们可以构建一个Aggregation对象,通过调用不同的方法来添加不同的聚合阶段。例如,可以使用match方法来添加过滤阶段,使用group方法来添加分组阶段,使用project方法来添加投影阶段等。 最后,调用MongoTemplate.aggregate的结果会返回一个聚合操作的结果。可以使用聚合操作的结果来进行相关的数据分析和处理。 总之,MongoTemplate.aggregate是Spring Data MongoDB提供的一个用于执行聚合操作的方法,通过构建聚合管道,可以对MongoDB集合中的数据进行处理和分析,并获得需要的结果。 ### 回答3: MongoTemplate.aggregate 是Spring Data MongoDB 中的一个函数,用于执行 MongoDB 的聚合操作。 聚合操作是 MongoDB 中一种高级数据处理方法,用于在集合中处理数据并返回结果。MongoTemplate.aggregate 函数可以通过传入不同的聚合管道操作,实现对 MongoDB 中的数据进行聚合操作。 聚合管道是一系列的聚合操作,每个操作都会对输入进行处理,并生成输出结果。管道操作可以包括筛选、排序、分组、计算、转换等多个步骤,可以根据具体需求来组合这些操作。 MongoTemplate.aggregate 函数的参数包括聚合管道操作和输出结果的类型。聚合管道操作可以使用 Aggregation 类提供的各种操作符,如 match、sort、group、project、limit 等。结果类型可以使用 Class 类型来指定,也可以使用 AggregationResults 类型来获取带有聚合操作结果的包装器。 使用 MongoTemplate.aggregate 函数可以轻松地执行各种聚合操作,同时结合 Spring Data MongoDB 提供的其他功能,还可以更方便地进行数据的查询、更新和删除等操作。 总的来说,MongoTemplate.aggregate 函数是 Spring Data MongoDB 提供的一个方便的接口,用于执行 MongoDB 中的聚合操作,可以通过传入不同的聚合管道操作来实现各种高级数据处理需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值