前置知识: 软件工程与测试

软件度量

10 min中级

代码行度量、功能点分析、圈复杂度与软件质量指标。

1. 从”体重秤”说起

1.1 为什么需要度量

想象一个人想减肥。他能”感觉”自己胖了,但没有体重秤就不知道具体数字、无法追踪变化、无法判断方法是否有效。

软件度量(Software Metrics)就是软件的”体重秤”:用量化数据衡量软件的质量、规模、复杂度,让”软件好不好”从”感觉”变成”可测量的数字”。

1.2 度量的四个目的

目的说明
评估质量量化软件质量属性
预测工作量估算项目规模和成本
监控进度跟踪开发进展
改进过程识别改进机会

1.3 度量分类

分类说明示例
产品度量软件产品属性代码行数、缺陷密度
过程度量开发过程属性开发周期、评审次数
项目度量项目管理属性成本、进度、人力

2. 规模度量:软件”有多大”

2.1 代码行(LOC)

指标说明
SLOC源代码行(不含空行和注释)
KLOC千行代码
LOC/人月人均生产率

局限性(为什么要谨慎用 LOC):

  • 语言差异大:同一功能,Python 和 Java 行数差异巨大
  • 不反映复杂度:100 行简单代码 vs 100 行复杂逻辑,难度完全不同
  • 不鼓励简洁代码:用 LOC 考核开发者,会诱导写”又长又啰嗦”的代码

2.2 功能点分析(FPA)

功能点(Function Point)衡量软件的”功能规模”,与实现语言无关——它数的是”软件提供给用户的功能数量”,而不是”代码行数”。

五种功能类型:

功能类型简单平均复杂权重说明
外部输入(EI)346用户输入
外部输出(EO)457用户输出
外部查询(EQ)346查询操作
内部逻辑文件(ILF)71015系统维护的数据
外部接口文件(EIF)5710系统引用的数据

为什么功能点比 LOC 好:它衡量”功能价值”而非”代码量”。同样 10 个功能点,用 Java 写可能是 1000 行、用 Python 写可能是 500 行——但功能点是相同的。这让跨语言、跨团队的规模比较成为可能。

3. 复杂度度量:代码”多难懂”

3.1 圈复杂度(Cyclomatic Complexity)

圈复杂度衡量程序的线性独立路径数——即”有多少条不同的执行路径”:

V(G)=E−N+2PV(G) = E - N + 2P

(EE 边数,NN 节点数,PP 连通分量数)

简化计算:V(G) = 决策点数 + 1(决策点 = if、while、case 等分支)

圈复杂度风险等级建议
1~10低简单,低风险
11~20中较复杂,中等风险
21~50高复杂,高风险
>50极高不可测试,必须重构

圈复杂度的意义:

  • 可测试性:复杂度 30 意味着至少 30 条路径,要写 30 个测试才能覆盖——测试成本极高
  • 可维护性:复杂度越高,改一个分支越容易漏掉其他分支的影响

常见工具:SonarQube、ESLint(JavaScript)等都能自动计算圈复杂度。

3.2 认知复杂度(Cognitive Complexity)

圈复杂度有个缺陷:它对”线性 if-else 链”和”嵌套 if”同样计分,但人类理解它们的难度完全不同。认知复杂度改进了这一点:

  • 不对线性逻辑(if-else 链)增加复杂度
  • 嵌套增加额外复杂度(嵌套越深越难懂)
  • 跳转(break、continue)增加复杂度

认知复杂度更符合”人类理解代码的难度”,是圈复杂度的现代替代。

4. 质量度量:软件”好不好”

4.1 缺陷度量

指标公式说明
缺陷密度缺陷数/KLOC每千行代码缺陷数
缺陷发现率缺陷数/测试时间单位时间发现的缺陷
缺陷修复率已修复/总缺陷修复进度
缺陷泄漏率生产缺陷/(测试缺陷+生产缺陷)测试有效性(越低越好)

缺陷泄漏率特别重要:它衡量”测试漏掉了多少缺陷”。泄漏率高说明测试体系需要改进。

4.2 代码质量指标

指标说明工具
重复率重复代码占比SonarQube
测试覆盖率被测试代码的占比JaCoCo、Istanbul
技术债务修复所有问题所需时间SonarQube
代码异味数代码坏味道数量SonarQube
依赖复杂度模块间耦合度Structure101

4.3 可维护性指数(Maintainability Index, MI)

MI=171−5.2ln⁡(avgHV)−0.23avgCC−16.2ln⁡(avgLOC)+50sin⁡(2.4avgCM)\text{MI} = 171 - 5.2 \ln(\text{avgHV}) - 0.23 \text{avgCC} - 16.2 \ln(\text{avgLOC}) + 50 \sin(\sqrt{2.4 \text{avgCM}})

(HV 为 Halstead 体积、CC 为圈复杂度、LOC 为代码行、CM 为注释比例)

范围评级
0~9低
10~19中
20~100高

5. 度量实践:度量仪表盘

5.1 建立仪表盘

维度指标目标
质量缺陷密度<5/KLOC
质量测试覆盖率>80%
效率开发速度故事点/Sprint
效率构建成功率>95%
可维护性圈复杂度<15
可维护性重复率<3%

度量仪表盘的原则:指标要”少而关键”(6-8 个),并且自动采集(CI 自动统计),不要手工填表。

5.2 Goodhart 法则:度量的陷阱

“当一个度量成为目标时,它就不再是一个好的度量。“(Goodhart’s Law)

经典例子:如果考核”代码行数”,开发者会写出又长又啰嗦的代码;如果考核”测试覆盖率”,开发者会写”没意义的断言”来凑覆盖率。

避免度量陷阱的方法:

  1. 度量是工具,不是目标:度量用于发现问题,不是用于考核个人
  2. 组合多个度量:单一指标容易被”优化作弊”,组合指标更可靠
  3. 定期审视度量的有效性:如果某个指标开始被”做假”,说明它失效了

6. 常见误区

误区一:LOC(代码行)是衡量开发效率的好指标。 → 恰恰相反,LOC 鼓励写烂代码。功能点(FPA)才衡量”功能价值”。

误区二:圈复杂度越低越好。 → 复杂度 1 的代码(没有分支)可能是”把复杂逻辑硬编码了”。关键不是”低”,而是”与逻辑复杂程度匹配”。

误区三:测试覆盖率 100% = 软件没 bug。 → 覆盖率只证明”代码被跑过”,不证明”测试是有效的”。100% 覆盖率+无效断言,依然会漏掉 bug。

误区四:度量是为了考核开发者。 → 度量是为了发现系统问题(哪个模块复杂、哪里质量差),不是为了”抓人”。

误区五:指标越多越好。 → 指标太多会淹没重点,团队不知道该优化哪个。少而关键(6-8 个)才有效。

7. 与相邻知识的关系

  • 技术债务管理:度量给债务「定价」——SQALE 把技术债务折算成修复时间,正是用本篇的度量思想把「代码很烂」翻译成「还清要 3 人月」,管理与层才听得懂;
  • 质量属性:质量属性定义「好」的方向(性能、可维护性……),度量给出「好到什么程度」的数值——一个是坐标轴,一个是坐标读数;
  • CI/CD 测试:度量仪表盘要求「自动采集」,落点就在 CI 流水线——覆盖率、圈复杂度、重复率由每次构建自动产出并留档,手工填表的度量活不过三个月;
  • 代码评审清单:评审是「事前」的质量闸门,度量是「事后」的质量读数——评审拦下的缺陷不会进入缺陷密度,两者的数字要放在一起解读才不失真。

8. 面试题思路

「测试覆盖率 100% 能说明什么、不能说明什么?」 按第 6 节误区三的框架答:覆盖率只证明「代码被执行过」,不证明「断言有效」。加分结构是举一个反例:删掉断言的测试照样拉高覆盖率但测不出任何缺陷;再补一刀——覆盖率适合当下限门槛(低于 60% 说不合格有道理),不适合当追求目标(冲 100% 必然催生凑数测试),这正是 Goodhart 法则的应用。

「圈复杂度怎么算?给一段嵌套 if 代码手算 V(G)。」 用简化公式 V(G) = 决策点数 + 1 数分支(if / while / for / case 各算一个,逻辑与/或按短路分支算两个),再按 1-10 / 11-20 / >20 的风险表给结论。面试官常追问「为什么圈复杂度高的代码难测」——答「线性独立路径数 = 最少需要的测试路径数」,把复杂度和测试成本直接挂钩。

「你们团队怎么选度量指标?」 按「少而关键 + 自动采集 + 防作弊」三层答:选 6-8 个与交付目标直接相关的指标;接进 CI 自动产出;组合互补指标(覆盖率配缺陷泄漏率)防止单指标被「优化作弊」。能主动说出 Goodhart 法则并给反例,是这题的核心得分点。

9. 动手与遮代码自检

先看任务与提示,自己算完再看参考实现。

练习 1:缺陷度量计算与解读(实践题)

任务:某版本共 12000 行代码。测试阶段发现 90 个缺陷,上线后一个月生产环境暴露 10 个;其中 30 个测试缺陷与 4 个生产缺陷发生在订单模块(2500 行)。计算:(1) 整体缺陷密度与泄漏率;(2) 订单模块缺陷密度与泄漏率;(3) 解读两个模块的数字分别说明什么。

提示:密度 = 缺陷数/KLOC;泄漏率 = 生产缺陷/(测试缺陷+生产缺陷)。两个模块的数字要对比着读。

参考实现:

text
(1) 整体:密度 = 100 缺陷 / 12 KLOC,约 8.3/KLOC
    泄漏率 = 10 / (90+10) = 10%
(2) 订单模块:测试 30 + 生产 4 = 34 个缺陷,密度 = 34 / 2.5 KLOC,约 13.6/KLOC
    泄漏率 = 4 / 34,约 11.8%
(3) 解读:订单模块密度是整体均值的 1.6 倍,符合「缺陷集群性」——它是高风险区,
    应优先重构与加测试;整体与模块的泄漏率接近,说明测试「漏」的比例均匀,
    不是订单模块独有问题,改进方向是整个测试策略而非只补订单用例。

自检:你有没有把「密度高」和「泄漏率高」混为一谈?前者说明「这里 bug 多」,后者说明「测试网有洞」——两者指向不同的改进行动。

练习 2:手算圈复杂度(实践题)

任务:数下面这段伪代码的圈复杂度,并按风险表给结论。

text
function check(order):
    if order == null:                     # 决策点 1
        return "invalid"
    if order.paid and order.shipped:      # 决策点 2、3(and 短路各算一个)
        return "done"
    for item in order.items:              # 决策点 4
        if item.price > 100:              # 决策点 5
            apply(item)
        else:
            skip(item)
    while order.retry < 3:                # 决策点 6
        order.retry += 1
    return "ok"

提示:用「决策点数 + 1」,注意 and 的短路行为让一个条件算两个决策点。

参考实现:

text
决策点:5 个显式 if/for/while + 1 个 and 短路 = 6
V(G) = 6 + 1 = 7 → 落在 1~10 区间,低风险
结论:可以接受,但每加一个分支都在消耗可测试性预算;继续膨胀到 10 以上
应考虑拆函数(与 360-Refactoring 的手法衔接)。

自检:and 算了两个决策点吗?如果把前两个 if 合并成一个带 and 的复合条件提前 return,复杂度是升还是降?(决策点数不变——and 仍算两个,但可读性更差:这正是认知复杂度比圈复杂度更贴近「人类难度」的原因。)

练习 3:给一个假想项目设计度量仪表盘(挑战题)

任务:为一个 10 人 Web 团队的电商项目设计度量仪表盘:选 6 个以内指标,每个注明「为什么选它、从哪自动采集、阈值多少、被作弊了怎么办」。

提示:从质量(密度/泄漏率/重开率)、可维护性(复杂度/重复率/覆盖率)、效率(构建成功率)三个维度各取所需;「防作弊」一栏对应 Goodhart。

参考实现:

text
| 指标           | 为什么选           | 采集点               | 阈值     | 防作弊                     |
| 缺陷泄漏率     | 测试有效性的总成绩 | 生产缺陷库 vs 测试库 | <5%      | 与重开率组合看             |
| 重开率         | 修复质量           | 缺陷流程日志         | <10%     | 只统计「同现象重开」        |
| 测试覆盖率     | 下限门槛           | CI 单测覆盖率报告    | >70%     | 配合变异测试抽查断言有效性  |
| 圈复杂度超标数 | 可维护性预警       | SonarQube            | 0 个 >20 | 只报新增,不追历史存量      |
| 重复率         | 维护成本预警       | SonarQube            | <3%      | 与「修改热点文件」交叉看    |
| 构建成功率     | 主干健康度         | CI 记录              | >95%     | 禁止重跑刷绿               |

自检:六个指标里有没有互相「打架」的(比如覆盖率达标但泄漏率超标时信谁)?你的答案应该是:以泄漏率为准绳,覆盖率只是过程指标。