2. 架构评估:如何给现有代码做“体检”?——圈复杂度、扇入扇出、依赖关系图等量化指标
接手一个老项目,第一件事是什么?
我个人的习惯是——先别急着改代码。你得先搞清楚,这堆代码到底“健康不健康”。就像人去医院,不能直接开刀,得先做个体检。血压、心率、血常规,一套下来,问题在哪就清楚了。
代码也一样。圈复杂度、扇入扇出、依赖关系图,这些就是代码的“体检指标”。今天我们就聊聊,怎么用这些量化工具,给现有嵌入式代码做一次全面诊断。
2.1 圈复杂度:你的函数是不是“一团乱麻”?
圈复杂度(Cyclomatic Complexity,CC)是最常用的指标。它衡量的是函数中独立路径的数量。说白了,就是你的函数里有多少个“岔路口”。
计算公式很简单:
CC = E - N + 2P
其中 E 是边数,N 是节点数,P 是连通分量数。对于单个函数,P 通常为 1,所以简化为:
CC = 判断节点数 + 1
举个例子:
// 圈复杂度 = 1
void led_on(void) {
GPIO_SetBits(GPIOB, GPIO_Pin_0);
}
// 圈复杂度 = 2
void led_ctrl(uint8_t state) {
if (state) { // 1个判断
GPIO_SetBits(GPIOB, GPIO_Pin_0);
} else {
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
}
}
// 圈复杂度 = 4
int process_data(uint8_t *buf, uint16_t len) {
if (buf == NULL) return -1; // 判断1
if (len == 0) return -2; // 判断2
for (int i = 0; i < len; i++) { // 判断3(循环条件)
if (buf[i] > 0x7F) { // 判断4
buf[i] = 0x7F;
}
}
return 0;
}
经验阈值:
- CC ≤ 10:健康,可维护性好
- 10 < CC ≤ 20:需要关注,建议重构
- CC > 20:危险信号,必须拆分
我在项目中遇到过最夸张的一个函数,圈复杂度高达 87。那是一个处理串口命令的函数,里面嵌套了 6 层 if-else,还有 3 个 switch-case。改一个 bug 要花半天,因为根本看不清逻辑流向。后来我把它拆成了 12 个小函数,每个 CC 都控制在 8 以下,测试覆盖率从 30% 直接升到了 85%。
小技巧:用 gcc -fcomplexity 或者 pmccabe 工具,可以一键扫描整个工程。别手算,太累。
2.2 扇入与扇出:谁在依赖谁?
扇入(Fan-in)和扇出(Fan-out)描述的是模块之间的调用关系。
- 扇出:一个函数调用了多少个其他函数。扇出高,说明这个函数“管得宽”。
- 扇入:有多少个函数调用了这个函数。扇入高,说明这个函数“被依赖得多”。
你想想看,如果一个底层驱动函数被 50 个地方调用(高扇入),你改它的时候就得小心翼翼,因为牵一发而动全身。反过来,如果一个函数扇出超过 10,它很可能在做太多事情,违背了“单一职责原则”。
| 指标 | 理想范围 | 风险信号 |
|---|---|---|
| 扇出 | 3 ~ 7 | > 10:函数职责过重 |
| 扇入 | 2 ~ 10 | > 20:底层模块耦合过高 |
避坑指南:我曾经在一个通信协议栈里,发现一个叫 crc_check() 的函数扇入高达 43。每次修改 CRC 算法,都要重新编译半个工程。后来我把它做成了弱符号(weak symbol),允许用户重写,扇入问题就解决了。
2.3 依赖关系图:把“蜘蛛网”画出来
圈复杂度和扇入扇出都是“点”上的指标。但代码的耦合问题,往往是“面”上的。依赖关系图(Dependency Graph)能帮你把整个模块的调用关系可视化。
我习惯用 doxygen + graphviz 自动生成调用图。生成之后,重点关注三类“坏味道”:
- 循环依赖:A 调用 B,B 调用 C,C 又调用 A。这是最严重的架构问题,会导致编译死循环、测试困难。
- 上帝模块:某个模块被几乎所有模块依赖。比如一个“全局工具函数库”,里面塞了字符串处理、CRC 计算、日志打印、定时器操作……这种模块必须拆分。
- 孤岛模块:没有任何模块依赖它,它也不依赖别人。这种模块要么是废弃代码,要么是设计过度抽象。
我的经验法则:
- 依赖图应该是一棵“树”,而不是一张“网”
- 层与层之间只能单向依赖(上层依赖下层)
- 同层模块之间尽量不互相调用
2.4 实操:给代码做一次“体检报告”
光讲理论不行,我们来看看实际怎么操作。我一般按以下步骤来:
- 工具扫描:用
lizard或cppcheck跑一遍全工程,生成圈复杂度和扇入扇出数据。 - 生成依赖图:用
doxygen+graphviz生成调用关系图。 - 标记问题点:把 CC > 15、扇出 > 10、存在循环依赖的模块标记出来。
- 制定重构优先级:
- P0(立即处理):循环依赖、CC > 30
- P1(本周处理):CC 20~30、扇出 > 15
- P2(本月处理):CC 10~20、扇入 > 20
工具推荐:
lizard:轻量级,支持 C/C++,直接输出圈复杂度和 NLOC(非注释行数)cppcheck:静态分析,能检测出潜在的逻辑问题doxygen + graphviz:生成调用图,可视化依赖关系
2.5 别忘了“人肉检查”
量化指标是工具,但不能完全替代人的判断。我记得有一次,工具报告一个函数的圈复杂度只有 8,看起来很健康。但我读代码时发现,这个函数里有一个 200 行的 switch-case,每个 case 都调用了不同的外部函数。虽然路径不多,但逻辑耦合非常严重。这种“假健康”的情况,工具是看不出来的。
所以我的建议是:量化指标做初筛,人工审查做确认。两者结合,才能给出准确的架构诊断。
最后提醒一句:别为了追求指标好看而盲目重构。圈复杂度从 15 降到 8,但如果引入了新的 bug,那就得不偿失了。重构前,先写好单元测试,确保行为不变。