DDD理论学习系列(10)-- 聚合
1.引言
聚合,最初是UML类图中的概念,表示一种强的关联关系,是一种整体与部分的关系,且部分能够离开整体而独立存在,如车和轮胎。
在DDD中,聚合也可以用来表示整体与部分的关系,但不再强调部分与整体的独立性。聚合是将相关联的领域对象进行显示分组,来表达整体的概念(也可以是单一的领域对象)。比如将表示订单与订单项的领域对象进行组合,来表达领域中订单这个整体概念。
我们知道,领域模型是由一系列反映问题域概念的领域对象(实体和值对像)组成,聚合正是应用在领域对象之上。如果要正确应用聚合,我们首先得理清领域对象间的关联关系。
2. 梳理关联关系
在设计领域模型的初期,我们习惯专注于领域中的实体和值对象,而忽略领域对象之间的关联关系,以至于我们会基于现实业务场景或数据模型来建立关联关系。这样就会引入大量不必要的关联,比如下图:
然而图中的关联关系都是必要的吗?我想未必。这样的关联关系,加大了实现领域模型的技术难度。
当我们建立对象的关联关系时,思考以下问题:
- 这个关联关系的作用时什么?
- 谁需要这个关联关系去发挥作用?
而如何简化关联呢?
- 基于业务用例而非现实生活建立必要的关联
- 减少不必要的关联
- 将双向的关联转换为单向关联
如果遵从这个原则,那我们的领域模型将会是这样的:
领域对象间清晰的关联关系,能够清晰反映领域概念,便于我们设计出比较理想的领域模型。理清了领域对象间的关联关系,我们下面来应用聚合。
3. 应用聚合
领域对象不是孤立存在的,往往几个对象的组合才能表示一个完整的概念,如上文所说的订单和订单项。那如何组合对象呢?也就是我们本文的主题。
聚合是领域对象的显示分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。
这句话涉及到几个概念,我们来拆解一下:
- 领域对象的显示分组
- 领域行为和不变性
- 一致性和事务性边界
其中我们需要澄清下领域不变性:
Domain invariants are statements or rules that must always be adhered to.
领域不变性指的是必须遵守的陈述或规则。换句话说,就是领域内我们关注的业务规则。比如,订单必须具有唯一订单编号、订单日期;订单必须冗余商品的基本信息(名称、价格、折扣);订单至少有一个商品,删除商品时,订单项需要一并删除;等等。
前两句话综合来说,就是聚合通过对领域对象的封装来体现领域中的业务规则。
而边界的目的是分离聚合内外,聚合内通过事物来保证强一致性。
总而言之,聚合不仅仅是简单的对象组合,其主要的目的是用来封装业务和保证聚合内领域对象的数据一致性。
一致性和事务性边界,又如何理解呢?
一致性是指数据一致性,事务性指的数据库的ACID原则。
下面我们来着重介绍下。
4.一致性边界
为了确保系统的可用性和可靠性,我们必须保证数据的一致性。
订单支付成功后,订单状态要更新为已支付状态,且现有库存要根据订单中商品实际销售数量进行扣减。
下面我们就以这个案例,来分析说明。
4.1.事务一致性
针对这个用例,传统的做法就是,在一个事务中,去更新订单状态和扣减库存。这样似乎满足了业务场景需求,但是我们不得不考虑另外一个问题——并发冲突。比如,在更新订单的同时,商城来了一批货,要进行库存更新,这个时候就存在潜在的冲突,而问题可能表现为数据库级别的阻塞或更新失败(由于悲观并发),如下图:
这个并发问题我们该如何解决呢?
首先我们要分析问题的原因,这个用例陈述了具体的业务规则。我们错误的将业务涉及到的所有领域对象都放到了一个事务性边界中去了。其实这个用例涉及到三个子域,销售、商品、库存子域。从领域不变性的角度来看,我们应该维护各自子域内业务规则的不变性,而不是为了业务场景实现一概而论。按照这个思想,我们把订单、商品、库存拆分成三个独立的聚合,如下图所示。
从图中我们可以看出,每个聚合都有自己的事务一致性边界。也就是说这三个聚合分别在不同的事务中维持自己的不变性,也就是说聚合是用来维护内部事务一致性。那针对以上用例,明显需要跨域多个聚合,我们又该如何保证一致性呢?因为我们不能在一个事务中更新多个聚合,所以我们只能实现最终一致性。
4.2. 最终一致性
最终一致性的实现原理是借助领域事件来完成事务的拆分,如下图所示。
而针对我们的用例,在更新订单支付状态时,发布一个订单已支付的领域事件,库存聚合订阅处理这个事件,即可完成库存的更新。事务拆分如下图:
4.3. 特殊情况
凡事没有绝对,在一个聚合中仅修改一个聚合是最佳方法。但有时候,在一个事务中更新多个聚合也是可行的,这需要结合具体场景区别对待。另外还有一点需要澄清,以上使用一致性的目的,主要是针对聚合的修改。在一个事务中加载和创建多个聚合是没有问题的,因为并不会导致并发冲突。
5. 聚合的设计
根据上面的阐述:聚合不仅仅是简单的对象组合,其主要的目的是用来封装业务和保证聚合内领域对象的数据一致性。
那聚合设计时要遵循怎样的原则呢?
- 遵循领域不变性
- 聚合内实现事务一致性,聚合外实现最终一致性
一个事物一次仅更新一个聚合。当业务用例要跨域多个聚合时,使用领域事件进行事务拆分,实现最终一致性。 - 基于业务用例而非现实生活场景
- 避免成为集合或容器
对聚合的一大误解就是,把聚合当作领域对象的集合或容器。当发现这个征兆时,你要考虑你聚合是否需要改造。 - 不仅仅是HAS-A关系
聚合不是简单的包含关系,要确定包含的领域对象是否为了满足某个行为或不变性。 - 不要基于用户界面设计聚合
聚合不应该根据UI界面的需求进行设计。而应该通过加载多个聚合数据映射到UI展示需要的视图模型中。 - 创建具有唯一标识的聚合根
聚合根作为聚合的网关,通过聚合根完成聚合中领域对象的持久化和检索。 - 优先使用值对象
聚合根内的其他领域对象优先设计成值对象 - 使用ID关联,而非对象引用
对象引用不仅会导致聚合边界的模糊,而且会导致延迟加载的问题。 - 通过唯一标识引用其他聚合
聚合边界之外的对象不能持有聚合内部对象的引用;聚合内部的领域对象可以持有其他聚合根的引用。 - 避免在聚合内使用依赖注入
对于依赖的对象,我们应该在调用聚合方法之前查找获取并通过参数传递。可以在应用服务中通过依赖注入资源库或领域服务获取聚合依赖的对象,然后传入聚合。 - 使用小聚合
通常,较小的聚合使系统更快且更可靠,因为更少的数据传输以及更少的并发冲突。
大聚合会影响性能:聚合的每一个成员都增加了从数据库加载和保存到数据库的数据量,直接影响到性能。
大聚合容易导致并发冲突:大的聚合可能有多个职责,意味着它涉及到多个业务用例。我们可以量化一个聚合涉及到的业务用例数,数量越大,设计的聚合边界越应该被质疑,尝试将其细化拆解成小聚合。
大聚合扩展性差:聚合的设计要关注可扩展性。大聚合可能会跨越多个数据库表或文档,这就在数据库级别形成了耦合,它将阻碍你对数据子集进行数据迁移。同时,在业务改变时,大聚合不能很好的适应变化。
6.最后
聚合是一个复杂的概念,其正确应用的关键是领域对象间关联关系的把握和领域不变性的理解。其实现的难点在于一致性的维护上:聚合内实现事务一致性,聚合外实现最终一致性。聚合的设计是一个持续性的活动,不可能在初始阶段就能设计出完美的聚合,我们应该根据对领域知识的深入和经验的积累持续改进聚合的设计。
DDD理论学习系列(10)-- 聚合的更多相关文章
- DDD理论学习系列(9)-- 领域事件
DDD理论学习系列--案例及目录 1. 引言 A domain event is a full-fledged part of the domain model, a representation o ...
- DDD理论学习系列——案例及目录
目录 DDD理论学习系列(1)-- 通用语言 DDD理论学习系列(2)-- 领域 DDD理论学习系列(3)-- 限界上下文 DDD理论学习系列(4)-- 领域模型 DDD理论学习系列(5)-- 统一建 ...
- DDD理论学习系列(7)-- 值对象
DDD理论学习系列--案例及目录 1.引言 提到值对象,我们可能立马就想到值类型和引用类型.而在C#中,值类型的代表是strut和enum,引用类型的代表是class.interface.delega ...
- DDD理论学习系列(11)-- 工厂
DDD理论学习系列--案例及目录 1.引言 在针对大型的复杂领域进行建模时,聚合.实体和值对象之间的依赖关系可能会变得十分复杂.在某个对象中为了确保其依赖对象的有效实例被创建,需要深入了解对象实例化逻 ...
- DDD理论学习系列(12)-- 仓储
DDD理论学习系列--案例及目录 1. 引言 DDD中Repository这个单词,主要有两种翻译:资源库和仓储,本文取仓储之译. 说到仓储,我们肯定就想到了仓库,仓库一般用来存放货物,而仓库一般由仓 ...
- DDD理论学习系列(13)-- 模块
DDD理论学习系列--案例及目录 1. 引言 Module,即模块,是指提供特定功能的相对独立的单元.提到模块,你肯定就会想到模块化设计思想,也就是功能的分解和组合.对于简单问题,可以直接构建单一模块 ...
- DDD理论学习系列(2)-- 领域
DDD理论学习系列目录 1. 引言 领域一词,主要有以下两个意思: 一国主权所达之地. 学术思想或社会活动的范围. 不管是指国家的主权范围也好还是学术活动范围,都是在讲一个范围,一个界限. 比如我们常 ...
- DDD理论学习系列(4)-- 领域模型
DDD理论学习系列目录 1.引言 我们还是先来拆词理解,领域模型可以拆为"领域"和"模型"二词. 领域:按照我们之前的文章的理解,DDD中的领域是指软件系统要解 ...
- DDD理论学习系列(5)-- 统一建模语言
DDD理论学习系列--案例及目录 1.引言 上一节讲解了领域模型,领域模型主要是将业务中涉及到的概念以面向对象的思想进行抽象,抽象出实体对象,确定实体所对应的方法和属性,以及实体之间的关系.然后将这些 ...
随机推荐
- python连接sql server数据库实现增删改查
简述 python连接微软的sql server数据库用的第三方模块叫做pymssql(document:http://www.pymssql.org/en/stable/index.html).在官 ...
- R绘图字体解决方案(转)
COS论坛里面经常会遇到的一个问题就是绘图时中文字体怎么解决.最初,一个流行的方法是使用family = "GB1",但一般这样做出来的图比较难看,而且并没有完全解决问题.后来发现 ...
- 使用sqlserver搭建高可用双机热备的Quartz集群部署【附源码】
一般拿Timer和Quartz相比较的,简直就是对Quartz的侮辱,两者的功能根本就不在一个层级上,如本篇介绍的Quartz强大的序列化机制,可以序列到 sqlserver,mysql,当然还可以在 ...
- js实现谷歌坐标转百度坐标
js实现谷歌坐标转百度坐标 谷歌坐标转百度坐标 实现算法如下(以js为例,其他语言调整就行): //$lat 维度:$lng 经度 function GCJTobaidu($lat, $lng){ ...
- Java IO流之对象流
对象流 1.1对象流简介 1.2对象流分类 输入流字节流处理流:ObjectInputStream,将序列化以后的字节存储到本地文件 输出流字节流处理流:ObjectOutputStream 1.3序 ...
- 基于FPGA的彩色图像转灰度算法实现
昨天才更新了两篇博客,今天又要更新了,并不是我垃圾产,只不过这些在上个月就已经写好了,只是因为比赛忙,一直腾不出时间整理出来发表而已,但是做完一件事情总感觉不写一博文总结一下就少点什么,所以之后的一段 ...
- python3实现TCP协议的简单服务器和客户端
利用python3来实现TCP协议,和UDP类似.UDP应用于及时通信,而TCP协议用来传送文件.命令等操作,因为这些数据不允许丢失,否则会造成文件错误或命令混乱.下面代码就是模拟客户端通过命令行操作 ...
- 快来领取一场专门讲解UTF-8与UTF-16编码算法的GitChat活动的免费名额
微信扫一扫,可打开该GitChat活动页面 字符编码是计算机世界里最基础.最重要.最令人困惑的一个主题之一.不过,在计算机教材中却往往浮光掠影般地草草带过,甚至连一本专门进行深入介绍的专著都找不到(对 ...
- 【javascript】Promise/A+ 规范简单实现 异步流程控制思想
——基于es6:Promise/A+ 规范简单实现 异步流程控制思想 前言: nodejs强大的异步处理能力使得它在服务器端大放异彩,基于它的应用不断的增加,但是异步随之带来的嵌套.难以理解的代码让 ...
- 每天一个JS 小demo之商品筛选。主要知识点:DOM方法综合运用
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"& ...