DDD中聚合的概念

DDD中的聚合模式是最难弄清楚的一种模式,在如何确定聚合边界这个问题上,更没有一个明确的指导原则,这导致DDD的落地比较难。不过,相信你读了这篇文章应该对聚合会有更深刻的理解。

本文分三部分来讲:
1、什么是聚合?
2、聚合解决了什么问题?
3、聚合的边界划分指导原则

1. 什么是聚合?

首先我们来看下聚合模式的定义:

将实体和值对象划分为聚合并围绕着聚合定义边界。选择一个实体作为每个聚合的根,并仅允许外部对象持有对聚合根的引用。作为一个整体来定义聚合的属性和不变量,并把其执行责任赋予聚合根或指定的框架机制。

这是典型的“模式语言”,说明了聚合是什么,聚合根(aggregation root)是什么,以及如何使用聚合。

但是,模式语言的问题在于过度精炼,对于还不熟悉这些模式的人,根本不知所云。为了能深入理解聚合模式的本质,我们还是要一步步回到聚合试图解决的问题上来。

2.聚合解决了什么问题?

我们先从一个问题域开始,拿大家都能理解的企业采购系统来举例:

  • 提交人通过采购系统提交一个采购申请,采购申请中包含了本次要采购的若干办公用品(称为采购项)和对应的数量
  • 主管对采购申请进行审批
  • 审批通过后,生成订单发送到供应商出货

要设计这样的一个采购系统,不同人有不同的方法。

2.1 面向数据库设计

我见过大部分人首先想到的是库表结构的设计,也就是面向数据库编程。大部分人都能够设计出如下的几张表,如下图所示:
在这里插入图片描述
可能你会问,面向数据库的设计有什么问题呢?我一直都是这么做的啊!你一直这么做并不代表你的方法就是最合理的。

为了能够保证业务规则的正确性和数据一致性,在上面的采购系统中,我们需要考虑如下几个问题:

  • 如果采购请求被删除,则和该采购请求相关的采购项是不是都应该被删除呢?
  • 如果你的主管正在对采购申请进行审批,而你又同时在修改采购申请中的采购项,那该如何进行并发处理呢?如果设计不当,要么你主管审批的就是过期的数据,要么你更新采购项会失败。
  • 虽然上面的问题都有对应的技术解决办法,但是过早地陷入到技术细节的讨论中,会让我们错失和业务专家充分讨论的机会,而很多业务隐含的概念是在和业务专家协作过程中显现的;同时技术复杂性和业务复杂性混合在一起,让我们顾此失彼。

总之,在简单的场景下,采用面向数据库的设计简单直接,能快速实现需求。但是在较复杂的业务场景下,如果一上来就在数据库这么低的层次上考虑问题,我们会花大量的时间在表结构的设计上,而没有重视对重要的业务规则的梳理。随着业务的快速发展,由于我们最初设计考虑不当,我们会疲于应付不断出现的新需求和bug,我们会陷入沉重的泥潭,最后系统只能推倒重来。

那我们有没有一种方法能够让我们聚焦于问题领域,而不是过早地陷入到技术细节中呢?答案就是:面向对象设计

2.2 面向对象设计

面向对象设计有助于我们提高抽象的层级,在面向对象的世界中,我们看到的结构是这样的:
在这里插入图片描述

面向对象的设计方法提高了抽象层级,忽略一些不必要的技术细节(例如不用再关心表的外键、表的关联关系等技术细节了),让我们能够更加专注地聚焦到问题领域,同时业务人员也能够看懂,技术和业务专家也能够基于统一语言进行持续的交流协作。

但是,业务规则如何保证?在传统的面向对象的设计中,并没有很好的方法能够对业务规则进行约束。例如:

从业务规则上来看,当采购申请审批通过了,就不允许申请者再对采购申请中的采购项进行修改。但是在面向对象的设计中,你没法阻止程序员写出如下的代码:

PurchaseRequest purchaseRequest = getPurchaseRequest(requestId);
PurchaseItem item = purchaseRequest.getItem(itemId);
item.setQuantity(1000);
savePurchaseItem(item);

语句1取得了采购申请的实例,语句2获取了该采购申请中的一个采购项,语句3,4对采购项的数量进行修改并保存。如果该采购申请已经审批通过了,那这种修改就违背了业务规则。

可能你会说在修改之前,我先对purchaseRequest的状态进行校验,如果状态是已审批通过,就不允许修改。加上校验的代码如下:

PurchaseRequest purchaseRequest = getPurchaseRequest(requestId);
PurchaseItem item = purchaseRequest.getItem(itemId);
if (purchaseRequest.status == "HAS_APPROVED") {throw new BizException("采购申请已审批通过,不允许对采购进行修改")
}item.setQuantity(1000);
savePurchaseItem(item);

但是PurchaseItem在任何地方都能够被提取出来,并且PurchaseItem对象可以在方法间进行传递。

要满足上述的业务规则,你需要在每个对PurchaseItem修改的地方加上上面这段校验代码。如果设计不当,那这段校验逻辑就会散落在各个地方,未来要修改这段校验逻辑,你需要找出散落的每个地方进行修改,这成本可想而知。

没有设计上的约束,那要保证业务规则的正确性并不是一件很容易的事。

2.3 面向DDD的设计

让我们回到本质问题:采购项脱离了采购申请有单独存在的价值吗?
答案显然是没有什么卵用。既然采购项没有单独存在的价值,那对采购项的修改本质上是不是对采购申请的修改?

如果我们认同:‘对采购项的修改就是对采购申请的修改’这个结论,那我们就不应该将采购项和采购申请分开来看待,而应该如下图所示:
在这里插入图片描述
我们把“采购申请”和“采购项”看做是一个整体,这个比对象更大粒度的整体就称为“聚合”。(讲了这么多,终于看到聚合两个字了)

这个聚合内部的业务逻辑,例如“采购申请审批通过后,不得对采购项进行修改”,应该内建于聚合内部。为了实现这一目标,我们约定:一切对采购项的操作(增删改查),都是对采购请求对象的操作。

也就是说,在代码中从来就不应该出现savePurchaseItem()这种方法,应该用purchaseRequest.modifyPurchaseItem()和purchaseRequest.savePurchaseItem()方法代替。

现在对purchaseItem的访问必须通过purchaseRequest对象,purchaseRequest对象作为访问聚合的入口,称为“聚合根”(又是一个重要的概念)。由于聚合是一个整体,对聚合的任何操作只能通过聚合根来进行,从而业务规则在聚合内部得到了保证。

读到这里大家大致明白聚合是什么了吧。

聚合的本质就是建立了比对象粒度更大的边界,聚合了那些紧密联系的对象,形成了一个业务上的整体。使用聚合根作为对外交互的入口,从而保证了多个互相关联的对象的一致性。

3.聚合的边界划分原则

虽然到目前我们大致理解了聚合模式的概念以及聚合模式解决的问题,但聚合的边界又该如何划分呢?可能有的人会问:

既然采购项是采购申请这个聚合的一部分,那产品是不是也是该聚合的一部分?如果说是为了业务规则得到保证,那审批人、提交人都放到采购申请这个聚合岂不是更好?

哪些对象该属于为一个聚合?哪些对象不属于一个聚合?也就是聚合边界的划分问题,有没有一个可指导的原则呢?

当然有。聚合边界的划分可以参考如下几个指导原则:

1、生命周期一致性原则
2、问题域一致性原则
3、场景一致性原则
4、聚合应尽可能地小

3.1 生命周期一致性原则

生命周期一致性是指聚合内部的对象,应该和聚合根具有相同的生命周期,聚合根消失,则聚合内部的所有对象都应该一起消失。

例如,在上面的例子中,聚合根采购请求被删除,那采购项也就没有存在的意义,但是申请人、审批人、产品和采购申请却不存在该关系。

如果违反生命周期一致性原则,会带来比较严重的后果。假如提交人也是采购申请这个聚合中的对象,代码如下:

public class PurchaseRequest {private Set<PurchaseItem> items;private User submitter;...
}

其中User对象的生命周期和PurchaseRequest对象的生命周期不一致。
那么当保存采购申请对象时,也会保存User对象的信息,代码如下:

r = purchaseRequestRepository.findOne(id);
//...一些修改
purchaseRequestRepository.save(r);

同时员工管理员也可以对同一个User对象进行修改,代码如下:

User user = userRepo.findOne(r.getSubmitter().getId());
//...一些修改
userRepo.save(user);

这将导致严重的后果:对于User对象的修改不确定性!

因此如果不确定是否应该将某个对象划入某个聚合,你不妨问下:
这个对象离开了这个聚合,是不是还有存在的价值?如果这个对象离开了这个聚合有单独存在的意义,那就不应该就它划入这个聚合。

回到上面那个例子:

  • Submitter/Approver 对应的 User 对象脱离了 PurchaseRequest,仍然有单独存在的价值;
  • Product 对象脱离了 PurchaseRequest,是可以单独存在的;
    所以以上两个对象都不属于采购申请这个聚合。

3.2 问题域一致性原则

上面的生命周期一致性只是指导原则之一,有时如果只考虑生命周期一致性原则可能会引起问题。

让我们考虑一个在线论坛这样的场景:

一个在线论坛,用户可以对论坛上用户的文章发表评论。文章显然应该是一个聚合根。如果文章被删除,那么,用户的评论看起来也要同时消失。那么评论是否可以属于文章这个聚合?

现在让我们来考虑评论是否还有其他用处。

例如,用户可以对用户的文章发表评论,同时也可以对该论坛的电子图书发表评论。如果只是因为文章和评论之间存在逻辑上的关联,就让文章聚合持有评论对象,那么显然就约束了评论的适用范围。所以,我们得到了一个新的、凌驾于原则1之上的原则——不属于同一个问题域的对象,不应该出现在同一个聚合中。
在这里插入图片描述

在上图中评论这聚合根可以持有其他聚合根的id(可评价对象id), 同时聚合之间的一致性通过最终一致性来保证(文章删除发送领域事件通知删除对应的评论)。

3.3 场景一致性原则

通过上面两个原则,我们基本能够划分清楚一个聚合的边界,但是仍然会存在一些复杂的情况。这时我们可以根据第三个原则来判断:场景一致性原则。

什么是场景一致性呢?场景一致性就是场景操作频率的一致性。

在很多业务场景中,我们会对领域对象进行查看、修改等各种操作。 经常被同时操作的对象,应该属于同一个聚合,而那些极少被同时关注的对象,即使上面两个原则都满足也不应该划为一个聚合。

不在同一个场景下操作的对象,放入同一个聚合意味着每次操作一个对象,就需要把其他对象的所有信息抓取到,这是非常没有意义的。这在日常开发中我也是深有体会。

从实现层次,如果不紧密相关的对象出现在同一个聚合中,会导致它们经常在不同的场景中被并发修改,也增加了这些对象之间冲突的可能性。

所以:大多数时候的操作场景都不一致的对象,应该把它们分到不同的聚合中

3.4 聚合应尽可能地小

在划分聚合时,除了应该满足上面三个指导原则外,我们还应该让我们的聚合尽可能地小。

通常,较小的聚合会让一个系统变得更快和更可靠,因为会传输较小的数据并且引发并发冲突的概率会较小。而设计一个大的聚合会带来各种问题:

  • 大聚合会降低性能
    聚合中的每个成员会增加数据的量,当这些数据需要从数据库中进行加载的时候,大聚合会增加额外的查询,导致性能降低

  • 大聚合更容易受到并发冲突的影响
    大聚合可能包含了很多职责,这意味着它要参与多个业务用例。随之而来的就是,有很大可能出现多个用户对单个聚合进行变更的情况,从而导致了严重的并发冲突,影响了程序的可用性和用户体验

  • 大聚合扩展性差
    大聚合意味着与更多的模型产生依赖关系,这会导致重构和扩展的难度增加。

原文链接

https://blog.csdn.net/u012179540/article/details/115152804

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/330035.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker 打包镜像_Spring Boot2 系列教程(四十一)部署 Spring Boot 到远程 Docker 容器

不知道各位小伙伴在生产环境都是怎么部署 Spring Boot 的&#xff0c;打成 jar 直接一键运行&#xff1f;打成 war 扔到 Tomcat 容器中运行&#xff1f;不过据松哥了解&#xff0c;容器化部署应该是目前的主流方案。不同于传统的单体应用&#xff0c;微服务由于服务数量众多&am…

MySQL日志:binlog、事务日志(redo、undo)

事务的隔离性是通过锁实现&#xff0c;而事务的原子性、一致性和持久性则是通过日志实现。Mysql的日志可以分为&#xff1a; binlog&#xff1a;server层实现事务日志&#xff1a;包括redo log、undo log&#xff0c;引擎层&#xff08;innodb&#xff09;实现 redo log red…

vmware安装centos8步骤

【readme】 vmware 安装centos8&#xff1b; 【1】新建虚拟机 step1&#xff09; 下载 centos8 http://download.nus.edu.sg/mirror/centos/8-stream/isos/x86_64/ 补充&#xff0c;通过代理服务器下载会快很多&#xff1b; step2&#xff09;vmare&#xff0c;点击文件&…

并发编程 – Concurrent 用户指南

转载自 并发编程 – Concurrent 用户指南1. java.util.concurrent – Java 并发工具包Java 5 添加了一个新的包到 Java 平台&#xff0c;java.util.concurrent 包。这个包包含有一系列能够让 Java 的并发编程变得更加简单轻松的类。在这个包被添加以前&#xff0c;你需要自己去…

小微企业名录查询系统_欢迎访问辽宁小微企业名录系统

欢迎访问辽宁小微企业名录系统http://xwqy.lngs.gov.cn辽宁小微企业名录系统是小微企业扶持政策的实施公示台、集装箱&#xff0c;通过访问该系统网站&#xff0c;及时全面知晓小微企业复工复产、“个转企”等各类扶持政策。按照《国务院关于扶持小型微型企业健康发展的意见》(…

常用限流算法分析

一、计数器&#xff08;固定窗口&#xff09;算法 计数器算法是使用计数器在周期内累加访问次数&#xff0c;当达到设定的限流值时&#xff0c;触发限流策略。下一个周期开始时&#xff0c;进行清零&#xff0c;重新计数。 此算法在单机还是分布式环境下实现都非常简单&#…

nginx学习小结

nginx 【0】README 本文po处理 nginx的主要内容&#xff0c;包括反向代理&#xff0c;负载均衡&#xff0c;动静分离&#xff0c;高可用集群等&#xff1b; 本文引用链接&#xff1a; vmware安装centos8&#xff0c;refer2 https://blog.csdn.net/PacosonSWJTU/article/detail…

缓存与数据库的一致性:先操作缓存还是先操作数据库?

数据缓存 在我们实际的业务场景中&#xff0c;一定有很多需要做数据缓存的场景&#xff0c;比如售卖商品的页面&#xff0c;包括了许多并发访问量很大的数据&#xff0c;它们可以称作是是“热点”数据&#xff0c;这些数据有一个特点&#xff0c;就是更新频率低&#xff0c;读…

Object.hashCode()与Object.equals()

【README】 本文旨在po出 hashCode &#xff0c; equals的api描述&#xff0c;以加深理解&#xff1b; 本文翻译自 jdk 文档&#xff1b; 【1】Object.hashCode() 1&#xff09;介绍&#xff1a;返回对象的哈希码值。支持此方法是为了有利于哈希表&#xff0c;例如由 java.u…

for in for of区别_(for…in) VS (for…of)

这篇文章应该是在一年多之前读过的&#xff0c;那会看完感觉作者文采不错&#xff0c;就做了收藏&#xff0c;做此分享&#xff0c;希望能帮助到你更好的理解js中的循环~~~以下正文。。。今天可是个好日子&#xff01;你问我为什么&#xff1f;你这都不知道&#xff0c;ChinaJo…

Innodb中的事务隔离级别和锁的关系

前言 我们都知道事务的几种性质&#xff0c;数据库为了维护这些性质&#xff0c;尤其是一致性和隔离性&#xff0c;一般使用加锁这种方式。同时数据库又是个高并发的应用&#xff0c;同一时间会有大量的并发访问&#xff0c;如果加锁过度&#xff0c;会极大的降低并发处理能力…

并发队列-无界非阻塞队列 ConcurrentLinkedQueue 原理探究

转载自 并发队列-无界非阻塞队列 ConcurrentLinkedQueue 原理探究一、 前言 常用的并发队列有阻塞队列和非阻塞队列&#xff0c;前者使用锁实现&#xff0c;后者则使用CAS非阻塞算法实现&#xff0c;使用非阻塞队列一般性能比较好&#xff0c;下面就看看常用的非阻塞Concurrent…

(转)如何保障微服务架构下的数据一致性?

转自&#xff1a; https://cloud.tencent.com/developer/article/1459734 【1】写在前面 随着微服务架构的推广&#xff0c;越来越多的公司采用微服务架构来构建自己的业务平台。就像前边的文章说的&#xff0c;微服务架构为业务开发带来了诸多好处的同时&#xff0c;例如单一…

python中math库_Python的math库、random库实际应用

昨天在说那个列表的时候&#xff0c;我挖了一个坑&#xff0c;不知道你们看出来没有&#xff1f;就是用循环语句写迭代的时候&#xff0c;总是运行不了结果&#xff0c;其实是因为我没有缩进的问题&#xff0c;因为有一个for循环&#xff0c;下面print如果没有对应的缩进&#…

(转)漫画:什么是分布式事务?

转自&#xff1a; https://blog.csdn.net/bjweimengshu/article/details/79607522 假如没有分布式事务 在一系列微服务系统当中&#xff0c;假如不存在分布式事务&#xff0c;会发生什么呢&#xff1f;让我们以互联网中常用的交易业务为例子&#xff1a; 上图中包含了库存和订…

Java 线程池框架核心代码分析

转载自 Java 线程池框架核心代码分析前言 多线程编程中&#xff0c;为每个任务分配一个线程是不现实的&#xff0c;线程创建的开销和资源消耗都是很高的。线程池应运而生&#xff0c;成为我们管理线程的利器。Java 通过Executor接口&#xff0c;提供了一种标准的方法将任务的提…

python渐变色代码_如何在Python中创建颜色渐变?

6 个答案: 答案 0 :(得分&#xff1a;54) 我还没有看到一个简单的答案就是使用colour package。 通过pip安装 pip install colour 如此使用&#xff1a; from colour import Color red Color("red") colors list(red.range_to(Color("green"),10)) # col…

(转)web.xml 中的listener、 filter、servlet 加载顺序及其详解

转&#xff1a; https://www.cnblogs.com/Jeely/p/10762152.html web.xml 中的listener、 filter、servlet 加载顺序及其详解 一、概述 1、启动一个WEB项目的时候&#xff0c;WEB容器会去读取它的配置文件web.xml&#xff0c;读取<listener>和<context-param>两个…

柱状图python_python柱状图一行

编写计算柱状图的python程序有很多种方法。 通过柱状图,我指的是一个计算对象在 iterable 并在字典中输出计数。例如: >>> L abracadabra >>> histogram(L) {a: 5, b: 2, c: 1, d: 1, r: 2} 编写此函数的一种方法是: def histogram(L): d {} for x in L: i…