3136 字
约 10 分钟
15
S03-A16 · 变更半径的测量:一次改动要碰多少地方

给订单加一个来源字段,最后改了九个文件、三个服务、两张表,外加一个定时任务里的 SQL。评审时没人反对这个字段,反对的是排期——谁也没料到收尾要两周。

"可维护性"大多是形容词,评审里争不出结论。变更半径是它唯一算得出来的一面:一次改动要碰多少地方,可以数、可以比、可以排。数得出来的东西才能谈判。


1. 半径的四个组成与计数口径

层 计数对象 为什么算它 计数陷阱
文件 改动涉及的文件数 最小粒度,决定评审量与冲突概率 生成物、纯格式化会把数抬高
模块 越过模块边界的次数 反映耦合,不反映工作量 同模块内多文件不算跨模块
服务 需要同时发布的部署单元 决定发布协调成本 只改配置不算服务半径
数据表 需 DDL 或数据回填的表 决定不可逆程度 加索引与改字段的风险不同级
─── 一次改动的半径记录,贴在 PR 描述里,四行就够
files:      9    ← 排除生成文件与纯格式化
modules:    4    ← 跨模块边界 4 次
services:   3    ← 必须同时发布的单元
tables:     2    ← 1 张加列(可回滚),1 张回填(不可逆)

口径必须先统一,否则数字会骗人:九个文件里六个是同模块的生成产物,真实半径只有三个文件。四条都要记——只记文件数会高估,只记服务数会低估,而"不可逆程度"只有 tables 那一行说得清。

services 这一层单独解释一句:它不是"涉及几个仓库",而是"要同时动几个部署单元"。同一个仓库里两个服务也是 2;一次只改一个服务但需要上下游配置同步,也算 2——判断标准是发布时要不要协调窗口。


2. 半径大的三种成因

成因 典型表现 判别信号 处置方向
抽象层次错位 一个字段在 DTO/VO/DO/Entity 里各出现一次 同一概念在多个模型里重复定义 定哪层为主,其余只做投影
共享状态过多 一张"通用配置表"被六个服务读 改它的 PR 总有跨服务影响 切出各服务私有配置
边界没有执行 跨模块 import 在编译期合法,靠约定不跨 违规只在评审时被发现 把边界变成构建期断言
─── 先判清是哪一种,再选手段:三种成因的解法互不通用
1. 同一概念出现几次?  按类名/字段名 grep,看落在几个模型里
2. 谁在共享?          按部署单元去重统计读取方
3. 边界能执行吗?      把依赖删掉再编译,能过=边界没有执行

最常见的误判是把"边界没有执行"当成"抽象层次错位",于是加一层抽象——半径没变小,耦合只是搬进了新层,还多了一个需要维护的东西。

"抽象层次错位"的根子通常在读写模型混用:写入时的校验规则、读出的展示结构、持久化的列定义,被当成了同一个对象的三个视图。一旦有人要求"这个字段到处都要有",三处就必然一起改。收窄的办法不是删模型,而是明确谁是主、其余由它投影出来。


3. 三种测量方式与各自的偏差

方式 做法 偏差方向 适合回答
按提交统计 数一个 PR 触及的文件与模块 高估:含格式化、生成物、顺手重命名 历史趋势
按需求回溯 一个需求从提出到上线实际改了什么 低估:漏掉上线后补的配置与数据修正 单次真实成本
按依赖图推演 从改动点沿依赖反向传播 高估:静态依赖远多于运行时依赖 最坏情况、评审预判

三种都不精确,组合才有意义:依赖图做预判,需求回溯校准真实成本,提交统计看趋势。单次值不可靠,趋势可靠——中位半径连着三个季度上升,比任何一次"这次改动很大"的抱怨都更有说服力。

需求回溯的可操作性比听起来好:一个需求单号通常能拉出全部关联 PR、迁移脚本与配置变更,把它们并成一个集合再数一次。它专治"上线后才发现还改了别处"——漏掉配置与数据修正的改动,在提交统计里根本看不见。

─── 依赖图推演的最小做法
1. 找出被改符号的全部引用(编译期依赖)
2. 只保留运行时会执行的路径,去掉测试与样例里的引用
3. 按部署单元聚合,得到"必须同时发布"的集合
4. 与同类改动的实测半径对比,偏差多少要记录,否则预判不会变准

4. 半径、回滚粒度与收窄手段

半径与回滚之间有一条硬约束:半径大于回滚单元时,回滚必然连带。你只能整体退回三个服务,而真正错的只有一处——这不是保守,是当前粒度下唯一正确的动作。"能不能只回滚这一处"的答案,在改动设计阶段就写定了。

手段 做法 收窄什么 代价
把变化点集中 同一概念的变体收敛到一处 文件数与模块数 要一次迁移,迁移当期半径变大
把共享状态显式化 共享物写成接口与契约,不留隐式依赖 服务数与连带回滚 多一层维护,契约得有人管
把边界变成断言 构建期检查,违规即失败 越界次数 要配构建规则与例外流程
数据与代码变更分离 先加列双写,再切读,最后删旧列 不可逆程度 周期变长,两版共存
// 断言比文档可靠:它不会被人跳过
─── 模块依赖规则示例(按你所用构建工具的语法核对)
禁止 domain → web、infra → domain 的反向依赖
违规即 CI 失败;例外必须写进白名单文件,并带失效日期

"加一层抽象"是双面的:它能收窄半径——把 N 处变化收进 1 处;也能放大半径——新层被所有人依赖之后,改它等于改全部。判据是新层的外部依赖方数量:被两个以上团队依赖的抽象层,每次改动都会产生跨团队半径。

四种手段有先后:先做"边界断言",因为它不改变结构、只暴露违规;再做"共享状态显式化",因为它需要拆依赖;"变化点集中"和"数据与代码分离"放在后面,前者要一次迁移,后者要两个版本共存。顺序反了,会在迁移途中同时面对越界、隐式依赖和不可逆数据变更三件事。

小而频繁的改动与大而少的改动,风险性质不同:前者单次风险低但暴露面多,靠回滚和观测兜住;后者单次风险高且常常不可逆,靠评审和演练兜住。用同一套流程管两种改动,一定会有一边过重、另一边过轻。

指标 怎么取 读法
中位半径 每季度 PR 半径的中位数 上升=耦合在长
半径尾部 p90 或 p95 的半径 尾部失控才是排期失控的真因
跨服务改动占比 跨服务 PR / 总 PR 高说明边界没在执行
连带回滚次数 回滚时被一起退回的单元数 大于 1 说明粒度小于半径

5. 排错:六种"半径说不清也降不下来"

现象 根因 处置
半径一直大,说不清哪层大 只记文件数 四条一起记,看哪一层在涨
每次改动都牵动同一个服务 共享状态集中在那里 显式化,或切出私有配置
回滚总是连带三个服务 回滚单元小于半径 拆发布单元,或把连带写进预案
加了抽象层后更难改 抽象层成了新的耦合中心 看它的依赖方数量,必要时拆薄
预判半径总远大于实测 依赖图算进了测试与样例引用 只保留运行时路径
新人一上手就改出跨服务改动 边界只有文档,没有执行 把规则变成构建期断言

最后一条最值得花力气:把边界写进文档,只能约束愿意读文档的人;写成构建期断言,才会约束所有人。这也是"边界"与"约定"的分界线——约定靠自觉,边界靠失败。

还有一条容易被忽略的因果:半径偏大的改动,评审往往反而更宽松。评审者面对九个文件时会本能地降低细读深度,于是大改动既贵又漏。收窄半径不只是省时间,也是把评审重新变成有效动作。


6. 边界:什么时候不该优化半径

场景 判断 更好的做法
代码量极小(单文件脚本级) 半径天然是 1 不测量,看测试与回滚即可
变更频率极低(一年几次) 收益摊不回来 接受大半径,写进变更预案
一次性迁移期 半径大是任务本身 只保回滚粒度,不优化半径
强一致、强监管领域 连带变更是正确行为 不追求收窄,追求可验证
// 三个月看一次的四个判断(只看趋势,不看单次)
1. 中位半径在涨吗?        涨 → 先找涨在哪一层
2. 尾部半径有没有失控?     有 → 那是排期风险的真正来源
3. 跨服务改动占比在涨吗?   涨 → 边界没在执行
4. 连带回滚多于单元数吗?   多 → 回滚粒度小于半径,先拆粒度

一句话收口:变更半径不能证明系统好,只能证明它可预测;而可预测的半径,是排期、评审与回滚预案三者唯一共同的输入。

优化的时机也要挑:在新功能刚稳定、改动频率低的那段时间做收窄,代价最低;在迁移中途或需求高峰做结构调整,等于把两种半径叠在一次发布里。


自测

  1. 半径的四个组成里,哪一项决定"不可逆程度",而不是工作量?
  2. 三种成因中,哪一种最容易被误判成"抽象层次错位"?误判后的后果是什么?
  3. 按提交统计半径时,偏差往哪个方向?为什么它仍值得用?
  4. "半径大于回滚单元"会导致什么必然结果?这个约束在哪个阶段就定下了?
  5. 加一层抽象什么时候会放大半径?判据是什么?
  6. 为什么说单次半径值不可靠,而趋势可靠?
S03-A16 · 变更半径的测量:一次改动要碰多少地方
https://www.clxhxhhr.top/posts/6319/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。