跳到内容

第 6 卷 / 第 5 章 / 第 2 课

5.2 代码质量信号、静态分析与评审 ​

代码质量无法被一个分数完整表示。复杂度、重复率、覆盖率和告警数量都是观察窗口:它们帮助团队定位值得阅读的区域,不能代替对业务风险、设计边界和运行结果的判断。

圈复杂度衡量控制流路径 ​

Cyclomatic Complexity 基于控制流图的独立路径数量。教学中常近似写成:

text
复杂度约为 1 + 决策点数量

但 case、短路布尔表达式、异常处理和不同语言结构的计数方式会随工具规则而异。应让同一工具持续测量趋势,不要手算后与另一个工具硬比较。

下面是完整的 Java 21 示例,可保存为 EligibilityRules.java,执行 javac --release 21 EligibilityRules.java。Registration 是输入快照,check 要求参数非空;等级范围沿用 4.1 的闭区间 [18, 100]。

java
public final class EligibilityRules {
    public enum Result { REJECTED, PENDING_PAYMENT, ACCEPTED }
    public record Registration(boolean banned, int level,
                               boolean paid, boolean waiver) {}

    public static Result check(Registration r) {
        if (r.banned()) return Result.REJECTED;
        if (r.level() < 18 || r.level() > 100) return Result.REJECTED;
        if (!r.paid() && !r.waiver()) return Result.PENDING_PAYMENT;
        return Result.ACCEPTED;
    }
}

只数 3 个 if 得到 4;把短路 || 与 && 也计入则得到 6。PMD 提供 ignoreBooleanPaths 选项来区分这两种口径。圈复杂度描述控制流独立路径的基,不是所有执行历史的总数;循环可以产生任意多次执行。因此分数为 6 也不代表写 6 个测试就证明正确。例如,被禁赛者即使已支付且等级合法仍应拒绝;合法但未支付者有豁免时应允许。

这个函数有多个路径,但提前返回和清晰命名可能仍比一个低复杂度、却把规则藏在晦涩表达式中的版本易读。复杂度上升时应追问:是否混合职责、是否缺少领域概念、测试案例是否足够,而不是见到阈值就机械拆函数。

组合多个质量信号 ​

信号能提示什么不能直接证明什么
圈/认知复杂度分支和理解负担可能较高一定存在缺陷
代码重复修改可能需要同步多处两段逻辑一定应抽象为一个
依赖与耦合变化可能传播依赖数量少就设计良好
覆盖率哪些代码未被测试执行断言充分、需求正确
变更频率/churn哪些区域经常被触碰高频变化一定是坏设计
缺陷与事故记录哪些模块真实造成损失没有事故就没有潜在风险

把“高复杂度 + 高频修改 + 多次事故”组合起来,通常比按全仓库统一阈值排序更有行动价值。

静态分析是可重复的自动评审 ​

编译器之外的分析器可以发现:

  • 明确的错误模式,如错误空值判断或资源未关闭;
  • API 误用与并发风险;
  • 重复代码、复杂度和未使用代码;
  • 团队约定的依赖和命名规则;
  • 部分安全与数据流问题。

规则应按风险管理:

text
阻断:高置信度正确性、安全、兼容问题
告警:需要人工判断的设计与维护性问题
信息:趋势观察或渐进治理项

抑制告警时写清原因和范围。全局关闭规则会让真正问题一起消失;一条长期存在却从不处理的告警,也会训练团队忽略 CI。

升级分析器或规则集时,先固定版本、阅读变更说明,在独立变更中处理新增告警,避免工具升级与业务改动混在一起。

报告为空,只说明已启用规则在配置的分析范围内没有给出告警,并不证明程序正确。缺失依赖、反射、生成代码与路径近似都可能导致漏报或误报。抑制项应指向具体规则与代码位置,解释使告警不成立的约束,适合时保留复现测试;“历史代码”四个字还不够交差。

代码评审补充自动化看不到的部分 ​

评审应优先确认:

  1. 需求和方案是否解决了正确问题;
  2. 设计边界、数据所有权和失败语义是否合理;
  3. 正确性、安全、并发与兼容风险是否被处理;
  4. 测试是否覆盖本次变化的关键风险;
  5. 名称、注释和文档是否让后来者理解“为什么”;
  6. 复杂度是否与当前需求相称。

格式、导入顺序和可自动修复的规则交给工具。人的注意力应留给需要上下文和判断的内容。

小变更按“一个概念”衡量 ​

评审友好的变更通常只做一件可独立理解、测试和回退的事。行数只是参考:自动生成文件可能很大却易核对,跨五十个文件的两百行手工修改可能很难审。

较大的功能可以拆成:

text
1. 增加特征测试
2. 纯重命名/移动
3. 引入新端口但仍走旧实现
4. 增加新实现及测试
5. 切换调用路径
6. 删除旧实现

每一步都保持系统工作,比在长期分支里一次交付全部变化更容易发现错误和回退。

写出可执行的评审说明 ​

变更描述应说明问题、行为、风险与证据。下面是写作提示,小变更用几句话回答即可:

markdown
## Why
当前问题、影响用户/系统的方式,以及为什么现在处理。

## What
本次改变的行为及其影响边界。

## Risk
失败模式、迁移与兼容影响、回滚方式。

## Verification
自动测试、手工检查、指标或截图。

评论要区分阻断问题和非阻断建议,并解释影响:

text
[blocking] 同一取消请求重试会释放两次名额;请按本次报名的取消转换实现幂等,并检查重放后总共只释放一个名额。
[suggestion] 将 time 改名为 cancellationDeadline 能说明用途;时区仍需由类型或转换规则明确。
[question] 取消后退款失败时,报名是否保持 CANCELLED,由哪个组件重试退款?

评审讨论针对代码与约束,不针对作者。遇到分歧时回到需求、质量属性、团队规范或实验数据;如果形成长期规则,把结论写入 ADR 或工程规范,避免每个 PR 重复争论。

指标怎样回到行动 ​

text
生产事故与交付阻力
  -> 找到高风险代码区域
    -> 小步重构与补充测试
      -> 静态规则和评审防止回归
        -> 观察缺陷率、交付时间和恢复能力是否改善

如果指标改善却没有减少缺陷、理解时间或修改风险,应重新检查指标是否选错。工具要帮助团队获得反馈,指标本身不是目标。

参考资料 ​

Built with VitePress | Software Systems Atlas