Day31-03|10.2.1 对象演化:让领域模型随着业务规则持续生长

一、教材定位

面向对象系统的架构演化,首先会反映在对象和类的变化上。业务概念增加、职责重新划分、状态规则变化、关联关系调整以及数据生命周期变化,都会推动对象模型演化。对象演化不是简单地增加一个类或修改一个属性,而是要保持封装、职责、协作、继承和数据一致性之间的平衡。

对象模型通常处于架构与实现之间:它比代码抽象,能够表达领域概念和职责;又比宏观架构具体,能够指导接口、状态和测试设计。因此,对象演化处理得好,可以让系统逐步吸收新需求;处理不好,则会形成上帝对象、贫血模型、循环依赖、继承层次混乱和难以测试的代码。

二、对象演化的基本含义

对象演化是对象的属性、操作、职责、关系、状态和协作方式随系统生命周期变化而调整的过程。演化可能表现为新增对象、删除对象、拆分对象、合并对象、替换实现、改变继承关系、引入接口、改变聚合边界和迁移状态。

对象演化有两个约束。第一,外部可观察行为和业务不变量不能被无意破坏;第二,内部结构可以改进,但必须通过兼容接口、适配器、迁移脚本和回归测试控制影响。对于在线系统,还要考虑旧版本对象与新版本对象并存,以及正在处理的请求和历史数据如何兼容。

三、对象演化的主要驱动因素

1. 业务概念变化

初始系统可能只有用户、商品和订单,随着业务发展会出现会员、价格规则、促销活动、渠道、结算、履约和售后等概念。若所有新概念都塞进原有对象,类会不断膨胀,职责边界模糊。架构师要判断新概念是原对象的属性、独立对象、策略对象,还是一个新的聚合。

2. 规则复杂化

早期折扣可能只是一个固定比例,后来增加满减、阶梯、会员等级、渠道限制、时间窗口和互斥规则。将规则不断写入订单类的条件分支,会导致修改风险扩大。可以通过策略、规格、规则对象或领域服务隔离变化,使核心对象保持稳定。

3. 生命周期和状态变化

对象的状态可能增加、删除或改变转移条件。例如订单从待支付、已支付、已发货、已完成扩展出部分退款、风控冻结、拆单和逆向物流。状态变化必须同时更新状态机、持久化、消息、权限、对账和异常补偿,不能只增加一个枚举值。

4. 质量属性变化

性能要求可能促使对象改为不可变、引入缓存或批量处理;并发要求可能促使对象增加版本号和乐观锁;安全要求可能拆分敏感对象并限制访问;可测试性要求可能引入接口和依赖注入。对象结构不仅服务功能,也承载质量属性设计。

5. 技术和持久化变化

从关系数据库迁移到文档数据库、事件存储或分布式数据库时,对象与数据模型的映射会改变。不能让持久化结构反过来决定领域对象的全部职责,也不能忽略查询、事务和一致性约束。对象演化需要协调领域模型、数据模型和访问模型。

四、对象演化的典型方式

1. 添加属性和操作

适用于变化确实属于现有对象职责,且不会显著改变对象边界。例如用户增加语言偏好,订单增加备注。新增属性要考虑默认值、旧数据、序列化、接口兼容和隐私。新增操作要检查是否会把无关职责引入对象。

2. 提取对象

当一个类包含多个独立职责、变化原因和数据集合时,可以提取值对象、策略对象、服务对象或新的实体。例如把订单中的地址字段提取为地址值对象,把计价条件提取为定价策略。提取可以降低复杂度,但要避免过度拆分导致对象数量膨胀和调用链变长。

3. 合并对象

当两个对象始终一起变化、生命周期一致、职责难以独立或通信成本过高时,可以合并。合并前要分析边界、数据所有权和并发控制,不能仅因为类数量多就随意合并。

4. 引入接口和多态

当同一职责有多种实现或未来变化方向明确时,可以通过接口、策略和工厂隔离变化。例如不同支付渠道、文件存储和计费规则。多态应有稳定的业务契约,不能只是为了追求设计模式而引入复杂层次。

5. 调整继承关系

继承适合表达稳定的“是一种”关系和可替换行为。如果子类不断覆盖父类逻辑、父类增加大量条件分支,说明继承结构可能已经不适合。可以改用组合、委托和策略。调整继承会影响序列化、数据库映射、接口和测试,需要渐进迁移。

6. 引入适配器和防腐层

当旧对象模型与新模型不兼容时,可以使用适配器转换接口和数据,或者建立防腐层隔离外部模型。适配器适合短期兼容和外部系统集成,但要记录退出计划,避免转换逻辑永久堆积。

五、对象职责和内聚性

对象演化首先要关注职责。一个对象若同时负责数据持久化、权限判断、外部调用、消息发送和复杂业务规则,变化会互相影响,难以测试。可以采用单一职责、信息专家、低耦合和高内聚等原则重新划分职责。

但不能机械追求一个类只有一个方法。职责划分应结合领域语义、事务边界、数据所有权和变化原因。过度拆分会产生大量贫血对象和服务编排代码,业务规则被分散在多个地方,反而难以维护。

六、实体、值对象和服务的演化

1. 实体

实体具有持续身份,生命周期和状态变化通常需要被追踪。实体演化要考虑标识不变性、并发版本、状态迁移、历史记录和外部引用。改变实体身份或合并实体,往往需要数据迁移和业务对账。

2. 值对象

值对象由属性而非独立身份定义,适合表达金额、地址、时间段、范围和复合条件。值对象通常应该不可变,这有助于线程安全、缓存和测试。把原始字符串和数字逐渐替换为值对象,可以提高模型表达能力,但需要兼容序列化和数据库字段。

3. 领域服务

当规则不自然地属于某个实体,或涉及多个聚合时,可以使用领域服务。领域服务不能成为无边界的万能工具类。随着系统演化,要持续检查服务是否拥有清晰输入、输出和事务边界,是否应该将部分规则下沉到实体或提取为策略。

七、状态和不变量的演化

状态机是对象演化中最容易遗漏的部分。增加状态不仅要修改枚举,还要定义允许进入和离开的条件、并发处理、回滚、超时、消息和权限。对每个状态,应明确可执行操作和禁止操作。

不变量是对象在任何有效状态下都必须满足的规则,例如订单金额不能为负、已支付订单不能再次支付、库存不能无故变为负数。演化时要识别新规则是否破坏旧不变量,旧数据是否满足新规则,迁移期间如何处理不完整状态。

可以采用状态转换表辅助分析:

当前状态 事件 前置条件 目标状态 失败处置
待支付 支付成功 金额和订单有效 已支付 保留待处理并重试查询
待支付 超时 超过支付窗口 已关闭 拒绝迟到回调或转人工
已支付 申请退款 满足退款规则 退款中 记录申请并进入补偿
退款中 渠道确认 流水可核对 已退款 更新账务并通知下游

状态转换表应与代码、数据库、消息和测试用例保持一致。

八、对象演化中的兼容策略

1. 接口兼容

新增字段通常比删除和改变语义安全;删除字段要经历弃用、监控和下线过程。接口版本化、适配器和默认值可以让新旧客户端共存。兼容不仅是结构兼容,还包括错误码、时序、幂等和性能行为。

2. 数据兼容

旧数据可能缺少新字段或使用旧状态。可采用批量迁移、惰性迁移、双读、双写、影子写入和回填校验。双写会带来一致性和失败补偿问题,必须有对账与退出计划。

3. 部署兼容

滚动发布时新旧版本会同时运行,消息和接口必须支持混合版本。数据库变更通常采用先扩展、再迁移、后收缩的顺序,避免新字段尚未存在或旧版本无法读取。

九、案例分析:会员计价对象演化

某电商系统最初在订单类中直接计算商品原价、会员折扣和优惠券。随着业务增加,出现渠道价、阶梯价、活动互斥、区域税费和价格冻结,订单类已经超过数千行,测试和修改困难。

错误做法是继续增加条件分支,或一次性把所有逻辑拆成大量服务。合理做法是先梳理价格规则的变化原因和数据边界,把金额、价格区间和时间窗口提取为值对象,把不同计价算法提取为策略,把规则组合和互斥约束集中到可验证的规则对象。订单保留“得到最终价格并冻结”的业务职责,而不是知道每一种渠道的全部细节。

迁移期间可以让旧订单逻辑与新计价策略并行计算,对结果进行影子比对;差异先记录不影响用户,确认规则一致后再逐步切换。对已生成订单,价格必须保持历史冻结值,不能因为新规则发布而重新计算。评价指标包括价格差异数、订单成功率、规则命中率、计算延迟和回滚能力。

十、综合知识练习

题1

对象演化为什么不能只修改类图?

参考答案:对象变化会影响接口、状态机、数据库映射、消息、权限、并发和测试。只修改类图而不处理实现、数据和运行契约,会产生定义与实现不一致,甚至破坏业务不变量。

题2

什么时候适合提取对象,什么时候可能是过度拆分?

参考答案:当一组数据和行为具有独立概念、生命周期或变化原因,并能形成清晰边界时适合提取;如果提取后只是增加调用跳转、没有独立职责和变化价值,导致对象过多、事务分散和理解成本上升,就可能是过度拆分。

题3

新增业务状态时必须检查哪些内容?

参考答案:检查状态进入和离开条件、允许操作、不变量、并发与幂等、超时、数据库字段、消息通知、权限、回滚、历史数据和测试用例,确保状态机、代码、数据和外部契约一致。

十一、论文写作素材

论文中可以这样描述对象演化:

“随着业务规则增加,原有核心对象同时承担数据、计价、外部调用和状态管理,出现职责膨胀和测试困难。我以领域概念和变化原因重新梳理对象模型,将稳定身份保留在实体中,将金额、地址和时间范围提取为不可变值对象,将可替换算法提取为策略,并通过领域服务协调跨聚合规则。迁移过程中采用接口适配、影子计算和新旧结果比对,保留旧数据和历史业务结果,增加状态转换、幂等、对账和回滚测试。演化后通过规则差异率、故障率、发布影响范围和测试时间验证对象模型改造效果。”

十二、今日复盘

  1. 对象演化涉及属性、操作、职责、关系、状态和协作方式。
  2. 演化要保持业务行为、不变量和外部契约,不能只改类图。
  3. 实体、值对象、领域服务和策略应根据身份、职责和变化原因划分。
  4. 状态机演化必须同步检查数据库、消息、权限、并发和补偿。
  5. 接口、数据和部署兼容是在线对象演化的关键。
  6. 案例答题要写清对象边界、迁移策略、验证指标和退出条件。