DDD诊所——聚合过大综合症

2023-10-24 18:10

本文主要是介绍DDD诊所——聚合过大综合症,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

“DDD诊所”是Thoughtworks DDD社区的一项活动,通过对同事们在实施DDD过程中遇到的问题进行分析和解答,共同提高开发水平。我们将其中一些典型案例整理成文供大家参考。之后也会考虑在适当的时候将这一形式对外部开放。

就诊日期:2022年6月8日

患者:某DevOps平台持续集成模块

诊金:0元(免费义诊)

【患者主诉】

疑似问题设计

某DevOps平台需提供持续交付流水线设计功能,使用户可在界面上规划完整流程。因此,流水线设计页面功能繁杂,涵盖阶段规划、前置触发条件设置、质量门禁控制等。随着功能扩展,页面复杂度逐渐提升。此类功能旨在协助用户优化持续交付流水线管理,提升交付效能与质量。

持续交付流水线界面原

功能介绍:

  1. 设计不同的阶段:用户可以根据需要设置不同的阶段,例如开发阶段、测试阶段等。对于每个阶段,用户可以设置不同的步骤,例如Checkout、编译、构建镜像和部署等。

  2. 设计前置触发条件:用户可以选择两种触发方式,一种是某个代码仓库提交触发,另一种是定时任务。用户可以设置多个前置触发条件。

  3. 设置质量门禁:用户可以选择给阶段设置质量门禁,例如单元测试覆盖率大于80%等。这些门禁指标可以在质量门禁管理功能中设置。在流水线执行时,如果不满足门禁指标,会阻止流水线进入下一个阶段。

项目架构师在接到需求后,根据需求设计了一个名为“持续交付流水线领域模型”。该模型的目的是方便用户在界面上完成一系列操作,如代码质量检查、编译代码、构建镜像和部署等。这些操作被看作是一个整体,并被组织在一个叫做“流水线”的聚合中。这个聚合包括质量门禁、不同的阶段和触发规则等。这种设计旨在保证流水线中各个组成部分的一致性。

请注意,图中的“<>”是一种自定义的衍生关系,意味着该对象映射自其他上下文。

持续交付流水线领域模型

团队按照这个模型落地了代码,随着交付的深入,这个模型的缺点也浮现出来。

引发问题

1. 认知负载上升

这个聚合包含了7个实体(不包括抽象类),每个实体都有自己相关的业务,因此这个聚合的认知负载相对较大。此外,该部分业务需要集成不同的外部依赖系统,如Sonar(用于实现质量门禁)、定时任务组件(用于定时任务触发器)、GitLab或GitHub(用于代码提交触发器)等。尽管可以通过使用Repository模式和依赖倒置原则来分离功能和实现,但开发人员或维护人员仍需要掌握相关知识。如果某个人接手了这个功能(例如修改质量门禁相关功能),他/她基本上需要了解整个系统的工作方式。

2. 部分可用性难以实现

这个功能集成了多个第三方系统,并被设计为一个聚合。由于这些功能中的任何一部分不可用时,整个功能都将受到影响,因此难以实现部分可用性。例如,当Sonar服务暂时不可用时,代码触发和阶段维护的部分也无法为用户提供服务。如果后续用户提出了部分可用性需求,要求Sonar不可用的情况下,要提示质量门禁设置失败,但同时,其他部分仍需为用户提供服务,那么根据这个设计就很难实现了,只能推倒重建,成本非常高。

不幸的是,在设计过程中我们不知不觉地构建了一个分布式单体。分布式单体架构是对一种设计糟糕的微服务架构的戏称。一般指那种由于架构师没有充分考虑和掌握分布式的优势和代价,凭感觉设计出的微服务架构。这种架构导致设计出的系统既不能享受分布式的弹性优势,又丢失了单体服务易于实现ACID事务强一致性方面的便利。通常出现在未经良好设计的微服务风格的分布式软件中。

3. 牺牲了性能和并发性

当前的交互设计是用户在设计界面对流水线的各部分进行设计,设计完成后按提交按钮提交所有部分。实际上,用户在每次修改时,不一定需要同时修改质量门禁、阶段、触发规则等所有的部分。然而由于聚合模式的特点,我们每次都需要对整个聚合的所有实体进行整存整取。即使用户只想修改其中一部分(如“触发规则”),我们仍需要更新名为“流水线”的聚合的整个7个模型,这将导致巨大的性能浪费。此外,如果两个用户同时操作业务上互不影响的两部分如“触发规则”和“质量门禁”时,会相互冲突,有一个用户要被提示“设计已变更,修改失败”,降低了系统的吞吐量。

【诊断】

初步诊断,患者的病情主要是聚合过大综合症,即聚合设计不合理,导致聚合过大。在DDD实践中,合理划分聚合是个比较有挑战的问题。

聚合过大综合症的病理分析

在DDD的落地实践过程中,聚合的大小经常被描述为一个不可言说的知识。很多时候,凭经验和感觉会导致比较差的设计。然而,在实践中,识别大聚合仍有迹可循,一般来说,出现了这三种情况时,就需要警惕聚合过大综合症:

1. 宽聚合

一个聚合聚合了多个同级实体,一个“父亲”多个 “儿子”。如图所示:一旦超过三就有大聚合的风险。

2. 深聚合

聚合的深度过深,例如聚合根有儿子实体,也有孙子实体,也有重孙实体。当层级达到三层时,就存在大聚合的风险。

3. 胖聚合

尽管结构简单,但实体对象实例多。例如订单和订单行作为了一个聚合,而实际业务经常出现有几千个订单行的订单。这种也存在大聚合的风险。

聚合过大的三个征兆

正如该案例所表现的那样,这是一个具有宽聚合和深聚合的聚合过大综合症症状。聚合过大综合症会导致一系列问题,例如认知负荷增加、部分可用性下降以及性能问题。而在该案例中,这些问题正是由聚合过大综合症所导致的。

【治疗建议】

出现聚合过大综合症,大多数情况是由于聚合划分不合理所导致的。为了解决这个问题,我们可以回顾《领域驱动设计》一书中有关聚合模式的定义,以了解如何合理地划分聚合。

识别聚合的方法

在《领域驱动设计:软件核心复杂性应对之道》一书中,聚合的定义如下:

在具有复杂关联的模型中,要想保证对象更改的一致性是很困难的。需要维护适用于密切相关的对象组的Invariant,而不仅仅是离散的对象。然而,过于谨慎的锁定机制又会导致多个用户之间毫无意义地互相干扰,从而使系统不可用。

我们应该将 Entity和 Value Object分门别类地聚集到Aggregate中并定义每Aggregate的边界。在每Aggregate中,选择一个Entity作为根,并通过根来控制对边界内其他对象的所有访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保Aggregate中的对象满足所有固定规则,也可以确保在任何状态变化时Aggregate作为一个整体满足固定规则。

根据聚合模式的定义,我们可以得出判断两个实体(Entity)是否属于一个聚合的依据,需要把握两个条件:

1. 存在整体部分关系

当某个Entity是另一个Entity的部分时,称为整体部分关系。例如:

  • 汽车轮胎是汽车的一部分
  • 学生是班级的一部分
  • 订单行是订单的一部分
2. 实体之间存在变更时需要遵守的固定规则(Invariants)

固定规则或称为不变量不变式,来自契约式设计。

在计算机科学中,不变量是指在计算机程序执行的某一阶段始终为真的逻辑论断。例如,循环不变量是一个条件,在一个循环的每个迭代开始和结束时都是真的。
— wiki <https://en.wikipedia.org/wiki/Invariant_(mathematics)>

在划分聚合时,我们关注的是一些由业务原因所约束的固定规则。这些规则通常是通过与业务人员沟通,了解“A更新的时候,会不会引起B的某个属性的更新”,“如果两个实体暂时不一致,是否会导致难以承受的业务后果”等问题来得出的。

例如,在订单系统中,假设需求是整个订单的总价等于订单行的总价之和,且总价必须小于3000(左图)。在这种情况下,订单与订单行之间就存在固定规则。因此,可以把它们放在同一个聚合中,并通过整存整取来维护这个固定规则。然而,如果业务场景是订单仅作为订单行的分组,用户需要按照订单行逐一结账(右图)那么订单和订单行就无需划分成一个聚合。

固定规则是划分聚合的重要条件

需要注意的是,固定规则中的一部分是由技术实现所约束的固定规则,例如数据库ID不重复、订单编号唯一等。这些规则通常是全局性的固定规则,需要在整个系统范围内遵循。然而,由于这些全局性的固定规则通常与特定的业务逻辑无关,因此在划分聚合时,它们通常不是参考因素。

大聚合都必须拆小么?

尽管利用业务固定规则通常能够确定较小的业务一致性边界,从而得出比较合适的聚合规模,但并不是所有业务都适用这一原则。在有些业务场景中,大聚合可能是不可避免的。在这种情况下,如果想维护固定规则,是否采用聚合模式,需要权衡使用大聚合带来的成本是否可以接受。聚合模式是一种设计模式,而非万能的解决方案,在不适用的场景下强行应用聚合可能会导致收益不成正比。

聚合模式是为了解决复杂业务中的一致性问题,将具备固定规则的一组对象整存整取的一种方案。采用聚合模式的优点在于比较简单地就能实现固定规则约束,代价就是整存整取带来的性能损失等问题。

【治疗方案】

在案例中,尽管流水线各部分之间存在整体部分关系,通过对业务进行分析,我们确定了以下固定规则:

  1. 阶段内的步骤之间存在严格的顺序依赖,因为下一个步骤通常依赖上一个步骤的产出物。因此,存在一个固定规则:某个步骤的执行顺序必须按照阶段的步骤列表中的顺序关系。
  2. 阶段质量门禁和门禁项之间存在固定规则,即当在门禁项发生变更时,门禁版本也必须随之更新门禁的版本有一定的业务含义(例如,门禁版本更新需要在界面上进行明确提示)。

整改后的聚合

因此,根据上述整改方案,原本的一个大聚合被拆分成了四个小聚合,每个聚合只包含少量实体。这种改动可以有效地降低聚合的规模,从而更好地平衡性能和业务一致性之间的关系。

【总结】

在领域驱动设计实践中,聚合的划分确实是一个难以把握的问题。聚合本身是一种有代价的模式,不合理的聚合划分可能导致严重的问题,从而引发一系列疑问,例如“为什么使用DDD后仍然遇到各种问题?”聚合过大综合症是聚合划分中的一种常见问题,当模型中出现宽聚合、深聚合或胖聚合时,我们需要警惕聚合过大综合症及其带来的难以维护、牺牲可用性和性能损失等问题。

要解决这个问题,我们需要回顾聚合解决的核心问题:如何维护对象之间的固定规则。再次考虑聚合的划分时,需要满足两个条件:整体-部分关系和实体间需要遵循的固定规则。当使用聚合模式的代价过大时,可以考虑其他方法来实现,例如通过锁机制锁定需要维护一致性的对象方法。

总之,在实践DDD时,我们应该关注聚合的划分和优化,以确保在保持业务一致性和完整性的同时,避免因聚合过大导致的性能和可用性问题。在面临聚合模式代价过大的情况时,可以灵活选择其他方法来实现业务一致性和完整性。


文/Thoughtworks 付施威
原文链接:DDD之聚合过大-Thoughtworks洞见

这篇关于DDD诊所——聚合过大综合症的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/276901

相关文章

Jenkins构建Maven聚合工程,指定构建子模块

一、设置单独编译构建子模块 配置: 1、Root POM指向父pom.xml 2、Goals and options指定构建模块的参数: mvn -pl project1/project1-son -am clean package 单独构建project1-son项目以及它所依赖的其它项目。 说明: mvn clean package -pl 父级模块名/子模块名 -am参数

ElasticSearch的DSL查询⑤(ES数据聚合、DSL语法数据聚合、RestClient数据聚合)

目录 一、数据聚合 1.1 DSL实现聚合 1.1.1 Bucket聚合  1.1.2 带条件聚合 1.1.3 Metric聚合 1.1.4 总结 2.1 RestClient实现聚合 2.1.1 Bucket聚合 2.1.2 带条件聚合 2.2.3 Metric聚合 一、数据聚合 聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如:

七、Maven继承和聚合关系、及Maven的仓库及查找顺序

1.继承   2.聚合   3.Maven的仓库及查找顺序

maven-聚合工程

聚合工程: 聚合工程里可以分为顶级项目(顶级工程、父工程)与子工程,这两者的关系其实就是父子继承的关系,子工程在maven里称之为模块(module),模块之间是平级,是可以相互依赖的。子模块可以使用顶级工程里所有的资源(依赖),子模块之间如果要使用资源,必须构建依赖(构建关系)一个顶级工程是可以由多个不同的子工程共同组合而成。

链路聚合配置

链路聚合配置前需要将物理接口进行清除,然后将接口加入到聚合内完成对接。 对接端口核心3口4口与财务核心的23口与24口进行对接。 拓扑如下:​​​​​​​ 配置如下: 核心路由器[CK]dhcp enable [CK]interface GigabitEthernet 0/0/0[CK-GigabitEthernet0/0/0]ip address dhcp-alloc[CK]a

华为eNSP:手工链路聚合和动态链路聚合

手工链路聚合(静态链路聚合) 一、拓扑图 二、交换机配置过程 [s1]int Eth-Trunk 1#创建进入链路聚合组1[s1-Eth-Trunk1]trunkport g0/0/1#将g0/0/1口加入聚合组1[s1-Eth-Trunk1]trunkport g0/0/2[s1-Eth-Trunk1]trunkport g0/0/3[s1-Eth-Trunk1]quit 配

Flink实战案例(二十三):自定义时间和窗口的操作符(四)window functions之增量聚合函数(一)ReduceFunction

实例一 例子: 计算每个传感器15s窗口中的温度最小值 val minTempPerWindow = sensorData.map(r => (r.id, r.temperature)).keyBy(_._1).timeWindow(Time.seconds(15)).reduce((r1, r2) => (r1._1, r1._2.min(r2._2))) 实例二 ReduceFun

【硬刚ES】ES基础(十四)Elasticsearch聚合分析简介

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的ES部分补充。

【硬刚ES】ES入门 (13)Java API 操作(4)DQL(1) 请求体查询/term 查询,查询条件为关键字/分页查询/数据排序/过滤字段/Bool 查询/范围查询/模糊查询/高亮查询/聚合查

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的ES部分补充。 1 请求体查询 2 高亮查询 3 聚合查询 package com.atguigu.es.test;import org.apache.http.HttpHost;import org.apache.lucene.search.TotalHits;import org.elasticse

hadoop 日志聚合 jobhistory配置

一、前言 安装了hadoop-2.7.3集群,登陆YARN WEB界面,发现下图mapreduce应用UI入口都无法使用,现记录下处理方案  二、处理方案 1.yarn-site.xml配置yarn.resourcemanager.webapp.address,如果没配置ApplicationMaster入口无法使用  <property>          <name>yarn.resou