2. 代码坏味道(上):重复代码、过长函数、过大的类、过长参数列表
大家好,我是你们的老朋友。今天咱们聊聊代码里的「坏味道」。
别误会,不是代码真的馊了。而是有些写法,一看就知道——这里迟早要出事。我做了十几年C语言开发,踩过的坑比写过的代码还多。今天这四种坏味道,几乎每个项目里都能碰到。
核心观点:坏味道不是bug,但它是bug的温床。闻到味道就改,别等它发酵。
2.1 重复代码:最懒的写法,最痛的代价
重复代码是什么?说白了,就是同一段逻辑在多个地方出现。你复制粘贴,我复制粘贴,大家都这么干。
我见过一个项目,同样的校验逻辑在五个文件里各写了一遍。后来需求改了,改代码的人只改了其中三个文件。结果呢?线上出了事故,查了两天才发现是漏改了。
我曾经在一个嵌入式项目里,因为重复的延时函数没统一,导致不同模块的时序偏差。那一次,整个产线停了两天。从那以后,我对重复代码零容忍。
怎么识别?很简单:
- 看到两段代码长得像双胞胎
- 一个函数里出现多次相同的逻辑块
- 不同文件里有几乎一样的结构体定义
怎么改?提取成函数或宏。举个例子:
// 坏味道:重复代码
void processA() {
int i;
for (i = 0; i < 100; i++) {
buffer[i] = buffer[i] * 2 + 1;
}
}
void processB() {
int i;
for (i = 0; i < 100; i++) {
buffer[i] = buffer[i] * 2 + 1;
}
}
// 重构后
static inline void transform_buffer(int *buf, int len) {
for (int i = 0; i < len; i++) {
buf[i] = buf[i] * 2 + 1;
}
}
void processA() { transform_buffer(buffer, 100); }
void processB() { transform_buffer(buffer, 100); }
我个人习惯:只要同一段代码出现第三次,就必须提取。第一次可能是巧合,第二次是偷懒,第三次就是技术债了。
2.2 过长函数:一个函数干十件事
你见过一个函数超过200行吗?我见过。而且不止一个。
过长函数的问题在于——你根本不知道它到底在干什么。读代码的人要上下翻好几屏,才能勉强理清逻辑。更可怕的是,这种函数往往耦合了多个职责,改一处可能崩十处。
为什么会这样?说白了,就是「顺手」二字。写代码时觉得「再加一个功能也没啥」,结果越加越多,最后成了一团乱麻。
我建议:一个函数最好控制在30行以内。超过50行,就该考虑拆分了。
// 坏味道:过长函数
void handle_data(int *data, int len, int mode) {
// 校验
if (data == NULL || len <= 0) return;
// 过滤
for (int i = 0; i < len; i++) {
if (data[i] < 0) data[i] = 0;
}
// 转换
if (mode == 1) {
for (int i = 0; i < len; i++) {
data[i] = data[i] * 2;
}
} else if (mode == 2) {
for (int i = 0; i < len; i++) {
data[i] = data[i] / 2;
}
}
// 输出
for (int i = 0; i < len; i++) {
printf("%d ", data[i]);
}
}
// 重构后
static int is_valid(int *data, int len) {
return data != NULL && len > 0;
}
static void clamp_data(int *data, int len) {
for (int i = 0; i < len; i++) {
if (data[i] < 0) data[i] = 0;
}
}
static void transform_data(int *data, int len, int mode) {
for (int i = 0; i < len; i++) {
data[i] = (mode == 1) ? data[i] * 2 : data[i] / 2;
}
}
static void print_data(int *data, int len) {
for (int i = 0; i < len; i++) {
printf("%d ", data[i]);
}
}
void handle_data(int *data, int len, int mode) {
if (!is_valid(data, len)) return;
clamp_data(data, len);
transform_data(data, len, mode);
print_data(data, len);
}
关键点:每个函数只做一件事。做好一件事。只做一件事。
2.3 过大的类:一个结构体装下全世界
C语言里没有类,但结构体就是我们的「类」。过大的结构体,什么字段都往里塞,最后成了一个「万能口袋」。
我在项目中遇到过这样的结构体:
// 坏味道:过大的结构体
struct device {
int id;
char name[64];
int type;
// 网络相关
char ip[16];
int port;
// 传感器相关
float temperature;
float humidity;
// 配置相关
int baudrate;
int parity;
// 状态相关
int status;
int error_code;
// 日志相关
char log_path[256];
int log_level;
// ... 还有20多个字段
};
你想想看,这个结构体到底代表什么?它既管网络,又管传感器,还管日志。改网络的人要小心别碰了传感器字段,改日志的人又怕影响状态。耦合度极高。
我曾经接手过一个项目,一个结构体有80多个字段。每次加新功能都要在这个结构体里加字段,最后编译都慢得不行。重构时拆成了6个独立的结构体,代码瞬间清爽了。
怎么拆?按职责分:
// 重构后
struct network_config {
char ip[16];
int port;
};
struct sensor_data {
float temperature;
float humidity;
};
struct serial_config {
int baudrate;
int parity;
};
struct device_status {
int status;
int error_code;
};
struct device {
int id;
char name[64];
int type;
struct network_config net;
struct sensor_data sensor;
struct serial_config serial;
struct device_status status;
};
我个人习惯:一个结构体超过10个字段,我就会停下来想想——是不是该拆了?
2.4 过长参数列表:函数签名比函数体还长
你见过这样的函数吗?
// 坏味道:过长参数列表
int configure_device(int id, char *name, char *ip, int port,
int baudrate, int parity, int mode,
int timeout, int retry_count, int log_level);
调用这个函数的人,得记住10个参数的顺序和含义。少传一个?编译报错。传错顺序?运行时崩溃。这种函数,说白了就是在折磨调用者。
为什么会这样?因为写函数的人图省事,把所有配置项都塞进参数列表里。他觉得「这样调用方灵活」,实际上调用方只想骂人。
怎么改?把相关参数封装成结构体:
// 重构后
struct device_config {
int id;
char *name;
char *ip;
int port;
int baudrate;
int parity;
int mode;
int timeout;
int retry_count;
int log_level;
};
int configure_device(struct device_config *cfg);
这样调用方只需要填充结构体,传一个指针就行。而且以后加参数,不用改函数签名,结构体里加字段即可。
原则:参数超过3个,就该考虑封装。超过5个,必须封装。
小结
这四种坏味道,说白了都是「偷懒」的代价。重复代码是懒得提取,过长函数是懒得拆分,过大的类是懒得解耦,过长参数列表是懒得封装。
但你要知道——今天偷的懒,明天都要加倍还。我见过太多项目,因为早期不重视这些坏味道,后期重构成本高到离谱,最后只能推倒重来。
嗯,今天就聊到这儿。记住:闻到坏味道就改,别等它发酵。
公众号:蓝海资料掘金营,微信deep3321