坏味道识别(上):神秘命名、重复代码、过长函数、过长参数列表、全局数据
各位好,今天我们聊聊代码里的“坏味道”。
说实话,我入行头三年,最怕听到的就是“重构”两个字。总觉得代码能跑就行,干嘛要动它?直到有一次,我接手一个遗留系统,打开一个函数,好家伙,800多行。我盯着屏幕看了半小时,愣是没看懂它在干嘛。那天晚上我就在想——如果代码能说话,它一定在骂人。
后来我慢慢明白,坏味道不是bug,但它比bug更致命。bug让你程序挂掉,坏味道让你的团队慢慢死掉。今天我们就来识别五种最常见的坏味道。
核心观点:坏味道是代码中潜在的设计问题信号。它们不会立即导致程序崩溃,但会显著降低可维护性和可扩展性。识别坏味道是重构的第一步,也是最关键的一步。
1. 神秘命名
神秘命名,说白了就是“看不懂”。变量叫 a、b、data,函数叫 doSomething、process。你想想看,这样的代码过两周你自己都看不懂,更别说别人了。
我个人习惯是:命名要能表达“为什么”和“做什么”,而不是“是什么”。比如:
// 坏味道:神秘命名
int d; // 这是啥?天数?折扣?
void process(); // 处理什么?怎么处理?
// 好味道:清晰命名
int discountRate;
void calculateOrderDiscount();
我在项目中遇到过最离谱的命名是一个叫 xxx 的类。问原作者,他说“当时随便起的,后来忘了改”。嗯,这个类后来被重构了三次,每次都要花半小时猜它的用途。
命名检查清单:
- 变量名能否让读者立即知道它的用途?
- 函数名是否描述了它做的事情?
- 类名是否反映了它的职责?
- 命名是否遵循团队约定(驼峰、下划线等)?
2. 重复代码
重复代码是坏味道里的“老油条”。它出现在各种地方:同一个类里的两个方法有相似逻辑、两个子类做类似的事情、甚至不同模块之间复制粘贴。
为什么会这样?说白了就是“复制粘贴一时爽,维护起来火葬场”。
// 坏味道:重复代码
public double calculateEmployeeBonus(Employee emp) {
double base = emp.getSalary() * 0.1;
double extra = emp.getYears() > 5 ? 1000 : 0;
return base + extra;
}
public double calculateManagerBonus(Manager mgr) {
double base = mgr.getSalary() * 0.1;
double extra = mgr.getYears() > 5 ? 1000 : 0;
double mgrExtra = mgr.getTeamSize() * 200;
return base + extra + mgrExtra;
}
// 重构后:提取公共方法
public double calculateBaseBonus(Employee emp) {
double base = emp.getSalary() * 0.1;
double extra = emp.getYears() > 5 ? 1000 : 0;
return base + extra;
}
public double calculateManagerBonus(Manager mgr) {
return calculateBaseBonus(mgr) + mgr.getTeamSize() * 200;
}
我曾经在一个项目里发现同一个SQL查询被复制了7次,分散在5个不同的文件里。后来业务改了查询条件,我们改了3个文件,漏了4个。嗯,线上出了bug才发现的。
避坑指南:我曾经以为“就复制一次没关系”,结果那次复制变成了一个持续3年的技术债。记住:第一次复制是偷懒,第二次复制是习惯,第三次复制就是灾难。
3. 过长函数
过长函数,我个人觉得是代码里最直观的坏味道。一个函数超过20行,你就该警惕了。超过50行,基本可以断定它做了太多事情。
你想想看,一个函数如果做了A、B、C三件事,你改A的时候会不会担心影响B和C?测试的时候要不要把所有路径都测一遍?
// 坏味道:过长函数(伪代码)
public void processOrder(Order order) {
// 1. 验证订单(20行)
// 2. 计算价格(30行)
// 3. 检查库存(15行)
// 4. 更新数据库(25行)
// 5. 发送通知(10行)
// 总共:100行
}
// 重构后:每个函数只做一件事
public void processOrder(Order order) {
validateOrder(order);
calculatePrice(order);
checkInventory(order);
updateDatabase(order);
sendNotification(order);
}
我记得有一次重构一个300行的函数,拆成8个小函数后,发现其中2个函数根本不需要——因为逻辑是重复的。你看,拆开之后问题自然就暴露了。
经验法则:如果一个函数你需要滚动屏幕才能看完,那它一定太长了。如果一个函数你需要写注释来解释它在做什么,那它应该被拆分了。
4. 过长参数列表
参数列表太长,调用者会崩溃。3个参数是极限,4个就该考虑重构了。超过5个?嗯,基本可以断定设计有问题。
// 坏味道:过长参数列表
public void createUser(String name, int age, String email,
String phone, String address, String city,
String country, String zipCode, boolean isActive) {
// ...
}
// 重构后:引入参数对象
public class UserInfo {
private String name;
private int age;
private String email;
private ContactInfo contact;
private boolean isActive;
// getters/setters...
}
public void createUser(UserInfo userInfo) {
// ...
}
我在项目中遇到过最夸张的一个函数有12个参数。调用的时候要按顺序传,传错一个就出bug。后来我们引入了参数对象,代码瞬间清爽了。
参数过多的常见解决方案:
- 引入参数对象:将相关参数封装成一个类
- 拆分为多个函数:不同场景用不同函数
- 使用建造者模式:当参数有大量可选值时
5. 全局数据
全局数据,说白了就是“谁都能改,谁都管不了”。全局变量、单例、静态类变量,这些都是全局数据的变种。
为什么它是个坏味道?因为副作用太难追踪了。你改了一个全局变量,可能影响10个不同的地方。而且这些影响往往是在运行时才暴露的,编译期根本发现不了。
// 坏味道:全局数据
public class Config {
public static String databaseUrl;
public static int maxConnections;
public static boolean debugMode;
}
// 某处代码
Config.databaseUrl = "new_url"; // 谁改的?为什么改?什么时候改的?
// 重构后:封装访问
public class Config {
private static String databaseUrl;
private static int maxConnections;
private static boolean debugMode;
public static String getDatabaseUrl() { return databaseUrl; }
public static void setDatabaseUrl(String url) {
// 可以加日志、校验、通知等
databaseUrl = url;
}
// ...
}
我曾经在一个项目里排查一个诡异的bug,查了整整两天。最后发现是某个模块在初始化时偷偷改了全局的数据库连接池配置。嗯,从那以后,我对全局数据就特别敏感。
避坑指南:我曾经以为“就一个全局变量,没事的”。结果这个“没事的”全局变量,后来被5个不同的模块读写,每次上线都要祈祷不要出问题。记住:全局数据是隐形的耦合,它让代码的依赖关系变得不可见。
小结
今天聊了五种坏味道:神秘命名、重复代码、过长函数、过长参数列表、全局数据。它们就像代码里的“小毛病”,单个看都不致命,但积累起来就能让一个项目瘫痪。
识别坏味道是重构的第一步。你不需要一次性把所有坏味道都消灭,但至少要能“看见”它们。下次你打开一个文件,看到模糊的命名、重复的逻辑、超长的函数,心里就要有个声音在说:“嗯,这里该重构了。”
记住一句话:代码是写给人看的,顺便给机器执行。如果人看不懂,那机器执行得再好也没用。
公众号:蓝海资料掘金营,微信deep3321