为什么是这些指标
dowsing 报告的每个指标都是一个选择,每个选择都有出处。本页讲的就是这些出处,以及更实用的一点——每个指标从哪里开始不再可靠。
先记住四件事
在谈具体指标之前,有四条注意事项对全部指标都成立。
- 各种复杂度代理彼此高度相关。 LoC、缩进深度、圈复杂度、认知复杂度——作为 hotspot 的一阶近似,它们给出的排序相似。没有一个能完整代表「代码有多难懂」。
- 「耦合」有若干互相竞争的定义,区别在分母。用哪一个,数字会有实质差异。dowsing 固定使用一个,并明确说明是哪一个。
- 这些指标多数与缺陷相关,而非导致缺陷。 请把它们当作排优先级的启发式,而不是证据。
- 历史数据被污染的程度,比公式的精度更影响结果。 未被追踪的重命名、格式化提交、squash——这些造成的偏移远大于任何定义上的细节。这就是这些数字可信吗单独成页的原因。
Hotspot
这个想法来自 Adam Tornhill:hotspot 是你不得不反复打交道的复杂代码。它建立在更早的实证研究之上:
- Graves et al. (2000, TSE) —— 过去的修改次数比代码行数更能预测缺陷。
- Nagappan & Ball (2005, ICSE) —— 相对 churn 能高精度预测缺陷密度。
在原始表述里,这是一个二维交叉,而不是固定的乘积。dowsing 实现的是两个归一化排名的乘积,因为一个做优先级排序的工具最终必须给出排名。
LoC 是复杂度的合法替身吗
在文件级别上,基本是的。Jay et al. (2009) 考察了 120 万个文件,发现圈复杂度 ≈ a·LoC + b,R² ≈ 0.90。
重要的但书是:归一化到函数级别后,相关性会减弱。 所以用 LoC 给文件排序没问题,一旦某个文件值得关注,就应该下沉到函数级别——这正是 dowsing health --file 给出逐函数数值的原因。
变更耦合
概念来自 Gall et al. (1998, ICSM)(逻辑耦合),由 Zimmermann et al. (2005, TSE)(关联规则)扩展,并由 D'Ambros et al. (2009, WCRE) 与缺陷建立联系。
记 |A| 为改动过 A 的提交数,|A∩B| 为同时改动两者的提交数:
| 流派 | 分母 | 公式 | 对称 |
|---|---|---|---|
| Tornhill / Code Maat / CodeScene | 平均 | 2·|A∩B|/(|A|+|B|) | 是 |
| Zimmermann(confidence) | |A| | |A∩B|/|A| | 否 |
| Jaccard | 并集 | |A∩B|/(|A|+|B|−|A∩B|) | 是 |
| min | min(|A|,|B|) | |A∩B|/min(|A|,|B|) | 是 |
dowsing 默认使用对称的 Tornhill / Code Maat 形式,同时也报告有方向的 confidence。这个选择很关键:同一对包,取表里不同的行会显示出差别很大的耦合度,所以一个不告诉你用了哪一个的工具,给出的数字根本无从解读。
默认阈值的出处
| 来源 | 最小修订数 | 最小共享 | 最小耦合度 | 大提交阈值 |
|---|---|---|---|---|
| Code Maat | 5 | 5 | 30% | > 30 个文件 |
| CodeScene | 10 | 10 | 50% | > 50 个文件 |
| Zimmermann/ROSE | support > 1 | — | confidence > 0.5 | > 30 个实体 |
dowsing 取 Code Maat 的数值,配上 CodeScene 更宽的 changeset 阈值。这些是起点,不是真理。 如果 30% 下什么都没出现,就把它调低——26% 但共变更 90 次的一对,仍然在告诉你一些事情。
认知复杂度
Campbell / SonarSource。 出发点是:圈复杂度是为衡量可测试性而设计的,作为可理解性的度量并不合适。认知复杂度被明确设计为给程序员的直觉赋值。
三条规则:
- 忽略那些把多条语句读成一条的简写结构。
- 线性流程每被打断一次 +1。
- 这种打断每嵌套一层,再加分。
| 类型 | 嵌套加分 | 增加嵌套深度 | 例子 |
|---|---|---|---|
| Structural | 是 | 是 | if、for、while、catch、switch、三元 |
| Hybrid | 否 | 是 | else if、else |
| Fundamental | 否 | 否 | 逻辑运算符链、递归、带标签的 break |
实际后果是:a && b && c 只算一次而非逐个运算符,而嵌套三层的 if 代价远高于顶层的 if。对一个声称衡量阅读成本的指标来说,这正是你想要的行为。
Code Health
dowsing 的 health 刻意不是 Maintainability Index。
MI 是一个回归多项式,因推导不透明而受到严厉批评(van Deursen, 2014):它的输入全都与规模相关,而平均化会掩盖风险最高的部分。它的系数在不同资料间也不一致。dowsing 不使用它。如果你在别处看到 MI 式的单一数字,请先问它是由什么构成的。
取而代之:
health = 10 − Σ(weight × severity)每一项都可见,每个权重都可配置,工具会告诉你哪个坏味道扣了几分。它比 MI 简单,而重点在于你能验算它。
Ownership 与 bus factor
按新增行数而非 blame 判定 ownership。 git blame 把一行归给最后碰它的人,于是格式化的人会盖过写逻辑的人。统计整个历史中的新增行数,是「谁了解这段代码」更稳定的信号。
minor contributor:ownership 低于 5% —— 来自 Bird et al.,他们发现 minor contributor 数量多与缺陷相关。
bus factor 遵循 Avelino et al. (ICPC 2016):为每个开发者、每个文件计算 Degree-of-Authorship,然后贪心地移除拥有文件最多的开发者,直到仍有作者的文件不足一半。DOA 的系数来自 Fritz et al. (ICSE 2010)。
DOA(d,f) = 3.293 + 1.098·FA + 0.164·DL − 0.321·ln(1 + AC)这是团队风险指标,不是绩效考核
Tornhill 这样警告,我们也一样。「bus factor 为 1」说的是某个包的知识集中在一个人身上。它没有说那个人做错了什么——通常恰恰相反。
这个数字还有一个特有的脆弱之处:如果开发者用多个邮箱提交、而你的 .mailmap 没有合并他们,dowsing 会把他们当成不同的人,从而高估 bus factor。包看起来比实际更安全。这种失效是无声的,所以值得明说。