Contents

代码复杂度度量实战:从圈复杂度到认知复杂度

我们常说某段代码复杂,但复杂到底怎么衡量?靠感觉?还是靠指标?代码复杂度度量是工程实践中最实用的质量评估手段之一,它能让代码审查从主观判断走向客观数据。本文从两种主流度量方法入手,用实际代码对比分析,帮你建立可落地的复杂度评估体系。

为什么需要度量复杂度

先说一个真实场景。团队 Code Review 时,有人觉得一个函数太长应该拆分,有人觉得还能接受。争论半天没有结论,因为每个人对复杂的感知不同。如果有量化指标,讨论就有了共同基础。

复杂度度量的核心价值有三个:

  1. 识别重构优先级:哪些模块最需要重构,看数据而不是靠猜
  2. 设置质量门槛:CI 流水线中自动拦截过高的复杂度
  3. 追踪改善趋势:重构前后对比指标,验证效果

圈复杂度:经典但不够完美

圈复杂度(Cyclomatic Complexity)由 Thomas McCabe 在 1976 年提出,是最经典的代码复杂度指标。它衡量的是程序控制流图中独立路径的数量。

计算规则:基础值为 1,每出现一个条件分支(if、for、while、case、&&、|| 等)加 1。

看一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def get_discount(price, user_type, is_vip):
    discount = 0
    if user_type == "premium":
        if is_vip:
            discount = 0.3
        else:
            discount = 0.15
    elif user_type == "standard":
        if is_vip:
            discount = 0.1
        else:
            discount = 0.05
    else:
        discount = 0
    return price * (1 - discount)

这段代码的圈复杂度是 6(1 个基础值 + 5 个分支条件)。一般认为:

圈复杂度 风险等级 建议
1-10 低风险 简单,易于测试
11-20 中风险 可接受,适度关注
21-50 高风险 需要重构
50+ 极高风险 必须重构

圈复杂度的优点是计算简单、工具成熟,但它有一个明显缺陷:无法区分嵌套层级带来的认知负担

对比两段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 代码 A:平铺条件,圈复杂度 3
def check_a(x):
    if x > 0:
        print("正数")
    if x > 10:
        print("大数")
    if x > 100:
        print("超大数")

# 代码 B:嵌套条件,圈复杂度同样是 3
def check_b(x):
    if x > 0:
        if x > 10:
            if x > 100:
                print("超大数")

两段代码圈复杂度完全相同,但代码 B 明显更难理解。这就是圈复杂度的盲区。

认知复杂度:更贴近人类直觉

认知复杂度(Cognitive Complexity)由 SonarSource(现 Sonar)在 2016 年提出,专门解决圈复杂度无法反映嵌套深度的问题。

核心规则

  1. 基础控制流(if、for、while、catch)每次 +1
  2. 嵌套层级每加深一层,额外 +1(这是关键区别)
  3. 布尔运算符(&&、||)每次 +1,但不因嵌套额外加分
  4. 递归调用 +1
  5. break/continue/goto 配合 label +1

用前面的例子重新计算:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 代码 A:平铺条件,认知复杂度 3
# 三个 if 各 +1,无嵌套额外加分
def check_a(x):
    if x > 0:          # +1
        print("正数")
    if x > 10:         # +1
        print("大数")
    if x > 100:        # +1
        print("超大数")

# 代码 B:嵌套条件,认知复杂度 6
# 第一层 if +1,第二层 if +2(1+嵌套1),第三层 if +3(1+嵌套2)
def check_b(x):
    if x > 0:           # +1
        if x > 10:      # +2 (嵌套1层)
            if x > 100: # +3 (嵌套2层)
                print("超大数")

认知复杂度 A=3,B=6,准确反映了 B 更难理解的事实。

再看一个更实际的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public String formatUser(User user) {
    if (user != null) {                          // +1
        if (user.isActive()) {                   // +2 (嵌套1)
            if (user.getRole() != null) {        // +3 (嵌套2)
                return user.getRole().getName();
            } else {
                return "guest";
            }
        } else {
            return "inactive";
        }
    }
    return "unknown";
}
// 圈复杂度: 4
// 认知复杂度: 6

认知复杂度更准确地反映了嵌套带来的理解负担。

实战:用工具度量你的代码

Python 项目:radon

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 安装
pip install radon

# 计算单个文件的圈复杂度
radon cc -s user_service.py

# 计算认知复杂度
radon mi -s user_service.py

# 批量扫描整个项目,输出 JSON
radon cc -s -j -i venv/ .

输出示例:

1
2
3
user_service.py
    F 46-58 format_user C 12
    F 60-75 validate_user C 18

C 12 表示该函数圈复杂度为 12(C 级别),C 18 表示 18(C 级别)。

JavaScript/TypeScript 项目:eslint-plugin-sonarjs

1
2
3
4
5
6
7
// .eslintrc.json
{
  "plugins": ["sonarjs"],
  "rules": {
    "sonarjs/cognitive-complexity": ["error", 15]
  }
}

配置后,ESLint 会在认知复杂度超过 15 时报错:

1
error: Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed

Java 项目:SonarQube

SonarQube 内置认知复杂度规则,在项目仪表盘直接展示:

1
2
3
4
<!-- sonar-project.properties -->
sonar.projectKey=my-project
sonar.sources=src
sonar.qualitygate.wait=true
1
sonar-scanner

如何设置合理阈值

没有放之四海皆准的阈值,但有一个经过大量项目验证的参考范围:

  • 圈复杂度:函数级别 10 为警告线,15 为红线
  • 认知复杂度:函数级别 15 为警告线,25 为红线

不同场景需要调整:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 在 CI 中配置复杂度门槛
complexity_rules:
  cyclomatic:
    warning: 10
    error: 15
  cognitive:
    warning: 15
    error: 25
  exclude:
    - "**/test/**"        # 测试代码放宽
    - "**/generated/**"   # 生成代码豁免

关键原则:新代码严格管控,遗留代码逐步改善。不要一次性要求所有老代码达标,而是在每次修改时遵循童子军原则——改到哪,改善到哪。

降低复杂度的三个实用技巧

技巧一:用卫语句消除嵌套

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 改造前:认知复杂度 6
def process_order(order):
    if order is not None:
        if order.is_paid:
            if order.is_shipped:
                return "completed"
            else:
                return "shipping"
        else:
            return "pending"
    return "invalid"

# 改造后:认知复杂度 3
def process_order(order):
    if order is None:
        return "invalid"
    if not order.is_paid:
        return "pending"
    if order.is_shipped:
        return "completed"
    return "shipping"

技巧二:用策略模式替代多重条件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 改造前:每加一种类型就加 elif
def calculate_fee(user_type, base):
    if user_type == "vip":
        return base * 0.7
    elif user_type == "premium":
        return base * 0.85
    elif user_type == "standard":
        return base
    else:
        return base * 1.1

# 改造后:扩展不修改,复杂度恒为 1
from abc import ABC, abstractmethod

class FeeStrategy(ABC):
    @abstractmethod
    def calculate(self, base: float) -> float: ...

class VipFee(FeeStrategy):
    def calculate(self, base): return base * 0.7

class PremiumFee(FeeStrategy):
    def calculate(self, base): return base * 0.85

class StandardFee(FeeStrategy):
    def calculate(self, base): return base

strategies = {
    "vip": VipFee(),
    "premium": PremiumFee(),
    "standard": StandardFee(),
}

def calculate_fee(user_type, base):
    strategy = strategies.get(user_type, StandardFee())
    return strategy.calculate(base)

技巧三:拆分函数,单一职责

当一个函数认知复杂度超过 15,通常是承担了太多职责。按逻辑阶段拆分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 拆分前:一个函数处理验证+计算+格式化,复杂度 22
def handle_payment(order):
    # 验证逻辑 (复杂度 8)
    # 计算逻辑 (复杂度 7)
    # 格式化逻辑 (复杂度 7)
    pass

# 拆分后:三个函数各司其职
def validate_order(order):   # 复杂度 8
    ...

def calculate_amount(order): # 复杂度 7
    ...

def format_receipt(order):   # 复杂度 7
    ...

def handle_payment(order):
    validate_order(order)
    amount = calculate_amount(order)
    return format_receipt(order)

小结

圈复杂度告诉你有多少条执行路径,认知复杂度告诉你读代码有多累。两者搭配使用效果最好:圈复杂度用于测试覆盖度规划,认知复杂度用于代码可维护性评估。

度量不是目的,改善才是。把复杂度指标接入 CI 流水线,让它成为代码质量的第一道防线,比事后人肉审查高效得多。从今天开始,给你的项目跑一次复杂度扫描,看看哪个函数藏着一颗定时炸弹。