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->amountorder->discountorder->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);

xywidthheight 这四个变量,总是成对出现。这就是数据泥团。它们应该被封装成一个 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 个是成对出现的坐标和尺寸。我把它抽成两个结构体后,代码可读性直接翻倍。

判断标准: 如果你发现三个以上的参数总是同时出现,或者你经常复制粘贴同一组变量,那就是数据泥团。

知识体系总览

下面这张图,把这四种坏味道的关系和重构方向梳理了一下。你可以对照着看,加深理解。

代码坏味道(下)知识体系 发散式变化 一个模块,多种修改原因 霰弹式修改 一个修改,散落多处 依恋情结 函数迷恋外部数据 数据泥团 数据总是一起出现 拆分职责 单一职责原则 集中管理 消除重复,统一配置 搬移函数 数据与行为在一起 封装结构体

总结一下

这四种坏味道,其实都指向同一个问题:代码的职责划分不清。要么是模块承担了太多职责,要么是同一个职责散落在太多地方,要么是数据和操作分离,要么是数据本身没有组织好。

我个人习惯是,每次提交代码前,都会快速扫一眼:

  • 这个文件是不是只改了一个原因?
  • 这个功能要改,是不是只动一个文件?
  • 这个函数是不是在“偷”别人的数据?
  • 这些参数是不是该打包了?

嗯,养成这个习惯,代码质量会提升一大截。你试试看。

公众号:蓝海资料掘金营,微信 deep3321