3. 代码坏味道(下):发散式变化、霰弹式修改、依恋情结、数据泥团
好,咱们接着聊代码坏味道。上一章我们讲了几个常见的,今天这四种,说实话,是我在实际项目中踩坑最多的。尤其是当你维护一个老系统,改一个功能要动七八个文件的时候——那种感觉,就像在拆炸弹,剪错一根线就炸了。
3.1 发散式变化:一个模块,改来改去
什么叫发散式变化?说白了,就是同一个模块,因为不同的原因,需要频繁修改。
举个例子。你写了一个数据上报模块,里面既处理了日志格式,又做了网络重连,还顺带压缩了数据。好,现在产品说日志格式要加个时间戳——你改这个模块。过两天运维说网络超时时间要调——你又改这个模块。再后来,测试说压缩算法要换——还是这个模块。
你想想看,一个模块承担了太多“变化的原因”。这就像一把瑞士军刀,什么都能干,但什么都不精。而且每次改,都可能把其他功能搞崩。
我在项目中遇到过这样一个案例:一个支付模块,里面混了汇率计算、风控校验、日志记录、短信通知。每次改汇率,都要重新测试风控逻辑。后来我把它拆成了四个独立的模块,每个只负责一件事。改汇率?只动汇率模块,其他三个碰都不碰。
怎么治?
- 拆分职责:把不同变化原因的逻辑,拆到不同的类或文件中
- 面向接口编程:让调用方依赖抽象,而不是具体实现
- 单一职责原则:一个模块,只应对一种变化
3.2 霰弹式修改:改一个功能,动七八个地方
这个和发散式变化正好相反。发散式是一个模块改来改去,霰弹式是一个功能要改,你得跑遍全项目。
我记得有一次,公司要改数据库连接池的配置。好家伙,我 grep 了一下,发现连接池的创建散落在 12 个不同的文件中。每个文件里都有一段差不多的代码:
// 文件A
pool = create_connection_pool("127.0.0.1", 3306, "user", "pass");
// 文件B
pool = create_connection_pool("127.0.0.1", 3306, "user", "pass");
// 文件C
pool = create_connection_pool("127.0.0.1", 3306, "user", "pass");
改一个 IP?12 个文件全要改。漏一个?线上就出问题。这就是典型的霰弹式修改。
怎么治?
- 集中管理:把重复的代码抽成一个函数或宏
- 使用配置文件:把 IP、端口、超时时间等放到一个配置文件中
- 单例模式:对于全局唯一的资源(如连接池),用单例统一管理
3.3 依恋情结:一个函数,老惦记别人的数据
依恋情结,听起来有点文艺。说白了就是一个函数,对另一个类的数据特别感兴趣。
比如你有一个 Order 结构体,里面存了订单金额、折扣、税率。然后你写了一个 calculate_final_price 函数,里面全是 order->amount、order->discount、order->tax_rate。这个函数明明应该属于 Order 自己,却偏偏要放在外面。
// 坏味道:函数依恋 Order 的数据
double calculate_final_price(Order *order) {
double discount = order->amount * order->discount_rate;
double tax = (order->amount - discount) * order->tax_rate;
return order->amount - discount + tax;
}
// 重构后:把函数移到 Order 内部
double Order_calculate_final_price(Order *order) {
double discount = order->amount * order->discount_rate;
double tax = (order->amount - discount) * order->tax_rate;
return order->amount - discount + tax;
}
为什么会这样?很多时候是因为开发者偷懒,或者一开始设计时没想清楚职责归属。我个人习惯是:如果一个函数里,超过一半的操作都在访问另一个结构体的字段,那它就应该搬过去。
3.4 数据泥团:总是一起出现的数据
数据泥团,指的是几个数据项总是一起出现,出现在多个函数的参数列表中。
比如你经常看到这样的代码:
void draw_rect(int x, int y, int width, int height);
void move_rect(int x, int y, int dx, int dy);
bool is_point_in_rect(int x, int y, int px, int py);
x、y、width、height 这四个变量,总是成对出现。这就是数据泥团。它们应该被封装成一个 Rect 结构体。
typedef struct {
int x, y;
int width, height;
} Rect;
void draw_rect(Rect *r);
void move_rect(Rect *r, int dx, int dy);
bool is_point_in_rect(Rect *r, int px, int py);
你想想看,这样改完有什么好处?
- 参数列表变短了,函数签名更清晰
- 数据之间的关系更明确
- 以后要加个
rotation字段?只改结构体,不用改所有函数
我在项目中遇到过最夸张的一个函数,参数有 9 个,其中 6 个是成对出现的坐标和尺寸。我把它抽成两个结构体后,代码可读性直接翻倍。
知识体系总览
下面这张图,把这四种坏味道的关系和重构方向梳理了一下。你可以对照着看,加深理解。
总结一下
这四种坏味道,其实都指向同一个问题:代码的职责划分不清。要么是模块承担了太多职责,要么是同一个职责散落在太多地方,要么是数据和操作分离,要么是数据本身没有组织好。
我个人习惯是,每次提交代码前,都会快速扫一眼:
- 这个文件是不是只改了一个原因?
- 这个功能要改,是不是只动一个文件?
- 这个函数是不是在“偷”别人的数据?
- 这些参数是不是该打包了?
嗯,养成这个习惯,代码质量会提升一大截。你试试看。