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...
}
知识体系总览
下面这张图,帮你快速回顾这四种坏味道的核心要点:
这四种坏味道,其实都有一个共同点:它们都在增加代码的「认知负荷」。你读代码的时候,需要花更多精力去理解它本来的意图。重构的目的,就是把这些额外的负担去掉,让代码回归简单、清晰。
嗯,这一章就聊到这里。下一章我们继续聊剩下的几种坏味道——相信我,它们同样「精彩」。