2、代码坏味道(上):神秘命名、重复代码、过长函数、过长参数列表

好,咱们正式开始聊代码的坏味道。

说实话,我刚开始写代码那几年,根本不知道什么叫「坏味道」。代码能跑,功能上线,就觉得万事大吉。直到有一次,我接手了一个遗留系统——那个项目里有个函数叫 processData(),你猜它干了什么?它既处理订单、又发送邮件、还更新库存。我花了整整两天才理清楚它的逻辑。嗯,从那天起,我彻底明白了:代码是写给人看的,顺便给机器执行

这一章,我们先聊四个最常见的坏味道。它们就像代码里的「感冒症状」——看着不致命,但拖着不治,迟早会发展成肺炎。

神秘命名:名字是代码的第一张脸

我个人习惯,给变量、函数、类起名字,至少花 30 秒想一想。为什么?因为名字是代码的第一张脸。你想想看,一个叫 d 的变量,和一个叫 daysSinceLastLogin 的变量,哪个更容易理解?

我在项目中遇到过最离谱的命名,是一个叫 a 的类。对,就一个字母 a。打开一看,里面全是静态工具方法。我当时真想问问原作者:你是怕别人看懂你的代码吗?

核心原则: 名字应该回答三个问题——它是什么?它做什么?为什么存在?

好的命名,不需要注释。举个例子:

// 坏味道
int d; // 天数

// 好味道
int daysSinceLastLogin;

你看,后者根本不需要注释。名字本身就是文档。

我的小技巧: 如果你发现一个名字需要写注释才能解释清楚,那说明名字起得不够好。试着换个更具体的词。

重复代码:复制粘贴一时爽,维护火葬场

重复代码,说白了就是「复制粘贴」的后遗症。我见过最夸张的一个项目,同一个校验逻辑在五个不同的地方各写了一遍。后来业务规则变了,改代码的人漏掉了其中两处——线上出了事故,大家加班到凌晨三点。

为什么会这样?因为人脑不擅长记住「我在哪里写过同样的东西」。你想想看,三个月后你回来改代码,你能记得住所有重复的地方吗?

重复代码有几种常见形态:

  • 完全相同的代码块:直接提取成函数
  • 结构相似、细节不同:用参数化方法或模板方法解决
  • 不同类中的相似逻辑:考虑提取到父类或工具类

举个例子:

// 坏味道:重复代码
public void printUserInfo(User user) {
    System.out.println("Name: " + user.getName());
    System.out.println("Age: " + user.getAge());
    System.out.println("Email: " + user.getEmail());
}

public void printAdminInfo(Admin admin) {
    System.out.println("Name: " + admin.getName());
    System.out.println("Age: " + admin.getAge());
    System.out.println("Email: " + admin.getEmail());
}

// 好味道:提取公共方法
public void printPersonInfo(Person person) {
    System.out.println("Name: " + person.getName());
    System.out.println("Age: " + person.getAge());
    System.out.println("Email: " + person.getEmail());
}
我曾经踩过的坑: 有一次我「偷懒」,觉得两段代码「差不多」,就没去合并。结果后来需求变了,两段代码需要不同的修改。我改完一处,忘了另一处——测试用例跑挂了,我才发现。从那以后,我见到重复代码就像见到蟑螂一样,必须立刻消灭。

过长函数:一个函数只做一件事

过长函数,是代码坏味道里最顽固的一种。我见过一个函数,足足 800 行。它干了什么?它干了所有事——读取文件、解析数据、校验格式、写入数据库、发送通知、记录日志……你管这叫一个函数?这分明是一个「程序」。

我个人习惯,一个函数不超过 20 行。超过 20 行,我就开始问自己:这个函数是不是做了太多事?

为什么函数要短?因为短函数容易理解、容易测试、容易复用。你想想看,一个 800 行的函数,你怎么写单元测试?你根本没法覆盖所有分支。

重构过长函数的核心手法是「提取方法」:

// 坏味道:过长函数
public void processOrder(Order order) {
    // 验证订单
    if (order.getItems().isEmpty()) {
        throw new IllegalArgumentException("订单不能为空");
    }
    if (order.getTotalPrice() <= 0) {
        throw new IllegalArgumentException("订单金额必须大于0");
    }
    
    // 计算折扣
    double discount = 0;
    if (order.getTotalPrice() > 1000) {
        discount = 0.1;
    } else if (order.getTotalPrice() > 500) {
        discount = 0.05;
    }
    
    // 保存订单
    double finalPrice = order.getTotalPrice() * (1 - discount);
    order.setFinalPrice(finalPrice);
    orderRepository.save(order);
    
    // 发送通知
    emailService.sendOrderConfirmation(order);
}

// 好味道:拆分成小函数
public void processOrder(Order order) {
    validateOrder(order);
    double discount = calculateDiscount(order);
    saveOrder(order, discount);
    sendNotification(order);
}

private void validateOrder(Order order) {
    if (order.getItems().isEmpty()) {
        throw new IllegalArgumentException("订单不能为空");
    }
    if (order.getTotalPrice() <= 0) {
        throw new IllegalArgumentException("订单金额必须大于0");
    }
}

private double calculateDiscount(Order order) {
    if (order.getTotalPrice() > 1000) return 0.1;
    if (order.getTotalPrice() > 500) return 0.05;
    return 0;
}

private void saveOrder(Order order, double discount) {
    double finalPrice = order.getTotalPrice() * (1 - discount);
    order.setFinalPrice(finalPrice);
    orderRepository.save(order);
}

private void sendNotification(Order order) {
    emailService.sendOrderConfirmation(order);
}
记住: 每个函数只做一件事。如果你不能用一句话说清楚这个函数在干什么,那它大概率需要拆分了。

过长参数列表:参数越多,调用越痛苦

过长参数列表,说白了就是「参数地狱」。我见过一个函数,参数列表有 12 个参数。每次调用它,你都得小心翼翼地数着顺序,生怕传错一个。

为什么会这样?因为设计者把本该属于一个对象的属性,拆成了一个个独立的参数。你想想看,一个「用户」有姓名、年龄、邮箱、地址……这些信息天然应该放在一个 User 对象里,而不是拆成四个参数传进去。

重构过长参数列表的常见手法:

  • 引入参数对象:把多个相关参数封装成一个对象
  • 保留整个对象:如果调用方已经有这个对象,直接传对象而不是拆开
  • 移除标记参数:用两个独立方法代替一个带布尔参数的方法

举个例子:

// 坏味道:过长参数列表
public void createUser(String name, int age, String email, String phone, 
                       String address, String city, String country, String zipCode) {
    // ...
}

// 好味道:引入参数对象
public void createUser(UserInfo userInfo) {
    // ...
}

// UserInfo 对象
public class UserInfo {
    private String name;
    private int age;
    private String email;
    private String phone;
    private Address address;
    // getters...
}

public class Address {
    private String street;
    private String city;
    private String country;
    private String zipCode;
    // getters...
}
我的经验: 如果一个函数的参数超过 3 个,我就开始警惕了。超过 5 个,基本可以确定需要重构。参数越多,调用方越容易出错,测试用例也越难写。

知识体系总览

下面这张图,帮你快速回顾这四种坏味道的核心要点:

代码坏味道(上):四种核心坏味道 代码坏味道 神秘命名 名字要回答三个问题: 它是什么? 它做什么? 为什么存在? 重复代码 提取公共方法 参数化 / 模板方法 过长函数 核心手法:提取方法 每个函数不超过20行 一个函数只做一件事 过长参数列表 引入参数对象 参数不超过3个

这四种坏味道,其实都有一个共同点:它们都在增加代码的「认知负荷」。你读代码的时候,需要花更多精力去理解它本来的意图。重构的目的,就是把这些额外的负担去掉,让代码回归简单、清晰。

嗯,这一章就聊到这里。下一章我们继续聊剩下的几种坏味道——相信我,它们同样「精彩」。


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