DDD理论学习系列(10)-- 聚合

1.引言

聚合,最初是UML类图中的概念,表示一种强的关联关系,是一种整体与部分的关系,且部分能够离开整体而独立存在,如车和轮胎。

在DDD中,聚合也可以用来表示整体与部分的关系,但不再强调部分与整体的独立性。聚合是将相关联的领域对象进行显示分组,来表达整体的概念(也可以是单一的领域对象)。比如将表示订单与订单项的领域对象进行组合,来表达领域中订单这个整体概念。

我们知道,领域模型是由一系列反映问题域概念的领域对象(实体和值对像)组成,聚合正是应用在领域对象之上。如果要正确应用聚合,我们首先得理清领域对象间的关联关系。

2. 梳理关联关系

在设计领域模型的初期,我们习惯专注于领域中的实体和值对象,而忽略领域对象之间的关联关系,以至于我们会基于现实业务场景或数据模型来建立关联关系。这样就会引入大量不必要的关联,比如下图:

frameborder="0" scrolling="no" style="border-width: initial; border-style: none; width: 474px; height: 270px;">

然而图中的关联关系都是必要的吗?我想未必。这样的关联关系,加大了实现领域模型的技术难度。

当我们建立对象的关联关系时,思考以下问题:

  1. 这个关联关系的作用时什么?

  2. 谁需要这个关联关系去发挥作用?

而如何简化关联呢?

  1. 基于业务用例而非现实生活建立必要的关联

  2. 减少不必要的关联

  3. 将双向的关联转换为单向关联

如果遵从这个原则,那我们的领域模型将会是这样的:

frameborder="0" scrolling="no" style="border-width: initial; border-style: none; width: 539px; height: 321px;">

领域对象间清晰的关联关系,能够清晰反映领域概念,便于我们设计出比较理想的领域模型。理清了领域对象间的关联关系,我们下面来应用聚合。

3. 应用聚合

领域对象不是孤立存在的,往往几个对象的组合才能表示一个完整的概念,如上文所说的订单和订单项。那如何组合对象呢?也就是我们本文的主题。
聚合是领域对象的显示分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。
这句话涉及到几个概念,我们来拆解一下:

  1. 领域对象的显示分组

  2. 领域行为和不变性

  3. 一致性和事务性边界

其中我们需要澄清下领域不变性

Domain invariants are statements or rules that must always be adhered to.
领域不变性指的是必须遵守的陈述或规则。换句话说,就是领域内我们关注的业务规则。比如,订单必须具有唯一订单编号、订单日期;订单必须冗余商品的基本信息(名称、价格、折扣);订单至少有一个商品,删除商品时,订单项需要一并删除;等等。

前两句话综合来说,就是聚合通过对领域对象的封装来体现领域中的业务规则。
而边界的目的是分离聚合内外,聚合内通过事物来保证强一致性。

总而言之,聚合不仅仅是简单的对象组合,其主要的目的是用来封装业务和保证聚合内领域对象的数据一致性。

一致性和事务性边界,又如何理解呢?
一致性是指数据一致性,事务性指的数据库的ACID原则。
下面我们来着重介绍下。

4.一致性边界

为了确保系统的可用性和可靠性,我们必须保证数据的一致性。

订单支付成功后,订单状态要更新为已支付状态,且现有库存要根据订单中商品实际销售数量进行扣减。

下面我们就以这个案例,来分析说明。

4.1.事务一致性

针对这个用例,传统的做法就是,在一个事务中,去更新订单状态和扣减库存。这样似乎满足了业务场景需求,但是我们不得不考虑另外一个问题——并发冲突。比如,在更新订单的同时,商城来了一批货,要进行库存更新,这个时候就存在潜在的冲突,而问题可能表现为数据库级别的阻塞或更新失败(由于悲观并发),如下图:

frameborder="0" scrolling="no" style="border-width: initial; border-style: none; width: 841px; height: 379px;">

这个并发问题我们该如何解决呢?
首先我们要分析问题的原因,这个用例陈述了具体的业务规则。我们错误的将业务涉及到的所有领域对象都放到了一个事务性边界中去了。其实这个用例涉及到三个子域,销售、商品、库存子域。从领域不变性的角度来看,我们应该维护各自子域内业务规则的不变性,而不是为了业务场景实现一概而论。按照这个思想,我们把订单、商品、库存拆分成三个独立的聚合,如下图所示。

frameborder="0" scrolling="no" style="border-width: initial; border-style: none; width: 841px; height: 442px;">

从图中我们可以看出,每个聚合都有自己的事务一致性边界。也就是说这三个聚合分别在不同的事务中维持自己的不变性,也就是说聚合是用来维护内部事务一致性。那针对以上用例,明显需要跨域多个聚合,我们又该如何保证一致性呢?因为我们不能在一个事务中更新多个聚合,所以我们只能实现最终一致性。

4.2. 最终一致性

最终一致性的实现原理是借助领域事件来完成事务的拆分,如下图所示。

frameborder="0" scrolling="no" style="border-width: initial; border-style: none; width: 841px; height: 257px;">

而针对我们的用例,在更新订单支付状态时,发布一个订单已支付的领域事件,库存聚合订阅处理这个事件,即可完成库存的更新。事务拆分如下图:

frameborder="0" scrolling="no" style="border-width: initial; border-style: none; width: 736px; height: 454px;">

4.3. 特殊情况

凡事没有绝对,在一个聚合中仅修改一个聚合是最佳方法。但有时候,在一个事务中更新多个聚合也是可行的,这需要结合具体场景区别对待。另外还有一点需要澄清,以上使用一致性的目的,主要是针对聚合的修改。在一个事务中加载和创建多个聚合是没有问题的,因为并不会导致并发冲突。

5. 聚合的设计

根据上面的阐述:聚合不仅仅是简单的对象组合,其主要的目的是用来封装业务和保证聚合内领域对象的数据一致性。

那聚合设计时要遵循怎样的原则呢?

  1. 遵循领域不变性

  2. 聚合内实现事务一致性,聚合外实现最终一致性
    一个事物一次仅更新一个聚合。当业务用例要跨域多个聚合时,使用领域事件进行事务拆分,实现最终一致性。

  3. 基于业务用例而非现实生活场景

  4. 避免成为集合或容器
    对聚合的一大误解就是,把聚合当作领域对象的集合或容器。当发现这个征兆时,你要考虑你聚合是否需要改造。

  5. 不仅仅是HAS-A关系
    聚合不是简单的包含关系,要确定包含的领域对象是否为了满足某个行为或不变性。

  6. 不要基于用户界面设计聚合
    聚合不应该根据UI界面的需求进行设计。而应该通过加载多个聚合数据映射到UI展示需要的视图模型中。

  7. 创建具有唯一标识的聚合根
    聚合根作为聚合的网关,通过聚合根完成聚合中领域对象的持久化和检索。

  8. 优先使用值对象
    聚合根内的其他领域对象优先设计成值对象

  9. 使用ID关联,而非对象引用
    对象引用不仅会导致聚合边界的模糊,而且会导致延迟加载的问题。

  10. 通过唯一标识引用其他聚合
    聚合边界之外的对象不能持有聚合内部对象的引用;聚合内部的领域对象可以持有其他聚合根的引用。

  11. 避免在聚合内使用依赖注入
    对于依赖的对象,我们应该在调用聚合方法之前查找获取并通过参数传递。可以在应用服务中通过依赖注入资源库或领域服务获取聚合依赖的对象,然后传入聚合。

  12. 使用小聚合
    通常,较小的聚合使系统更快且更可靠,因为更少的数据传输以及更少的并发冲突。
    大聚合会影响性能:聚合的每一个成员都增加了从数据库加载和保存到数据库的数据量,直接影响到性能。
    大聚合容易导致并发冲突:大的聚合可能有多个职责,意味着它涉及到多个业务用例。我们可以量化一个聚合涉及到的业务用例数,数量越大,设计的聚合边界越应该被质疑,尝试将其细化拆解成小聚合。
    大聚合扩展性差:聚合的设计要关注可扩展性。大聚合可能会跨越多个数据库表或文档,这就在数据库级别形成了耦合,它将阻碍你对数据子集进行数据迁移。同时,在业务改变时,大聚合不能很好的适应变化。

6.最后

聚合是一个复杂的概念,其正确应用的关键是领域对象间关联关系的把握和领域不变性的理解。其实现的难点在于一致性的维护上:聚合内实现事务一致性,聚合外实现最终一致性。聚合的设计是一个持续性的活动,不可能在初始阶段就能设计出完美的聚合,我们应该根据对领域知识的深入和经验的积累持续改进聚合的设计。

相关文章

  • DDD理论学习系列(1)-- 通用语言

  • DDD领域驱动之干货 (一)

  • DDD理论学习系列(2)-- 领域

  • DDD理论学习系列(3)-- 限界上下文

  • DDD理论学习系列(4)-- 领域模型

  • 事件总线知多少(2)

  • DDD理论学习系列(5)-- 统一建模语言

  • DDD理论学习系列(6)-- 实体

  • DDD理论学习系列(7)-- 值对象

  • DDD理论学习系列(8)-- 应用服务&领域服务

  • DDD理论学习系列(9)-- 领域事件

  • 从事件和DDD入手来构建微服务

  • DDD领域驱动之干货 (一)

  • WeText项目:一个基于.NET实现的DDD、CQRS与微服务架构的演示案例

  • 【DDD/CQRS/微服务架构案例】在Ubuntu 14.04.4 LTS中运行WeText项目的服务端

  • 基于.NET CORE微服务框架 -surging的介绍和简单示例 (开源)

  • 剥析surging的架构思想

  • 基于.NET CORE微服务框架 -谈谈surging的服务容错降级

  • 我眼中的ASP.NET Core之微服务

  • .NET Core 事件总线,分布式事务解决方案:CAP

原文地址:http://www.cnblogs.com/sheng-jie/p/7158555.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

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

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

相关文章

#面试!,一定要注意,避免踩这些雷!!

大家好,我是雄雄。前言今天,对2班的同学们进行了模拟面试,由于学生们第一次参与模拟面试,所以会出现各种各样的问题,有应该出的,也有不该出的,现在做个简单的总结,供三班的孩子们参考…

一次动态代理的填坑之旅

转载自 一次动态代理的填坑之旅 背景 想在现有的接口加上熔断降级或者限流的功能,比较好的方式是通过注解的方式,并基于动态代理进行实现,下面代码是Rhino的实现 Rhino public class ServiceImpl {Degrade(rhinoKey "syncMethod-0&…

3班的第一次模拟面试

大家好,我是雄雄,前几天总结了下面试别的班时所出现的问题,今天,3班进行了第一次模拟面试。虽然在面试之前千叮咛万嘱咐,但是在正式模拟面试时还是有些问题。一方面由于这样的模拟面试以前没有过,所以紧张过…

漫画:如何实现大整数相加

转载自 漫画:如何实现大整数相加 在程序中列出的 “竖式” 究竟是什么样子呢?我们以 426709752318 95481253129 为例,来看看大整数相加的详细步骤: 第一步,把整数倒序存储,整数的个位存于数组0下标位置&…

ssl2644-线段树练习1【线段树】

正题 题意 一块长m的墙&#xff0c;有n个大小不同的盒子放在前面&#xff0c;求没有被挡住的墙的总长度 解题思路 用线段树&#xff0c;0表示有没被遮挡的&#xff0c;1表示完全被遮挡&#xff0c;-1表示有遮挡的和没遮挡的。然后记数。 代码 #include<cstdio> usin…

Entity Framework Core 执行SQL语句和存储过程

无论ORM有多么强大&#xff0c;总会出现一些特殊的情况&#xff0c;它无法满足我们的要求。在这篇文章中&#xff0c;我们介绍几种执行SQL的方法。 表结构 在具体内容开始之前&#xff0c;我们先简单说明一下要使用的表结构。 public class Category{ public …

我去,终于解决了!

大家好&#xff0c;我是雄雄。今天给大家带来的是【IntelliJ IDEA中配置SSM框架总是报错&#xff0c;启动不了Tomcat的解决方法】。前言以前不管是在开发还是在授课&#xff0c;使用的软件要么是eclipse&#xff0c;要么就是myeclipse&#xff0c;最近听闻身边的人都在用idea,并…

EF Core 插件 —— ToSql

背景 在使用Entity Framework Core进行开发时&#xff0c;若不使用Logger很难查看到一个查询的SQL语句&#xff0c;同时也有些开发者因为不了解EF Core哪些Linq可以Translate成SQL&#xff0c;哪些不能而踩坑导致全表查询的&#xff0c;因此开发了Pomelo.EntityFrameworkCore.…

如何在idea中使用jstl标签库

大家好&#xff0c;我是雄雄&#xff0c;上期我们解决了个bug&#xff0c;详情点击这里&#xff1a;我去&#xff0c;终于解决了 &#xff0c;今天&#xff0c;我们来看看【如何在idea中使用jstl标签库】前言&#xff1a;像myeclipse和eclipse这种编辑器&#xff0c;jstl标签库…

ssl2645-线段树练习2【线段树】

正题 题意 一块长m的墙&#xff0c;有n个大小不同的盒子放在前面&#xff0c;求可以看到多少盒子 解题思路 用线段树&#xff0c;用cover表示可以看到的颜色&#xff0c;-1表示可以看到多种颜色&#xff0c;然后统计&#xff0c;用find数组去重。 代码 #include<cstdio…

Redis单线程架构

转载自 Redis单线程架构 redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务。 引出单线程模型 开启三个redis-cli客户端同时执行命令&#xff1a; 1、客户端1设置一个字符串键值对&#xff1a; 2、客户端2对counter自增操作&#xff1a; 3、客户端3对c…

idea中报错……的解决方式!

大家好&#xff0c;我是雄雄&#xff0c;在用idea的时候&#xff0c;又报错了&#xff0c;哈哈哈。前言还以为所有的问题都解决的差不多了&#xff0c;于是就找了个jsp的案例用idea来熟练熟练&#xff0c;刚开始的时候顺风顺水&#xff0c;按照之前的方法搭建项目&#xff0c;没…

.NET Core快速入门教程 5、使用VS Code进行C#代码调试的技巧

.NET Core 快速入门教程 .NET Core 快速学习、入门系列教程。这个入门系列教程主要跟大家聊聊.NET Core的前世今生&#xff0c;以及Windows、Linux&#xff08;CentOS、Ubuntu&#xff09;基础开发环境的搭建、第一个.NET Core App&#xff0c;以及开发工具的使用、调试技巧&a…

ssl2646-线段树练习3【线段树】

正题 题意 一条长m线&#xff0c;有n条长度不同的线段&#xff0c;求该线被分割成多少段 解题思路 标记颜色&#xff0c;然后统计是用一个color表示上次搜到的(因为线段树的查找顺序是从左到右的)去重 代码 #include<cstdio> #include<cstring> using namespa…

Unicode与UTF-8的区别

转载自 Unicode与UTF-8的区别 要弄清Unicode与UTF-8的关系&#xff0c;我们还得从他们的来源说起&#xff0c;下来我们从刚开始的编码说起&#xff0c;直到Unicode的出现&#xff0c;我们就会感觉到他们之间的关系 ASCII码 我们都知道&#xff0c;在计算机的世界里&#x…

实体类?Dao接口?Mapper映射文件?都别写了!!!用这种方法就可以

大家好&#xff0c;我是雄雄&#xff0c;前两天一直在解决使用idea整合SSM的报错问题&#xff0c;今天&#xff0c;给大家带来的是如何使用插件快速生成实体类、接口以及映射文件&#xff0c;相信你看过本文之后对你有很大的帮助。前言每次我们在写代码的时候&#xff0c;都会对…

DDD理论学习系列(11)-- 工厂

1.引言 在针对大型的复杂领域进行建模时&#xff0c;聚合、实体和值对象之间的依赖关系可能会变得十分复杂。在某个对象中为了确保其依赖对象的有效实例被创建&#xff0c;需要深入了解对象实例化逻辑&#xff0c;我们可能需要加载其他相关对象&#xff0c;且可能为了保持其他…