DDD诊所——聚合过大综合症
2023-08-18 14:24:43来源:Thoughtworks洞见
作者|付施威
一、DDD诊所 —— 聚合过大综合症“DDD诊所”是Thoughtworks DDD社区的一项活动,通过对同事们在实施DDD过程中遇到的问题进行分析和解答,共同提高开发水平。我们将其中一些典型案例整理成文供大家参考。之后也会考虑在适当的时候将这一形式对外部开放。
(资料图)
就诊日期:2022年6月8日
患者:某DevOps平台持续集成模块
诊金:0元(免费义诊)
二、【患者主诉】疑似问题设计某DevOps平台需提供持续交付流水线设计功能,使用户可在界面上规划完整流程。因此,流水线设计页面功能繁杂,涵盖阶段规划、前置触发条件设置、质量门禁控制等。随着功能扩展,页面复杂度逐渐提升。此类功能旨在协助用户优化持续交付流水线管理,提升交付效能与质量。
图片
持续交付流水线界面原
功能介绍:
设计不同的阶段:用户可以根据需要设置不同的阶段,例如开发阶段、测试阶段等。对于每个阶段,用户可以设置不同的步骤,例如Checkout、编译、构建镜像和部署等。设计前置触发条件:用户可以选择两种触发方式,一种是某个代码仓库提交触发,另一种是定时任务。用户可以设置多个前置触发条件。设置质量门禁:用户可以选择给阶段设置质量门禁,例如单元测试覆盖率大于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
在划分聚合时,我们关注的是一些由业务原因所约束的固定规则。这些规则通常是通过与业务人员沟通,了解“A更新的时候,会不会引起B的某个属性的更新”,“如果两个实体暂时不一致,是否会导致难以承受的业务后果”等问题来得出的。
例如,在订单系统中,假设需求是整个订单的总价等于订单行的总价之和,且总价必须小于3000(左图)。在这种情况下,订单与订单行之间就存在固定规则。因此,可以把它们放在同一个聚合中,并通过整存整取来维护这个固定规则。然而,如果业务场景是订单仅作为订单行的分组,用户需要按照订单行逐一结账(右图)那么订单和订单行就无需划分成一个聚合。
图片
固定规则是划分聚合的重要条件
需要注意的是,固定规则中的一部分是由技术实现所约束的固定规则,例如数据库ID不重复、订单编号唯一等。这些规则通常是全局性的固定规则,需要在整个系统范围内遵循。然而,由于这些全局性的固定规则通常与特定的业务逻辑无关,因此在划分聚合时,它们通常不是参考因素。
大聚合都必须拆小么?尽管利用业务固定规则通常能够确定较小的业务一致性边界,从而得出比较合适的聚合规模,但并不是所有业务都适用这一原则。在有些业务场景中,大聚合可能是不可避免的。在这种情况下,如果想维护固定规则,是否采用聚合模式,需要权衡使用大聚合带来的成本是否可以接受。聚合模式是一种设计模式,而非万能的解决方案,在不适用的场景下强行应用聚合可能会导致收益不成正比。
聚合模式是为了解决复杂业务中的一致性问题,将具备固定规则的一组对象整存整取的一种方案。采用聚合模式的优点在于比较简单地就能实现固定规则约束,代价就是整存整取带来的性能损失等问题。
五、【治疗方案】在案例中,尽管流水线各部分之间存在整体部分关系,通过对业务进行分析,我们确定了以下固定规则:
阶段内的步骤之间存在严格的顺序依赖,因为下一个步骤通常依赖上一个步骤的产出物。因此,存在一个固定规则:某个步骤的执行顺序必须按照阶段的步骤列表中的顺序关系。阶段质量门禁和门禁项之间存在固定规则,即当在门禁项发生变更时,门禁版本也必须随之更新门禁的版本有一定的业务含义(例如,门禁版本更新需要在界面上进行明确提示)。图片
整改后的聚合
因此,根据上述整改方案,原本的一个大聚合被拆分成了四个小聚合,每个聚合只包含少量实体。这种改动可以有效地降低聚合的规模,从而更好地平衡性能和业务一致性之间的关系。
六、【总结】在领域驱动设计实践中,聚合的划分确实是一个难以把握的问题。聚合本身是一种有代价的模式,不合理的聚合划分可能导致严重的问题,从而引发一系列疑问,例如“为什么使用DDD后仍然遇到各种问题?”聚合过大综合症是聚合划分中的一种常见问题,当模型中出现宽聚合、深聚合或胖聚合时,我们需要警惕聚合过大综合症及其带来的难以维护、牺牲可用性和性能损失等问题。
要解决这个问题,我们需要回顾聚合解决的核心问题:如何维护对象之间的固定规则。再次考虑聚合的划分时,需要满足两个条件:整体-部分关系和实体间需要遵循的固定规则。当使用聚合模式的代价过大时,可以考虑其他方法来实现,例如通过锁机制锁定需要维护一致性的对象方法。
总之,在实践DDD时,我们应该关注聚合的划分和优化,以确保在保持业务一致性和完整性的同时,避免因聚合过大导致的性能和可用性问题。在面临聚合模式代价过大的情况时,可以灵活选择其他方法来实现业务一致性和完整性。
关键词: