我们常说某段代码复杂,但复杂到底怎么衡量?靠感觉?还是靠指标?代码复杂度度量是工程实践中最实用的质量评估手段之一,它能让代码审查从主观判断走向客观数据。本文从两种主流度量方法入手,用实际代码对比分析,帮你建立可落地的复杂度评估体系。
为什么需要度量复杂度
先说一个真实场景。团队 Code Review 时,有人觉得一个函数太长应该拆分,有人觉得还能接受。争论半天没有结论,因为每个人对复杂的感知不同。如果有量化指标,讨论就有了共同基础。
复杂度度量的核心价值有三个:
- 识别重构优先级:哪些模块最需要重构,看数据而不是靠猜
- 设置质量门槛:CI 流水线中自动拦截过高的复杂度
- 追踪改善趋势:重构前后对比指标,验证效果
圈复杂度:经典但不够完美
圈复杂度(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 年提出,专门解决圈复杂度无法反映嵌套深度的问题。
核心规则:
- 基础控制流(if、for、while、catch)每次 +1
- 嵌套层级每加深一层,额外 +1(这是关键区别)
- 布尔运算符(&&、||)每次 +1,但不因嵌套额外加分
- 递归调用 +1
- 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
|
如何设置合理阈值
没有放之四海皆准的阈值,但有一个经过大量项目验证的参考范围:
- 圈复杂度:函数级别 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 流水线,让它成为代码质量的第一道防线,比事后人肉审查高效得多。从今天开始,给你的项目跑一次复杂度扫描,看看哪个函数藏着一颗定时炸弹。