软件度量
代码行度量、功能点分析、圈复杂度与软件质量指标。
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) | 3 | 4 | 6 | 用户输入 |
| 外部输出(EO) | 4 | 5 | 7 | 用户输出 |
| 外部查询(EQ) | 3 | 4 | 6 | 查询操作 |
| 内部逻辑文件(ILF) | 7 | 10 | 15 | 系统维护的数据 |
| 外部接口文件(EIF) | 5 | 7 | 10 | 系统引用的数据 |
为什么功能点比 LOC 好:它衡量”功能价值”而非”代码量”。同样 10 个功能点,用 Java 写可能是 1000 行、用 Python 写可能是 500 行——但功能点是相同的。这让跨语言、跨团队的规模比较成为可能。
3. 复杂度度量:代码”多难懂”
3.1 圈复杂度(Cyclomatic Complexity)
圈复杂度衡量程序的线性独立路径数——即”有多少条不同的执行路径”:
( 边数, 节点数, 连通分量数)
简化计算: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)
(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)
经典例子:如果考核”代码行数”,开发者会写出又长又啰嗦的代码;如果考核”测试覆盖率”,开发者会写”没意义的断言”来凑覆盖率。
避免度量陷阱的方法:
- 度量是工具,不是目标:度量用于发现问题,不是用于考核个人
- 组合多个度量:单一指标容易被”优化作弊”,组合指标更可靠
- 定期审视度量的有效性:如果某个指标开始被”做假”,说明它失效了
6. 常见误区
误区一:LOC(代码行)是衡量开发效率的好指标。 → 恰恰相反,LOC 鼓励写烂代码。功能点(FPA)才衡量”功能价值”。
误区二:圈复杂度越低越好。 → 复杂度 1 的代码(没有分支)可能是”把复杂逻辑硬编码了”。关键不是”低”,而是”与逻辑复杂程度匹配”。
误区三:测试覆盖率 100% = 软件没 bug。 → 覆盖率只证明”代码被跑过”,不证明”测试是有效的”。100% 覆盖率+无效断言,依然会漏掉 bug。
误区四:度量是为了考核开发者。 → 度量是为了发现系统问题(哪个模块复杂、哪里质量差),不是为了”抓人”。
误区五:指标越多越好。 → 指标太多会淹没重点,团队不知道该优化哪个。少而关键(6-8 个)才有效。