坏味道识别(上):重复代码、过长函数、过大的类(模块)、过长参数列表
各位同学,咱们今天聊聊代码里的「坏味道」。
别误会,不是代码发霉了。这个词是 Martin Fowler 老爷子提出来的,指的是代码中那些潜在的设计问题。它们不会让程序立刻崩溃,但会慢慢拖垮你的项目。
我做了十几年 C 语言开发,见过太多项目从「还能跑」变成「不敢动」。根源在哪?就是这些坏味道在作祟。今天咱们先聊四种最常见的:重复代码、过长函数、过大的模块、过长参数列表。
核心观点:坏味道不是 bug,但它是 bug 的温床。识别它们,是重构的第一步。
1. 重复代码:最明显的坏味道
重复代码是什么?说白了,就是同样的逻辑在代码里出现了两次以上。
我见过最夸张的一个项目,一个数据校验的逻辑,在 12 个文件里各写了一遍。后来需求变了,改了一个文件,另外 11 个全漏了。结果线上出了事故,查了两天才找到原因。
我曾经踩过的坑:有个模块里三段代码几乎一模一样,只是变量名不同。我当时图省事,直接复制粘贴。后来维护时改一处漏一处,被测试同事追着骂了一周。从那以后,我看到重复代码就条件反射地要提取。
怎么识别?其实很简单:
- 看到 Ctrl+C / Ctrl+V 的痕迹,基本就是重复代码
- 两个函数 80% 以上的代码相同
- 同一个算法在不同地方实现了两遍
举个例子:
// 坏味道:重复代码
void processA() {
int i;
for (i = 0; i < 100; i++) {
data[i] = data[i] * 2 + 1;
printf("data[%d] = %d\n", i, data[i]);
}
}
void processB() {
int i;
for (i = 0; i < 100; i++) {
data[i] = data[i] * 2 + 1;
printf("data[%d] = %d\n", i, data[i]);
}
// 就最后多了一行额外的处理
data[0] = 0;
}
你看,processA 和 processB 的前 90% 代码完全一样。这就是典型的重复代码。
我的建议:遇到重复代码,别犹豫,直接提取成公共函数。哪怕只是多一行差异,也值得提取。你想想看,将来改一处和改两处,维护成本差太多了。
2. 过长函数:一眼望不到头
我个人习惯,一个函数超过 30 行就开始警惕。超过 50 行,基本可以断定它「太长」了。
为什么?因为人脑的短期记忆有限。一个函数如果超过一屏,你很难同时记住开头和结尾的逻辑。调试的时候,来回翻页,效率极低。
过长函数的典型特征:
- 函数名很难概括它的功能(因为做了太多事)
- 函数内部有多个缩进层级(if 套 if,for 套 for)
- 注释特别多(因为代码本身不够清晰)
看个例子:
// 坏味道:过长函数(约 80 行)
void handleUserInput(char *input) {
// 验证输入
if (input == NULL || strlen(input) == 0) {
printf("输入为空\n");
return;
}
// ... 20 行验证逻辑
// 解析命令
if (strncmp(input, "login", 5) == 0) {
// ... 15 行登录逻辑
} else if (strncmp(input, "logout", 6) == 0) {
// ... 15 行登出逻辑
} else if (strncmp(input, "query", 5) == 0) {
// ... 20 行查询逻辑
}
// 记录日志
// ... 10 行日志逻辑
}
这个函数干了三件事:验证、处理、记录。每一件都值得单独拆成一个函数。
重构思路:每个函数只做一件事。把「验证输入」拆出去,把「处理命令」拆出去,把「记录日志」拆出去。主函数就变成几行调用,一目了然。
3. 过大的类(模块):职责不清的「大泥球」
在 C 语言里,我们不说「类」,说「模块」。一个 .c 文件动辄几千行,里面塞了各种不相关的功能——这就是过大的模块。
我记得有个项目,一个叫 utils.c 的文件,里面既有字符串处理,又有网络通信,还有文件 I/O。你想想看,这哪是工具模块,分明是个大杂烩。
过大的模块有什么问题?
- 耦合度高:改一行代码,可能影响十几个不相关的功能
- 可读性差:新同事接手,光看这个文件就要花一周
- 测试困难:一个模块的测试用例能写几百个
| 模块大小 | 建议 |
|---|---|
| < 200 行 | 理想状态,职责单一 |
| 200 - 500 行 | 可以接受,但需要关注 |
| 500 - 1000 行 | 黄色预警,考虑拆分 |
| > 1000 行 | 红色警报,必须重构 |
我的经验:判断一个模块是否过大,有个简单方法——你能否用一句话说清楚这个模块的职责?如果不能,说明它做了太多事。比如「这个模块负责网络通信」是好的,「这个模块负责网络通信、数据解析和日志记录」就太杂了。
4. 过长参数列表:函数签名的「灾难」
一个函数有 7 个、8 个甚至更多参数,你见过吗?我见过最夸张的一个,参数列表写了 3 行,总共 12 个参数。调用的时候,调用者自己都搞不清哪个参数对应哪个位置。
过长参数列表的问题:
- 调用容易出错:参数顺序搞反是家常便饭
- 难以扩展:想加一个新参数,所有调用处都要改
- 可读性差:看到函数声明就头疼
看个反面教材:
// 坏味道:过长参数列表(7 个参数)
int createConnection(char *host, int port, char *user,
char *password, int timeout, int retryCount,
int useSSL);
调用的时候,你得小心翼翼地对齐每个参数。万一哪个顺序搞错了,调试起来能把你逼疯。
重构方案:把相关的参数封装成结构体。比如上面的例子,可以把连接相关的参数放到一个 ConnectionConfig 结构体里。
// 重构后:参数减少到 1 个
typedef struct {
char *host;
int port;
char *user;
char *password;
int timeout;
int retryCount;
int useSSL;
} ConnectionConfig;
int createConnection(ConnectionConfig *config);
你看,这样一改,函数签名清爽多了。将来想加参数,只需要在结构体里加字段,不用改函数声明。
我曾经犯过的错:有个网络库的初始化函数,参数从 3 个慢慢加到 8 个。每次加参数,我都要全局搜索所有调用处,一个个改。后来实在受不了,重构成了结构体传参。改完之后,调用代码从 8 行缩成了 2 行,舒服多了。
小结一下
这四种坏味道,其实是互相关联的。过长函数往往伴随着重复代码,过大的模块往往有过长参数列表的函数。你想想看,是不是这个理?
识别坏味道只是第一步。下一节咱们会聊怎么动手重构。但在此之前,我建议你先拿自己的项目练练手——打开一个 .c 文件,数数有没有超过 50 行的函数,有没有超过 5 个参数的函数。嗯,你会发现,坏味道比你想的要多得多。
最后送大家一句话:代码和人一样,保持清爽才能活得久。别让你的代码变成「大泥球」。