3112 字
约 10 分钟
14
S03-A15 · 可维护性的评估:好维护是怎么看出来的

接手一个项目,上一份评估报告写着"代码质量良好:命名规范、分层清晰、注释齐全"。三个月后真实感受是:改一个字段要动七个文件,跑一次回归要四十分钟,出了问题只能靠日志猜。报告没错——它评估的是代码的样子,而日常消耗你的是变更的成本。

可维护性不是代码属性,是变更成本属性。它衡量的是一件很具体的事:把一个已知的改动从想法送到线上,需要多少时间、多少上下文、多少次验证、多少次返工。这篇给一套可观测的判据,以及一套不给出分数、只给出场景的结论写法。


1. 四个可观测维度

四个维度都能从历史数据里量出来,不需要读代码。

维度 观测方式 典型量级(按团队历史校准) 恶化信号
改动波及面 一个需求 PR 触及的文件/模块数 p50 三到五个文件 p90 突然翻倍
上下文需求 完成改动前需要读多少个文件才敢动手 五个以内可接受 需要跨三个模块才敢改
局部可验证 能否只跑一部分测试就确认正确 单模块测试一分钟内 任何改动都要全量回归
失败可定位 从报警到定位到代码行的时长 分钟级 只能靠复现与猜测

四个维度里,改动波及面最容易量,失败可定位最难量但最有决策价值——它决定值班的人半夜能不能睡着。

─── 变更半径:从提交历史反推,不需要任何工具链改造
git log --since="180 days" --pretty=%H --name-only | \
  awk 'BEGIN{RS=""} {print NF-1}' | sort -n | \
  awk '{a[NR]=$1} END{print "p50="a[int(NR*0.5)], "p90="a[int(NR*0.9)]}'
// 只看同一 PR 内的文件数,跨 PR 统计会被拆单污染
// 数字本身不判优劣,要和自己半年前的同口径结果比

一个隐含前提:变更半径必须在同一批需求类型上比。修文案的 PR 和改结算逻辑的 PR 天然差十倍,混在一起算平均,得到的数字既不可比也不可解释。

"局部可验证"这一维度常被误读成"测试多不多"。它实际问的是:改完之后,能不能只跑一小块就获得信心。这取决于测试的切分方式——按模块切、按依赖注入切、还是全都依赖同一个数据库与同一套夹具。一个所有测试都要起完整容器环境的应用,局部验证能力等于零,因为每次验证的最小单位就是全量。

─── 局部可验证的判定:不看用例数,看"改一个类要跑什么"
change: OrderPricing.java
must_run:
  - OrderPricingTest            // 单模块,20s
  - PricingContractTest         // 依赖跨模块契约,2min
  - full_regression             // 若改了费用汇总口径,则必须
// 前两项能在本地跑完 = 局部可验证
// 三项都指向 full_regression = 该维度实际上已经失效

2. 三个证据来源与各自的偏差

证据 怎么取 强项 偏差
变更历史 提交、PR、故障单的时间序列 客观、可回溯 受提交粒度与拆单习惯影响
上手时间 新人从入职到首个非平凡提交 直接反映认知成本 样本极少,一个人就是噪声
故障恢复时长 从报警到恢复的时长分布 反映定位能力 受值班熟练度与工具影响

三个来源的偏差方向不同,所以结论要交叉:变更历史说"波及面在涨",上手时间说"新人两周才敢改",故障恢复说"平均一小时"——三者同向时结论确定;只有一项异常时,先怀疑度量。

上手时间是唯一直接测"需要多少上下文"的证据,也是最容易被浪费的:多数团队让新人第一周看文档、第二周改文案,这两周的数据被排除在统计外。从第一天开始记录他卡在哪,比事后补一份文档有用得多。

故障恢复时长要拆成两段才有信息量:定位时长与修复时长。前者反映可观测性与代码结构,后者反映改动难度与发布链路。合成一个总时长,会被发布排队这种与代码无关的因素主导。

─── 故障恢复时长:必须是分段中位数,不能是平均值
SELECT incident_id,
       TIMESTAMPDIFF(MINUTE, alerted_at, located_at) AS locate_min,
       TIMESTAMPDIFF(MINUTE, located_at, fixed_at)  AS fix_min,
       TIMESTAMPDIFF(MINUTE, fixed_at, restored_at) AS release_min
FROM incident
WHERE severity IN ('P1','P2') AND created_at >= DATE_SUB(NOW(), INTERVAL 180 DAY);
// locate_min 长时间不降 = 代码结构或可观测性问题
// release_min 长时间不降 = 发布链路问题,与代码无关
─── 上手轨迹:按天记三件事,不做主观评分
D1  能跑起来吗        → 卡在依赖下载 / 环境变量 / 证书
D3  能改一处并验证吗  → 卡在不知道改完该跑哪些测试
D7  能独立改一个需求吗 → 卡在跨模块调用链读不懂
// 每一"卡"都是一个可改进项,直接对应文档或工具缺口

静态指标的失效要单独说。圈复杂度、代码行数、注释率与维护难度的相关性很弱:三十行的状态机可能是全项目最难改的地方,八百行的纯数据映射文件可能三年没人碰。复杂度分数测的是代码形态,不是变更成本。

静态指标 看起来在测什么 实际测到什么 结论
圈复杂度 逻辑难懂程度 分支数量 与改动成本弱相关
注释率 可读性 注释字符占比 与理解成本几乎无关
重复代码率 一致性风险 文本相似度 误报多,且常与解耦冲突
测试覆盖率 安全网强度 执行过的行数 不反映断言质量

覆盖率尤其要小心:它证明代码被执行过,不证明失败会被发现。一条只断言"不抛异常"的用例,能给覆盖率贡献两个百分点,对可维护性没有贡献。


3. 判据、时间维度与结论写法

"有没有测试"不是判据,"测试能不能定位失败"才是。判据可以写得很硬:改动引入一个 bug,测试是否在十分钟内指出具体位置。做不到这一点,测试数量再多也只是回归成本。

─── 可维护性判据写进验收,而不是写进文档
@Test
void failurePointsAtCause() {
    var result = pricing.apply(orderWithNegativeQty());
    // 断言要指向业务不变量与具体字段
    assertThat(result.errors())
        .extracting("field", "code")
        .containsExactly(tuple("quantity", "MUST_BE_POSITIVE"));
}
// 只断言"抛了异常"的用例,失败时仍然需要人工定位

文档的作用被高估:文档描述意图,不描述约束。真正拖慢改动的是那些没人写下来的隐含约束——字段必须按某个顺序赋值、缓存键必须带租户前缀、某个接口必须幂等。约束只能靠断言与测试固化,文档里的约束会随版本漂移。

时间维度是最容易被忽略的一条:今天好维护不代表半年后好维护。评判对象是变化率而不是当前状态,所以下一条的结论写法必须带有效期。

─── 评估结论的写法:给场景,不给分数
不合格:可维护性评分 7.2/10,代码质量良好
合格:  改结算金额字段需动 6 个文件、跨 2 个模块;
        单模块测试无法覆盖,需全量回归 40 分钟;
        上月同类改动 3 次,其中 1 次引发线上问题
// 每条都对应一个可验证的动作,分数对应不了任何动作

与团队熟悉度的混淆是最危险的错觉:熟悉的人觉得好维护,因为他已经把约束记在脑子里了。判据很简单——换一个人来改,成本是否相同。如果答案是"只有他知道怎么改",那这个模块的可维护性接近零,无论代码多漂亮。

结论必须带有效期,并写清重测触发条件。可维护性是变化率而不是状态:模块被拆、依赖大版本升级、核心成员离开、月度改动次数翻倍——这四件事发生后,旧结论一律作废,不必等到重测周期。

改进优先级只有一条顺序:先降低变更传播半径,再谈代码风格。传播半径是结构问题,风格是表达问题;结构没理顺时改风格,等于给一个漏水的管子刷漆。


4. 排错:评估结论与实际感受不一致的五种情况

现象 原因 处理
报告说良好,实际很难改 只看了静态指标 补变更历史与故障时长
变更半径很小但仍很慢 慢在验证与等待,不在改动 量"改动到合并"的分段耗时
新人上手快,老人却总踩坑 老人绕过了既有约束,隐性知识在退化 把踩过的坑转成断言
覆盖率很高但故障多 断言只覆盖 happy path 看变异杀伤与逃逸缺陷
换个团队评估结论完全相反 熟悉度差异被当成了质量差异 统一判据:换人改一次要多久

第二条值得展开:可维护性差经常表现为等待——等评审、等流水线、等环境、等联调。这些时间不在代码里,却往往是变更成本的主要部分。


5. 边界:什么时候可维护性投入该压到最低

场景 合理投入 理由
一次性脚本、数据迁移 零,只保证能重跑 生命周期以天计
即将废弃的模块 只做隔离,不做重构 改进无处落地
变更频率极低的代码(年改一次) 低,够用即可 摊销不了改造成本
原型与验证性代码 低,明确标注"不维护" 混淆原型与产品才是真风险
强监管、长生命周期系统 高 变更成本会持续复利

判据可以统一成一句话:预期剩余变更次数 × 单次改动成本。剩余变更次数少于三次,重构的投入收不回来;大于二十次,任何降低单次成本的动作都划算。这条乘法比任何评分都可靠,因为它把"未来还会改几次"这个前提摆到了明面上。

原型代码要标注"不维护",否则它会以产品代码的身份进入评估,把预算引开。标注不是免责,而是分配。


6. 自测

  1. 为什么说可维护性是变更成本属性,而不是代码属性?
  2. 四个可观测维度里,哪一个最难量但最有决策价值?
  3. 上手时间被统计浪费的常见原因是什么?
  4. 覆盖率为什么不能作为可维护性的判据?
  5. "熟悉的人觉得好维护"为什么是最危险的错觉?用什么判据拆穿它?
  6. 改进优先级里,为什么先降变更传播半径而不是先改代码风格?
S03-A15 · 可维护性的评估:好维护是怎么看出来的
https://www.clxhxhhr.top/posts/6311/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。