代码坏味道识别:重复代码、过长函数、过大的类、过长参数列表、发散式变化、霰弹式修改

大家好,我是你们的老朋友。今天我们来聊聊重构路上最基础、也最关键的一步——识别代码坏味道。

说实话,我见过太多团队一上来就撸起袖子改代码,结果越改越乱。为什么?因为他们没搞清楚「病根」在哪。代码坏味道,就是病根。你想想看,一个医生连病人哪里疼都不知道,怎么开药方?

这一节,我带你逐个拆解6种最常见的坏味道。每种我都会结合项目里的真实案例来讲。嗯,咱们开始吧。

核心观点:坏味道不是bug,但它会「孕育」bug。识别坏味道,是重构的第一步,也是最重要的一步。

代码坏味道识别 · 知识体系 坏味道 识别与重构 重复代码 DRY原则 过长函数 提取方法 过大的类 单一职责 过长参数 参数对象 发散式变化 拆分职责 霰弹式修改 集中变化 6种坏味道 · 每种都有对应的重构手法

1. 重复代码:最臭的味道

重复代码,说白了就是「Ctrl+C / Ctrl+V」留下的后遗症。我个人习惯,看到重复代码就像看到鞋底的口香糖——必须立刻清理掉。

我在项目中遇到过最夸张的一次:一个支付模块里,同样的金额校验逻辑复制了7次。后来需求变了,要加一个手续费计算。你猜怎么着?改漏了3处,线上出了P0事故。

避坑指南:我曾经以为「就复制一次,没事的」。结果那次事故让我明白:重复代码不是「省事」,是「埋雷」。

怎么识别?很简单:

  • 看到两段代码长得像双胞胎
  • 一个函数里出现多次相同的逻辑片段
  • 不同类里有相似的方法

重构手法:提取函数、提取基类、使用模板方法模式。

2. 过长函数:一个函数写500行?

我见过最长的函数,足足800行。一个函数里干了三件事:解析配置、处理业务逻辑、还要写日志。你想想看,这样的函数谁敢动?

过长函数的判断标准其实很主观。我个人习惯:一个函数超过20行,我就开始警惕了。超过50行,基本可以断定它「太长了」。

小技巧:如果你给一个函数起名字时,需要用「并且」连接两个动词(比如「解析配置并且处理订单」),那它就该拆了。

重构手法:提取方法、以查询取代临时变量、分解条件表达式。

// 坏味道示例:一个函数干了太多事
void processOrder(Order& order) {
    // 1. 验证订单
    if (order.items.empty()) throw std::runtime_error("empty order");
    // 2. 计算价格
    double total = 0;
    for (auto& item : order.items) total += item.price * item.quantity;
    // 3. 应用折扣
    if (order.coupon != "") total *= 0.9;
    // 4. 更新库存
    for (auto& item : order.items) inventory[item.id] -= item.quantity;
    // 5. 发送通知
    email.send("order processed");
}

// 重构后:每个函数只做一件事
void processOrder(Order& order) {
    validateOrder(order);
    double total = calculateTotal(order);
    applyDiscount(order, total);
    updateInventory(order);
    sendNotification(order);
}

3. 过大的类:一个类塞了太多职责

过大的类和过长函数是「近亲」。一个类里塞了数据访问、业务逻辑、UI渲染、日志记录……嗯,这已经不是类了,这是「上帝对象」。

我记得有个项目,一个叫 OrderManager 的类,足足有3000行。里面什么都有:订单CRUD、支付处理、物流跟踪、甚至还有发送邮件的方法。后来要加一个「订单导出」功能,我光是找到该在哪个文件里改,就花了半天。

怎么判断一个类「过大」?

  • 类的名字里出现「Manager」「Util」「Helper」——警惕!
  • 类的注释超过3段
  • 你无法在30秒内说清楚这个类「到底负责什么」

重构手法:提取类、提取接口、使用委托。

4. 过长参数列表:函数签名比函数体还长

这个我太有感触了。有一次接手一个老项目,看到一个函数签名是这样的:

void createReport(
    const std::string& title,
    const std::string& author,
    const std::string& department,
    int year, int month, int day,
    bool includeSummary,
    bool includeCharts,
    bool includeAppendix,
    const std::string& outputPath
);

10个参数!调用的时候,你根本记不住第5个参数是 year 还是 month。更可怕的是,后来要加一个 includeFooter 参数,所有调用处都得改一遍。

经验之谈:我个人习惯,参数超过3个,就考虑封装成结构体或类。超过5个,必须重构。

重构手法:引入参数对象、保留整个对象、以查询取代参数。

5. 发散式变化:一个类因为多种原因被修改

发散式变化,说白了就是「一个类,多个老板」。今天因为数据库改了要改它,明天因为UI改了也要改它,后天因为业务规则改了还得改它。

为什么会这样?因为这个类承担了太多职责。每个「变化原因」对应一个职责。如果一个类有3个变化原因,那它就该拆成3个类。

我举个例子:一个 Invoice 类,既要计算金额,又要格式化输出,还要持久化到数据库。哪天数据库从MySQL换成PostgreSQL,你改的是 Invoice 类。哪天输出格式从PDF换成HTML,你改的还是 Invoice 类。这不合理。

重构手法:提取类、拆分模块、使用策略模式。

6. 霰弹式修改:改一个功能,要改N个地方

霰弹式修改和发散式变化是「双胞胎」,但方向相反。

  • 发散式变化:一个类因为多个原因被改
  • 霰弹式修改:一个原因导致多个类被改

举个例子:你要改一个「用户注册成功后发送欢迎邮件」的逻辑。结果发现,你需要在 UserService、RegistrationHandler、EmailService、甚至 AuditLog 里都加上相关代码。改一个功能,要改4个文件。这就是霰弹式修改。

避坑指南:我曾经在一个项目中,为了加一个「用户注销」功能,改了7个文件。改完之后,测试跑了3天。后来我重构了,把所有注销相关的逻辑集中到一个 UserDeactivationService 里。再改,只改一个文件就够了。

重构手法:搬移函数、搬移字段、将类内联化(把分散的逻辑集中到一个类里)。

总结一下

这6种坏味道,其实可以分成两类:

类别 坏味道 核心问题 重构方向
代码内部 重复代码 复制粘贴 提取复用
过长函数 职责过多 拆分函数
过大的类 职责过多 拆分/提取类
变化相关 过长参数列表 参数爆炸 封装参数
发散式变化 一个类多个变化原因 拆分职责
霰弹式修改 一个变化改多处 集中变化点

识别坏味道,说白了就是培养「代码嗅觉」。你闻着不对劲的地方,往往就是需要重构的地方。别怕改代码,怕的是不知道为什么要改、改哪里。

嗯,这一节就到这里。记住:坏味道不可怕,可怕的是闻到了还装作没闻到。

我的建议:每次提交代码前,花5分钟扫一眼自己的代码。问问自己:有没有重复?函数是不是太长了?参数是不是太多了?养成习惯,坏味道会越来越少。

可可精品资料库