learn.lianglianglee.com/专栏/领域驱动设计实践(完)/068 聚合设计原则.md.html
2022-05-11 19:04:14 +08:00

1145 lines
50 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>068 聚合设计原则.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/领域驱动设计实践(完)/001 「战略篇」访谈 DDD 和微服务是什么关系?.md.html">001 「战略篇」访谈 DDD 和微服务是什么关系?.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/002 「战略篇」开篇词:领域驱动设计,重焕青春的设计经典.md.html">002 「战略篇」开篇词:领域驱动设计,重焕青春的设计经典.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/003 领域驱动设计概览.md.html">003 领域驱动设计概览.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/004 深入分析软件的复杂度.md.html">004 深入分析软件的复杂度.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/005 控制软件复杂度的原则.md.html">005 控制软件复杂度的原则.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/006 领域驱动设计对软件复杂度的应对(上).md.html">006 领域驱动设计对软件复杂度的应对(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/007 领域驱动设计对软件复杂度的应对(下).md.html">007 领域驱动设计对软件复杂度的应对(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/008 软件开发团队的沟通与协作.md.html">008 软件开发团队的沟通与协作.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/009 运用领域场景分析提炼领域知识(上).md.html">009 运用领域场景分析提炼领域知识(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/010 运用领域场景分析提炼领域知识(下).md.html">010 运用领域场景分析提炼领域知识(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/011 建立统一语言.md.html">011 建立统一语言.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/012 理解限界上下文.md.html">012 理解限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/013 限界上下文的控制力(上).md.html">013 限界上下文的控制力(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/014 限界上下文的控制力(下).md.html">014 限界上下文的控制力(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/015 识别限界上下文(上).md.html">015 识别限界上下文(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/016 识别限界上下文(下).md.html">016 识别限界上下文(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/017 理解上下文映射.md.html">017 理解上下文映射.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/018 上下文映射的团队协作模式.md.html">018 上下文映射的团队协作模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/019 上下文映射的通信集成模式.md.html">019 上下文映射的通信集成模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/020 辨别限界上下文的协作关系(上).md.html">020 辨别限界上下文的协作关系(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/021 辨别限界上下文的协作关系(下).md.html">021 辨别限界上下文的协作关系(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/022 认识分层架构.md.html">022 认识分层架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/023 分层架构的演化.md.html">023 分层架构的演化.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/024 领域驱动架构的演进.md.html">024 领域驱动架构的演进.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/025 案例 层次的职责与协作关系(图文篇).md.html">025 案例 层次的职责与协作关系(图文篇).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/026 限界上下文与架构.md.html">026 限界上下文与架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/027 限界上下文对架构的影响.md.html">027 限界上下文对架构的影响.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/028 领域驱动设计的代码模型.md.html">028 领域驱动设计的代码模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/029 代码模型的架构决策.md.html">029 代码模型的架构决策.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/030 实践 先启阶段的需求分析.md.html">030 实践 先启阶段的需求分析.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/031 实践 先启阶段的领域场景分析(上).md.html">031 实践 先启阶段的领域场景分析(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/032 实践 先启阶段的领域场景分析(下).md.html">032 实践 先启阶段的领域场景分析(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/033 实践 识别限界上下文.md.html">033 实践 识别限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/034 实践 确定限界上下文的协作关系.md.html">034 实践 确定限界上下文的协作关系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/035 实践 EAS 的整体架构.md.html">035 实践 EAS 的整体架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/036 「战术篇」访谈DDD 能帮开发团队提高设计水平吗?.md.html">036 「战术篇」访谈DDD 能帮开发团队提高设计水平吗?.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/037 「战术篇」开篇词:领域驱动设计的不确定性.md.html">037 「战术篇」开篇词:领域驱动设计的不确定性.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/038 什么是模型.md.html">038 什么是模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/039 数据分析模型.md.html">039 数据分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/040 数据设计模型.md.html">040 数据设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/041 数据模型与对象模型.md.html">041 数据模型与对象模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/042 数据实现模型.md.html">042 数据实现模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/043 案例 培训管理系统.md.html">043 案例 培训管理系统.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/044 服务资源模型.md.html">044 服务资源模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/045 服务行为模型.md.html">045 服务行为模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/046 服务设计模型.md.html">046 服务设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/047 领域模型驱动设计.md.html">047 领域模型驱动设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/048 领域实现模型.md.html">048 领域实现模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/049 理解领域模型.md.html">049 理解领域模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/050 领域模型与结构范式.md.html">050 领域模型与结构范式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/051 领域模型与对象范式(上).md.html">051 领域模型与对象范式(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/052 领域模型与对象范式(中).md.html">052 领域模型与对象范式(中).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/053 领域模型与对象范式(下).md.html">053 领域模型与对象范式(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/054 领域模型与函数范式.md.html">054 领域模型与函数范式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/055 领域驱动分层架构与对象模型.md.html">055 领域驱动分层架构与对象模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/056 统一语言与领域分析模型.md.html">056 统一语言与领域分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/057 精炼领域分析模型.md.html">057 精炼领域分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/058 彩色 UML 与彩色建模.md.html">058 彩色 UML 与彩色建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/059 四色建模法.md.html">059 四色建模法.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/060 案例 订单核心流程的四色建模.md.html">060 案例 订单核心流程的四色建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/061 事件风暴与业务全景探索.md.html">061 事件风暴与业务全景探索.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/062 事件风暴与领域分析建模.md.html">062 事件风暴与领域分析建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/063 案例 订单核心流程的事件风暴.md.html">063 案例 订单核心流程的事件风暴.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/064 表达领域设计模型.md.html">064 表达领域设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/065 实体.md.html">065 实体.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/066 值对象.md.html">066 值对象.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/067 对象图与聚合.md.html">067 对象图与聚合.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/领域驱动设计实践(完)/068 聚合设计原则.md.html">068 聚合设计原则.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/069 聚合之间的关系.md.html">069 聚合之间的关系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/070 聚合的设计过程.md.html">070 聚合的设计过程.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/071 案例 培训领域模型的聚合设计.md.html">071 案例 培训领域模型的聚合设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/072 领域模型对象的生命周期-工厂.md.html">072 领域模型对象的生命周期-工厂.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/073 领域模型对象的生命周期-资源库.md.html">073 领域模型对象的生命周期-资源库.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/074 领域服务.md.html">074 领域服务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/075 案例 领域设计模型的价值.md.html">075 案例 领域设计模型的价值.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/076 应用服务.md.html">076 应用服务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/077 场景的设计驱动力.md.html">077 场景的设计驱动力.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/078 案例 薪资管理系统的场景驱动设计.md.html">078 案例 薪资管理系统的场景驱动设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/079 场景驱动设计与 DCI 模式.md.html">079 场景驱动设计与 DCI 模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/080 领域事件.md.html">080 领域事件.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/081 发布者—订阅者模式.md.html">081 发布者—订阅者模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/082 事件溯源模式.md.html">082 事件溯源模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/083 测试优先的领域实现建模.md.html">083 测试优先的领域实现建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/084 深入理解简单设计.md.html">084 深入理解简单设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/085 案例 薪资管理系统的测试驱动开发(上).md.html">085 案例 薪资管理系统的测试驱动开发(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/086 案例 薪资管理系统的测试驱动开发(下).md.html">086 案例 薪资管理系统的测试驱动开发(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/087 对象关系映射(上).md.html">087 对象关系映射(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/088 对象关系映射(下).md.html">088 对象关系映射(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/089 领域模型与数据模型.md.html">089 领域模型与数据模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/090 领域驱动设计对持久化的影响.md.html">090 领域驱动设计对持久化的影响.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/091 领域驱动设计体系.md.html">091 领域驱动设计体系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/092 子领域与限界上下文.md.html">092 子领域与限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/093 限界上下文的边界与协作.md.html">093 限界上下文的边界与协作.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/094 限界上下文之间的分布式通信.md.html">094 限界上下文之间的分布式通信.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/095 命令查询职责分离.md.html">095 命令查询职责分离.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/096 分布式柔性事务.md.html">096 分布式柔性事务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/097 设计概念的统一语言.md.html">097 设计概念的统一语言.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/098 模型对象.md.html">098 模型对象.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/099 领域驱动设计参考过程模型.md.html">099 领域驱动设计参考过程模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/100 领域驱动设计的精髓.md.html">100 领域驱动设计的精髓.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/101 实践 员工上下文的领域建模.md.html">101 实践 员工上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/102 实践 考勤上下文的领域建模.md.html">102 实践 考勤上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/103 实践 项目上下文的领域建模.md.html">103 实践 项目上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/104 实践 培训上下文的业务需求.md.html">104 实践 培训上下文的业务需求.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/105 实践 培训上下文的领域分析建模.md.html">105 实践 培训上下文的领域分析建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/106 实践 培训上下文的领域设计建模.md.html">106 实践 培训上下文的领域设计建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/107 实践 培训上下文的领域实现建模.md.html">107 实践 培训上下文的领域实现建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/108 实践 EAS 系统的代码模型.md.html">108 实践 EAS 系统的代码模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/109 后记:如何学习领域驱动设计.md.html">109 后记:如何学习领域驱动设计.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>068 聚合设计原则</h1>
<h3>聚合设计原则</h3>
<p>对比对象图和聚合,我们认为引入聚合的目的是控制对象之间的关系,这实则是引入聚合的技术原因。正如我在第 3-1 课《表达领域设计模型》中所说“领域驱动设计引入聚合Aggregate来划分对象之间的边界在边界内保证所有对象的一致性并在对象协作与独立之间取得平衡。”显然聚合保持了对象图的简单性降低了实现的难度解决了可能的性能问题。</p>
<p>聚合的设计原则要结合聚合的本质特征,每一条本质特征都可以提炼出设计聚合的原则:</p>
<ul>
<li>聚合需要维护领域概念的完整性:这意味着聚合边界内所有对象的生命周期是保持一致的,它们一起创建、一起销毁、一起删除。聚合的生命周期统一由工厂和资源库进行管理。</li>
<li>聚合必须保证领域规则的不变量:不变量是指在数据变化时必须保持的一致性规则,可以视为它是业务规则的约束,无论数据怎么变化,都要维持一个恒定不变的等式。</li>
<li>聚合需要遵循事务的 ACID 原则:聚合在对象图中是不可分割的工作单元,聚合内的数据保持一致,聚合之间相互隔离互不影响,聚合内数据发生的变化需要持久化。</li>
</ul>
<h4>领域概念的完整性</h4>
<p>聚合作为一个受到边界控制的领域共同体,对外由聚合根体现为一个统一的概念,对内则管理和维护着强耦合的对象关系,它们具有一致的生命周期。例如,订单聚合由 Order 聚合根体现订单的领域概念,调用者甚至不需要知道订单项,也不会认为配送地址是一个可以脱离订单而单独存在的领域概念。如果要创建订单,则订单项、配送地址等聚合边界内的对象也需要一并创建,否则这个订单对象就是不完整的。同理,销毁订单对象乃至删除订单对象(倘若设计为可删除),属于订单属性的其他聚合边界对象也需要被销毁乃至删除。如果不能做到这一点,就可能产生垃圾数据。</p>
<p>领域概念的完整性可以与组合关系中的”物理包容“对照理解,即类之间若存在合成关系,则有很大可能放入到同一个聚合边界内。当然,也会有例外场景,这正是软件设计为难之处,因为没有标准答案。进行领域设计建模时,类之间的关系与现实世界中各种对象之间的关系并不一致。我们务必牢记:<strong>设计的决策必须基于当前的业务场景来决定</strong></p>
<p>因此,在考虑领域概念的完整性时,必须结合具体的业务场景。例如,在现实世界中,汽车作为一个领域概念整体,只有组装了发动机、轮胎、方向盘等必备零配件,汽车才是完整的,才能够发动和驾驶。但是,在汽车销售的零售商管理领域中,若为整车销售,则轮胎、方向盘等零配件可以作为 Car 聚合的内部对象,但发动机 Engine 具有自己的唯一身份标识,可能需要独立于汽车被单独跟踪,则 Engine 就可以作为单独的聚合;若为零配件销售,则方向盘、轮胎也具有自己的身份标识而被单独管理和单独跟踪,也需要为其建立单独的聚合。</p>
<p>追求概念的完整性固然重要,但保证概念的独立性同样重要:</p>
<ul>
<li>既然一个概念是独立的,为何还要依附于别的概念呢?——发动机需要独立跟踪,还需要纳入到汽车这个整体概念中吗?</li>
<li>一旦这个独立的领域概念被分离出去,原有的聚合是否还具备领域概念的完整性呢?——例如,离开了发动机的汽车,概念是否完整?</li>
</ul>
<p>在理解概念的完整性时,我们不能以偏概全,将完整性视为“关系的集合”,只要彼此关联,就是完整概念的一部分。毕竟,聚合并非完全独立的存在,聚合之间同样存在协作依赖关系。</p>
<p>Vaughn Vernon 建议“设计小聚合”,这主要是从系统的性能和可伸缩性角度考虑的,因为维护一个庞大的聚合需要考虑事务的同步成本、数据加载的内存成本等。且不说这个所谓的“小”到底该多小,但至少过分的小带来的危害要远远小于不当的大。所谓“两害相权取其轻”,在根据领域概念完整性与独立性划分聚合边界时,可以先保证聚合尽量的小,小到只容下一个实体类。当对象图中每个实体都成为一个独立的聚合时,聚合就失去了存在的价值。这显然不合理。于是,我们需要再一次遍历所有实体,判断它们可否合并到已有聚合中。根据类关系与语义相关性的强弱,我们谋求着把别的实体放进当前选定的最小聚合,就需要寻找合并的理由。我们需要针对聚合内的聚合根实体询问完整性,针对聚合内的非聚合根实体询问独立性:</p>
<ul>
<li>目标聚合是否已经足够完整?</li>
<li>待合并实体是否会被调用者单独使用?</li>
</ul>
<p>考虑在线试题领域中问题与答案的关系。Question 若缺少 Answer 就无法保证领域概念的完整性,调用者也不会绕开 Question 单独查看 Answer因为 Answer 离开 Question 是没有任何意义的。因此Question 与 Answer 属于同一个聚合,且以 Question 实体为聚合根。</p>
<p>同样是问题与答案之间的关系如果为知乎问答平台设计领域模型情况就发生了变化。虽然从领域概念的完整性看Question 与 Answer 依然属于强相关的关系Answer 依附于 Question没有 Question 的 Answer 也没有任何意义,但由于业务场景允许阅读 Answer 的读者可以单独针对它进行赞赏、赞同、评论、分享、收藏等操作,如下图所示:</p>
<p><img src="assets/becd14e0-ccbc-11e9-9f23-07a3e2a236db" alt="62821636.png" /></p>
<p>这些操作就等同于为 Answer 赋予了“完全民事行为能力”,具备了独立性,就可以脱离 Question 聚合成为单独的 Answer 聚合。</p>
<p>与实体相反,领域设计模型中值对象不存在这种独立性。根据聚合的定义,最小的聚合必须至少要有一个实体,这就意味着值对象不能单独成为一个聚合。值对象必须寻找一个聚合,作为它要依存的主体。个别值对象如 Money 等与单位、度量有关的类甚至会在多个聚合中重复出现。</p>
<h4>不变量</h4>
<p>不变量这个词很不好理解。它的英文为 Invariant除了翻译为“不变量”之外还有人将其翻译为“不变条件”或“固定规则”。后两个翻译应属于“意译”想要表达它指代的是领域逻辑中的规则或验证条件。这个含义反转过来就未必成立了。业务规则不一定是不变量例如“招聘计划必须由人力资源总监审批”是一条业务规则但该规则实际上是对角色与权限的规定并非不变量。验证条件也未必是不变量例如“报表类别的名称不可短于 8 个字符,且不允许重复”是验证条件,该验证条件规定了报表类别的 Name 属性值的合法性,也不能算是不变量。</p>
<p>Eric Evans 在《领域驱动设计》一书中将不变量定义为是“在数据变化时必须保持的一致性规则,涉及聚合成员之间的内部关系”。这句话传递了三个重要概念(特征):数据变化、一致、内部关系。如果我们将聚合中的对象视为变化因子,则不变量就是要保持它们之间的关系在数据发生变化时仍然保持一致。实际上,这更像是数学中“不变式(同样为英文的 Invariant”的概念例如等式 3x+y=1003x+y=100无论 x 和 y 怎么变化,都必须恒定地满足这个相等关系。等式中的 x 和 y 可类比为聚合中的对象,该等式则是施加在聚合边界之上的业务约束。这就解释了前述业务规则与验证条件为何不是不变量——因为它们并未牵涉到聚合内部数据的变化,也没有对聚合内对象之间的关系进行约束。参考 Eric Evans 在书中给出的不变量案例:“采购项的总量不能超过 PO 总额的限制”就完全符合不变量的特征。该不变量约束了采购项Line Item与订单Purchase Order之间的关系即无论采购项怎么变化都不允许它的总量超过 PO 总额。该不变量可以描述为如下数学公式:</p>
<pre><code>SUM(Purchase Order Line Item) &lt;= PO Approved Limit
</code></pre>
<p>该不变量决定了 LineItem 与 PurchaseOrder 必须放在一个聚合中,因为只有将它们控制在聚合边界内,才能够有效满足该不变量。</p>
<p>要完全理解何为“不变量”虽有这三大特征作为辨别的依据仍非易事。为了让不变量帮助我们确定聚合的边界可以放宽定义将其视为“施加在聚合边界内部各个对象之上的业务约束”。例如业务约束规定一篇博文Post必须至少有一个博文类别Post Category就可以当做是一个不变量。要满足这个不变量就需要将 Post 与 PostCategory 放到同一个聚合中:</p>
<p><img src="assets/ee896350-ccbc-11e9-8d89-4fa271cb1633" alt="77139148.png" /></p>
<p>在设计聚合时,可以结合领域逻辑去寻找具有不变量特征的业务约束。通常,此类约束表现为用例的前置条件与后置条件,或者用户故事的验收标准。即使不是为了设计聚合,业务分析人员也应当给出业务约束的描述。例如,在航班计划业务场景中,编写“修改航班计划起飞时间与计划到达时间”这一用户故事时,就需要给出验收标准,如:</p>
<ul>
<li>若该航班有共享航班,在修改航班计划起飞时间与计划到达时间时,关联的所有共享航班的计划起飞时间与计划到达时间也要随之修改,以保持与主航班的一致。</li>
</ul>
<p>这一验收标准实则可以视为航班与共享航班之间的不变量,这就要求我们针对这一业务场景,将 Flight 与 SharedFlight 两个实体放在同一个聚合中,且以 Flight 实体为聚合根。</p>
<h4>事务的 ACID</h4>
<p>事务Transaction本身是技术实现层次的解决方案如何实现事务当然是底层框架的事儿但如果领域模型没有设计好对象之间的边界没有得到控制要满足事务的 ACID 特性就会变得困难。这事实上也是在领域设计模型中引入聚合的部分原因。</p>
<p>分析事务的 ACID 特性,我们发现这些特性可以很好地与聚合的特性匹配:</p>
<table>
<thead>
<tr>
<th align="left">特性</th>
<th align="left">事务</th>
<th align="left">聚合</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">原子性Atomicity</td>
<td align="left">事务是一个不可再分割的工作单元</td>
<td align="left">聚合需要保证领域概念的完整性,若有独立的领域类,应分解为专门的聚合,这意味着聚合是不可再分的领域概念</td>
</tr>
<tr>
<td align="left">一致性Consistency</td>
<td align="left">在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏</td>
<td align="left">聚合需要保证聚合边界内的所有对象满足不变量约束,其中最重要的不变量就是一致性约束</td>
</tr>
<tr>
<td align="left">隔离性Isolation</td>
<td align="left">多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果</td>
<td align="left">聚合与聚合之间应该是隔离的,聚合的设计原则要求通过唯一的身份标识进行聚合关联</td>
</tr>
<tr>
<td align="left">持久性Durability</td>
<td align="left">事务对数据库所作的更改持久地保存在数据库之中,不会被回滚</td>
<td align="left">一个聚合只有一个资源库,由资源库保证聚合整体的持久化</td>
</tr>
</tbody>
</table>
<p>先抛开聚合如何满足事务的 ACID 不提单从这些特性之间的一一匹配足以说明辨别事务边界有助于我们设计聚合。Vernon 就认为:“在一个事务中只修改一个聚合实例”。在提交事务时,事务边界之内的所有内容都必须保持一致。换言之,倘若无法满足聚合内的事务需求,则说明我们的聚合边界设计存在疑问。当然,这里提及的事务并不包含所谓的“柔性事务”,满足的事务一致性指的是“强一致性”,而非“最终一致性”。</p>
<p>考虑电商领域订单与订单项的关系。在创建、修改或删除订单时,都必须要求订单与订单项的数据强一致性。以创建订单为例,如果插入 Order 记录成功,插入 OrderItem 出现了失败,就要求对已经创建成功的 Order 记录进行回滚,否则此时的订单就受到了破坏。这也正是将 Order 与 OrderItem 放到同一个聚合中的主要原因。反观博客平台博客Blog与博文Post之间的关系则有所不同。Blog 记录的创建与 Post 记录的创建并非原子操作,它们归属于两个不同的工作单元。虽然业务的前置条件要求在创建 Post 之前,对应的 Blog 必须已经存在,但并没有要求 Post 与 Blog 必须同时创建。修改和删除操作同样如此。因此, Blog 和 Post 应该属于两个完全独立的聚合。</p>
<p>正如维护领域概念的完整性与业务约束的不变量并非设计聚合的绝对标准,事务与聚合之间的对应也存在例外,特别是当完整性与独立性、不变量、事务这三大原则之间存在冲突时,该如何设计聚合,确实是一件让人头疼的事情。</p>
<p>以银行的“取款”用例来说明。当储户账户发起取款操作时需要扣除账户Account的余额Balance同时系统会创建一条新的交易记录Transaction以便于银行对账并支持储户的交易查询功能。显然如果账户余额扣除成功而取款的交易记录却创建失败就会导致二者出现数据不一致的情况。要保持这种一致性事务范围就必须包含 Account、Balance 与 Transaction 这三个类,其中 Account 与 Transaction 都是实体。</p>
<p>按照事务与聚合之间的匹配关系,聚合的边界就应该包括 Account、Balance 与 Transaction 这三个类。Account 和 Balance 存在领域概念完整性要求,且 Balance 并非实体,将它们放在一个聚合中,没有任何争议。但是对于 Transaction 呢?由于储户可以执行交易查询功能,这意味着调用者可以绕开 Account单独查询 Transaction。显然Transaction 具有独立性,应该单独为它建立一个聚合,但这样的设计又无法保证 Account 与 Transaction 之间的事务一致性。</p>
<p>虽说聚合与事务的边界重合,但并不足以说明在聚合之上就不可引入事务的强一致性。从职责上看,聚合对事务 ACID 的满足实则是委派给资源库完成的它才是事务的工作单元Unit of Work。事务是有范围Scope当一个业务用例需要多个聚合共同参与时每个聚合对应的事务同样可以共同协作。在领域驱动设计推荐的分层架构与设计要素中可以定义应用服务作为内外协作的门面并由其引入外部框架来支持满足用例需求的整体事务再由领域服务封装聚合、资源库之间的协作实现真正的业务需求。取款业务的实现如下所示</p>
<pre><code class="language-java">public class AccountAppService {
@Autowired
private WithdrawingService service;
@Transactional
public void withdraw(AccountId id, Amount amount) {
service.execute(id, amount);
}
}
public class WithdrawingService {
@Repository
private AccountRepository accountRepo;
@Repository
private TransactionRepository transRepo;
public void execute(AccountId id, Amount amount) {
Account accout = accountRepo.findBy(id);
account.substract(amount);
accountRepo.save(account);
Transaction trans = Transaction.createFrom(id, amount, TransactionType.Withdraw);
transRepo.save(trans);
}
}
</code></pre>
<p>在满足跨聚合之间的强一致性时,要判断参与协作的多个聚合是否在同一个进程边界。引入分布式事务来满足这种强一致性往往得不偿失,非万不得已,应尽量避免。即使不考虑分布式事务的成本,纵然多个聚合都在一个进程边界内,仍然需要慎重思考所谓的“强一致性”是否就是必然?例如,是否可以考虑引入最终一致性。在确定一致性的强弱时,需要与领域专家沟通,尝试从用户角度思考聚合实例的变更是否允许一定时间的延迟。仍以“取款”场景为例,只要保证交易数据最终一定能记录下来,同时让账户余额的变更保持实时性,无论是储户还是银行的管理层,都是可以接受最终一致性的。</p>
<p>最终一致性很好地协调了聚合与事务的一致性边界。Vaughn Vernon 就建议“在一致性边界之外使用最终一致性方式”。在微服务架构下,实现事务的最终一致性更是常态。微服务的边界可能与限界上下文的边界重合,而在一个限界上下文中,可能包含一到多个聚合。因此,在实现跨聚合的事务一致性时,还需要判断参与业务场景的多个聚合到底是在一个进程边界内,还是需要跨进程通信。前者可以考虑在应用服务中引入事务来保障数据的强一致性,后者可以考虑引入 Saga 模式实现数据的最终一致性。至于如何实现事务的一致性,我会在后面的章节进一步探讨。</p>
<p>综上,我们可以从领域概念的完整性与独立性、不变量和事务等多个角度审视聚合的边界,帮助我们高质量地设计聚合。在这些设计原则中,我们需格外重视概念的独立性,它直接影响了聚合的边界粒度。领域驱动设计规定<strong>只有聚合根才是访问聚合边界的唯一入口</strong>,这可以视为设计聚合的<strong>最高原则</strong>。因此Eric Evans 规定:</p>
<blockquote>
<p>聚合外部的对象不能引用根实体之外的任何内部对象。根实体可以把对内部实体的引用传递给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个值对象的副本传递给另一个对象,而不必关心它发生什么变化,因为它只是一个值,不再与聚合有任何关联。作为这一规则的推论,只有聚合的根才能直接通过数据库查询获取。所有其他内部对象必须通过遍历关联来发现。</p>
</blockquote>
<p>如果认可这一最高原则及基于该原则的推论,即可证明独立性之至高重要性:作为聚合内部的非聚合根实体,它只能通过聚合根被外界访问,即非聚合根实体无法被独立访问;若需要独立访问该实体,则只能作为聚合根,意味着它需要独立出来,定义为一个单独的聚合。倘若既要满足概念的完整性,又必须支持独立访问实体的需求,同时还需要约束不变量,保证数据一致性,就必然需要综合判断。而聚合的<strong>最高原则</strong>又规定了访问聚合的方式,使得概念独立性在这些权衡因素中稍占上风,成为聚合设计原则的首选。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/领域驱动设计实践(完)/067 对象图与聚合.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/领域驱动设计实践(完)/069 聚合之间的关系.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997e95e82c3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>