识别代码坏味道:从“能跑”到“好改”的第一步
说实话,我见过太多“能跑但不敢动”的C代码了。
有一次我接手一个嵌入式项目,光是一个函数就写了800多行。我当时盯着屏幕,心里就一个念头:这代码,改一行崩一片。后来我花了整整两周,才把那个函数拆成十几个小函数。
你想想看,如果代码本身就在“坏掉”的边缘,你加功能、修Bug,只会让它越来越烂。所以,识别代码坏味道,是重构的第一步,也是最关键的一步。
核心观点:坏味道不是Bug,但它预示着Bug即将到来。识别它,比修复它更重要。
1. 过长函数:一个函数只做一件事
我个人的习惯是:一个函数超过30行,我就开始警惕。超过50行,基本就要拆了。
为什么?因为函数越长,你越难理解它在干什么。你想想看,一个函数里既有初始化、又有数据处理、还有错误恢复,你读代码的时候脑子得来回切换上下文,累不累?
// 坏味道:过长函数
void process_data(int *data, int len) {
// 初始化
int sum = 0;
for (int i = 0; i < len; i++) {
sum += data[i];
}
// 计算平均值
float avg = (float)sum / len;
// 打印结果
printf("Sum: %d, Avg: %.2f\n", sum, avg);
// 写入文件
FILE *fp = fopen("result.txt", "w");
if (fp) {
fprintf(fp, "%d %.2f\n", sum, avg);
fclose(fp);
}
// 错误处理
if (avg < 0) {
printf("Error: negative average\n");
}
}
你看,这个函数干了五件事。我建议拆成五个小函数,每个只做一件事。
我的经验:拆函数的时候,别怕函数名长。比如 calculate_average()、write_result_to_file(),名字长一点没关系,关键是看一眼就知道它干什么。
2. 过大的类:一个类管太多事
在C语言里,虽然没有“类”这个关键字,但我们用结构体+函数指针模拟面向对象时,很容易造出一个“巨无霸”结构体。
我记得有一次,一个结构体里塞了30多个字段,有配置参数、有状态标志、有缓冲区、还有回调函数指针。我当时就一个感觉:这结构体,谁改谁出事。
// 坏味道:过大的结构体
typedef struct {
// 配置参数
int baud_rate;
int data_bits;
int stop_bits;
char parity;
// 状态标志
int is_connected;
int is_sending;
int is_receiving;
// 缓冲区
char tx_buffer[1024];
char rx_buffer[1024];
int tx_len;
int rx_len;
// 回调函数
void (*on_send_done)(void);
void (*on_recv_done)(void);
void (*on_error)(int err_code);
// 统计信息
int total_sent;
int total_recv;
int error_count;
} UartManager;
这个结构体,说白了就是“串口管理器”什么都管。我建议拆成:UartConfig、UartStatus、UartBuffer、UartCallbacks、UartStats。每个结构体只负责一件事。
注意:拆结构体的时候,别拆得太碎。我见过有人把一个结构体拆成十几个小结构体,结果代码反而更难读了。适度拆分,才是关键。
3. 重复代码:复制粘贴一时爽,维护火葬场
重复代码,是我在项目中遇到最多的坏味道。没有之一。
我曾经在一个项目里,发现同一个校验函数被复制粘贴了5次,每次只改了一个参数。我当时就想:写这段代码的人,是不是复制粘贴上瘾了?
// 坏味道:重复代码
int validate_uart_config(UartConfig *cfg) {
if (cfg->baud_rate < 9600 || cfg->baud_rate > 115200) return -1;
if (cfg->data_bits < 5 || cfg->data_bits > 8) return -1;
if (cfg->stop_bits < 1 || cfg->stop_bits > 2) return -1;
return 0;
}
int validate_spi_config(SpiConfig *cfg) {
if (cfg->clock_speed < 100000 || cfg->clock_speed > 10000000) return -1;
if (cfg->data_bits < 8 || cfg->data_bits > 16) return -1;
if (cfg->mode < 0 || cfg->mode > 3) return -1;
return 0;
}
你看,这两个函数结构一模一样,只是校验的参数不同。我建议提取一个通用的校验框架:
// 重构后:消除重复
typedef struct {
const char *name;
int min;
int max;
} RangeCheck;
int validate_range(RangeCheck *checks, int count, int *values) {
for (int i = 0; i < count; i++) {
if (values[i] < checks[i].min || values[i] > checks[i].max) {
printf("%s out of range\n", checks[i].name);
return -1;
}
}
return 0;
}
避坑指南:我曾经以为“就复制一次,没事的”。结果三个月后,需求变了,我改了5个地方,漏了2个。从那以后,我再也不复制粘贴了。
4. 过长参数列表:参数超过3个,就该考虑结构体了
我个人习惯:函数参数超过3个,我就开始考虑用结构体传参。超过5个,那基本就是坏味道了。
为什么?因为参数越多,调用者越容易搞错顺序。你想想看,一个函数有7个参数,你调用的时候得一个一个对,烦不烦?
// 坏味道:过长参数列表
void send_packet(int fd, char *data, int len, int timeout, int retry_count, int use_checksum, int priority) {
// ...
}
// 重构后:使用结构体
typedef struct {
int fd;
char *data;
int len;
int timeout_ms;
int retry_count;
int use_checksum;
int priority;
} SendPacketParams;
void send_packet(SendPacketParams *params) {
// ...
}
你看,用结构体传参,调用的时候只需要填字段,顺序错了也没关系。而且以后加参数,不用改函数签名,多方便。
注意:别把所有参数都塞进一个结构体。如果有些参数是“配置类”的,有些是“运行时”的,建议分开。我见过有人把配置和运行时参数混在一起,结果代码越改越乱。
5. 全局变量滥用:全局变量是“隐式依赖”的元凶
全局变量,是C语言里最容易被滥用的特性。没有之一。
我记得有一次,一个项目里用了30多个全局变量。我改一个函数,结果其他三个函数的行为都变了。我当时就一个感觉:这代码,改一行崩一片。
// 坏味道:全局变量滥用
int g_baud_rate;
int g_data_bits;
int g_stop_bits;
char g_parity;
int g_is_connected;
int g_error_count;
void init_uart() {
g_baud_rate = 115200;
g_data_bits = 8;
g_stop_bits = 1;
g_parity = 'N';
g_is_connected = 0;
g_error_count = 0;
}
void send_data(char *data, int len) {
if (!g_is_connected) {
printf("Not connected\n");
return;
}
// 使用 g_baud_rate, g_data_bits 等
}
你看,send_data() 依赖了 g_is_connected,但你在别的地方改了它,send_data() 的行为就变了。这就是“隐式依赖”。
我建议:把全局变量封装到结构体里,通过函数访问:
// 重构后:封装全局变量
typedef struct {
int baud_rate;
int data_bits;
int stop_bits;
char parity;
int is_connected;
int error_count;
} UartContext;
static UartContext ctx; // 只在当前文件可见
void uart_init() {
ctx.baud_rate = 115200;
ctx.data_bits = 8;
ctx.stop_bits = 1;
ctx.parity = 'N';
ctx.is_connected = 0;
ctx.error_count = 0;
}
int uart_is_connected() {
return ctx.is_connected;
}
我的经验:全局变量不是不能用,但一定要控制可见范围。能用 static 就用 static,能封装就封装。我见过最惨的一次,一个全局变量被20个函数读写,最后谁都不敢改。
知识体系总览
下面这张图,是我总结的“代码坏味道识别”的核心逻辑。你可以把它当作一个检查清单:
总结一下
识别代码坏味道,说白了就是培养一种“代码嗅觉”。你看到一段代码,心里要有个判断:这代码,以后改起来会不会痛?
我个人习惯是:每次提交代码前,都过一遍这五个检查点。如果发现坏味道,就顺手重构。别等到代码烂到没法改的时候再动手,那时候就晚了。
一句话记住:好代码不是写出来的,是改出来的。识别坏味道,就是改对方向的第一步。