This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -431,9 +431,9 @@ function hide_canvas() {
<p>组合关系体现了类实例之间整体与部分之间的关系体现了“has”的概念即一个类实例“包含了”另一个或多个类实例。组合关系体现了类概念之间的一对一、一对多和多对多关系。依据关系的强弱组合关系又分别分为“合成Composition”关系与“聚合Aggregation”关系。前者的关系更强例如计算机和 CPU 之间就是合成关系,因为离开了 CPU计算机就不能正常运行后者的关系较弱例如计算机和键盘之间就是聚合关系即使没有键盘计算机仍然能够正常运行还可以使用其他输入设备来取代键盘。</p>
<p>从生命周期的角度看,如果是合成关系,表示这个整体/部分关系属于同一个生命周期,即在创建时,除了要创建代表整体概念的主对象,同时还需要创建代表部分概念的从对象,销毁也当遵循这一依存关系。如果是聚合关系,则可以独立地创建和销毁各自类的对象。</p>
<p>组合关系在 UML 中都用菱形来表示。合成为实心菱形,聚合为空心菱形,以此来形象说明其耦合的强弱。注意,菱形应放在主类一边,例如:</p>
<p><img src="assets/1294a110-ccb8-11e9-beb5-a53251e30de8" alt="82327752.png" /></p>
<p><img src="assets/1294a110-ccb8-11e9-beb5-a53251e30de8" alt="png" /></p>
<p>我们还可以在组合关系的连线上通过数字来标记它们之间到底是一对一、一对多还是多对多。例如一个 Computer 可能包含多个 CPU</p>
<p><img src="assets/254ec9c0-ccb8-11e9-9f23-07a3e2a236db" alt="36739349.png" /></p>
<p><img src="assets/254ec9c0-ccb8-11e9-9f23-07a3e2a236db" alt="png" /></p>
<p>如果类之间存在一对多关系,可以用集合来表示多的一方,例如 Order 与 OrderItem就可以定义 List<OrderItem> 作为 Order 的属性:</p>
<pre><code class="language-java">public class Order {
private List&lt;OrderItem&gt; orderItems;
@@ -456,7 +456,7 @@ public class Course {
<p>若类之间的这种多对多关系自身代表了一个领域概念则又不然应该将此关系建模为领域对象多对多关系也就随之分解为两个一对多关系。例如教师Teacher与课程Course之间存在多对多关系但这种关系实际上体现为课程表Curriculum领域概念。在引入了 Curriculum 类之后,实际就将 Teacher 与 Course 类之间的多对多关系转换为了两个独立的一对多关系。</p>
<h4>协作关系</h4>
<p>协作关系造成的耦合最弱可以理解为是类实例之间的“use”关系。这种协作关系往往通过参数传递给类的实例方法。在 UML 中往往用一个带箭头的线条来表达究竟是谁依赖谁。若被使用的对象为抽象类型则线条为虚线表示协作关系为弱依赖。例如Driver 类与 Car 类之间的关系:</p>
<p><img src="assets/539c9410-ccb8-11e9-8d89-4fa271cb1633" alt="37250087.png" /></p>
<p><img src="assets/539c9410-ccb8-11e9-8d89-4fa271cb1633" alt="png" /></p>
<p>Car 对象作为 drive() 方法的参数传递给 Driver由于 Car 是一个抽象类型,因此用虚线箭头来表示。实现代码为:</p>
<pre><code class="language-java">public abstract class Car {
public abstract void run();
@@ -476,7 +476,7 @@ public class Driver {
<li>避免双向依赖</li>
</ul>
<p>由于对象图是现实世界模型的体现,如果两个领域概念之间确实存在关系,领域设计模型就必然要体现这种关系。倘若依赖关系不可避免,我们要做的首先确定表达关系的正确形式。例如针对一对多关系,可以结合领域逻辑,探索是否可以通过为关系添加约束将一对多关系转为一对一关系。例如一个 User 拥有多个 Role但是在同一个场景中一个用户只能担任一个角色这取决于角色的名称。因此通过为关系添加角色名称约束一对多关系就转变成了一对一关系</p>
<p><img src="assets/a76df200-ccb8-11e9-beb5-a53251e30de8" alt="61910655.png" /></p>
<p><img src="assets/a76df200-ccb8-11e9-beb5-a53251e30de8" alt="png" /></p>
<p>要降低依赖的强度一种策略是引入抽象。前面讲解对象范式时已经提及这里不再赘述。对于组合关系而言正确识别关系是合成还是聚合也有利于降低依赖强度因为聚合关系要弱于合成关系。Grady Booch 将合成表达的整体/部分关系定义为“物理包容”即整体在物理上包容了部分也意味着部分不能脱离于整体单独存在。Grady Booch 说:“区分物理包容是很重要的,因为在构建和销毁组合体的部分时,它的语义会起作用。”例如 Order 与 OrderItem 就体现了物理包容的特征,一方面 Order 对象的创建与销毁意味着 OrderItem 对象的创建与销毁;另一方面 OrderItem 也不能脱离 Order 单独存在,因为没有 Order 对象OrderItem 对象是没有意义的。</p>
<p>与“物理包容”关系相对的是聚合代表的“逻辑包容”关系,即它们在逻辑上(概念上)存在组合关系,但整体并不在物理上包容部分。例如 Customer 与 Order虽然客户拥有订单但客户并没有在物理上包容拥有的订单。这时这两个对象的生命周期是完全独立的。</p>
<p>避免双向依赖是我们的设计共识除非一些特殊的模式需要引入“双重委派”例如设计模式中的访问者Visitor模式但这种双重委派主要针对的是类之间的协作关系。倘若类存在组合关系避免双向依赖的关键就是保持类的<strong>单一导航方向</strong></p>
@@ -490,7 +490,7 @@ Set&lt;Student&gt; students = dddCourse.getStudents();
</code></pre>
<p>调用固然方便了对象的加载却变得有些笨重彼此的关系也会更加复杂。在进入领域设计阶段我们除了需要通过领域设计模型正确地表达现实世界的领域逻辑之外还需要考虑质量因素对设计模型产生的影响。例如具有复杂关系的对象图对于运行性能和内存资源消耗是否带来了负面影响想想看当我们通过资源库Repository分别获得 Student 类和 Course 类的实例时是否需要各自加载所有选修课程与所有选课学生更不幸的是当你为学生加载了所有选修课程之后业务场景却不需要这些信息这不白费力气吗或许有人说延迟加载Lazy Loading可以解决此等问题但延迟加载不仅会使模型变得更加复杂还会受到 ORM 框架提供的延迟加载实现机制的约束,引入了对外部框架的依赖。</p>
<p>即便解决了这些性能问题,让我们看看存在双向导航的对象图,会成为什么样的形状?——大约会形成如下所示的一张彼此互联互通的对象网:</p>
<p><img src="assets/e4860d30-ccb8-11e9-beb5-a53251e30de8" alt="75382955.png" /></p>
<p><img src="assets/e4860d30-ccb8-11e9-beb5-a53251e30de8" alt="png" /></p>
<p>在带来引用便利的同时,双向导航让对象图成为了彼此相连、四通八达如蜘蛛网一般的网状结构。随着领域模型规模的增长,这种网状结构会变得越来越复杂,对象的层次会变得越来越深,最后陷入牵一发而动全身的悲惨境地。</p>
<p>我们需要<strong>从单一导航方向的视角对关系建模</strong>,这样可以让模型中类的依赖变得更简单。同时,还需要<strong>引入边界来降低和限制领域类之间的关系</strong>。Eric Evans 就说:“减少设计中的关联有助于简化对象之间的遍历,并在某种程度上限制关系的急剧增多。但大多数业务领域中的对象都具有十分复杂的联系,以至于最终会形成很长、很深的对象引用路径,我们不得不在这个路径上追踪对象。在某种程度上,这种混乱状态反映了现实世界,因为现实世界中就很少有清晰的边界。”</p>
<p>领域设计模型并非现实世界的直接映射如果现实世界缺乏清晰的边界在设计时我们就应该给它清晰地划定边界。划定边界时同样需要依据“高内聚低耦合”原则让一些高内聚的类居住在一个边界内彼此友好地相处不相干或者弱耦合的类分开居住各自守住自己的边界在开放合理“外交”通道的同时随时注意抵御不正当的访问要求就能形成睦邻友好的协作条约。这种边界不是限界上下文形成的控制边界因为它限制的粒度更小可以认为是类层次的边界。当我们引入这种类层次的边界后原本复杂的对象图就能拆分为各个组合简单且关系清晰的小型对象图。Eric Evans 将这个边界称之为<strong>聚合Aggregate</strong></p>
@@ -506,11 +506,11 @@ Set&lt;Student&gt; students = dddCourse.getStudents();
<li>由聚合根统一对外提供履行该领域概念职责的行为方法,实现内部各个对象之间的行为协作</li>
</ul>
<p>下图从聚合结构、行为协作与聚合边界三个角度展现了聚合的基本特征:</p>
<p><img src="assets/08759350-ccb9-11e9-8d89-4fa271cb1633" alt="33534279.png" /></p>
<p><img src="assets/08759350-ccb9-11e9-8d89-4fa271cb1633" alt="png" /></p>
<p>在聚合的内部,包含了耦合度高的实体和值对象。每个聚合只能选择一个实体作为根,并通过根来控制外界对边界内其他对象的所有访问。由聚合根公开外部接口,满足聚合之间的协作需求;同时,保证聚合内各个对象之间的良好协作。聚合内部的各个对象都应是自治的,在职责上形成分治,但对外的权利却是由聚合根来支配。聚合的边界就是封装的边界,隔离出不同的访问层次。对外,整个聚合是一个完整的概念单元;对内,则需要由聚合来维持业务不变量和数据一致性。</p>
<h4>OO 聚合与 DDD 聚合</h4>
<p>对比类之间的关系我们必须厘清面向对象的聚合Aggregation以下简称 OO 聚合与领域驱动设计的聚合Aggregate以下简称 DDD 聚合之间的区别。以问题Question与答案Answer为例前者代表了两个类之间的关系可以描述为“一个 Question 聚合了零到 N 个 Answer”后者代表的是包围在这两个类之外的边界可以描述为“聚合边界内包含了 Question 与 Answer”</p>
<p><img src="assets/2073b220-ccb9-11e9-9f23-07a3e2a236db" alt="71399514.png" /></p>
<p><img src="assets/2073b220-ccb9-11e9-9f23-07a3e2a236db" alt="png" /></p>
<p>审视类的组合关系,我必须再次强调合成与聚合之间的差异。我原本打算以 Order 与 OrderItem 之间的关系来对比 OO 聚合与 DDD 聚合。但实际上从类之间的关系来看Order 与 OrderItem 之间的关系其实是比聚合更强的合成关系,它们实例的生命周期是绑定在一起的。</p>
<p>是否只要类之间存在整体/部分的组合关系,就一定可以将这些类放在一个边界内定义为 DDD 聚合呢不一定例如在“获取客户订单”这一业务场景下Customer 与 Order 之间也存在整体/部分的组合关系,但它们却不应该放在同一个 DDD 聚合内。因为这两个类并没有共同体现一个完整的领域概念;同时,这两个类也不存在不变量的约束关系。</p>
<p>故而,<strong>我们不要将 OO 聚合与 DDD 聚合混为一谈</strong>。DDD 聚合边界内的各个类可以具有继承关系、组合关系与协作关系,即 DDD 聚合并不必然代表边界内的对象一定存在 OO 聚合关系。反过来,如果类之间存在所谓“物理包容”的合成关系,通常会考虑将其放入到同一个 DDD 聚合边界内;毕竟,一个类的实例在物理上包容了另一个类的实例,还有什么理由将它们活生生地拆开呢?</p>