代码坏味道(上):重复代码、过长函数、过大的类、过长参数列表、发散式变化
各位同学好,今天我们聊聊代码里的「坏味道」。
别误会,不是真的闻到了什么。这是 Martin Fowler 在《重构》里提出的概念——代码中那些暗示着「该动手了」的信号。我做了十几年项目,见过太多代码从「还能忍」变成「彻底崩盘」。其实,崩盘之前,坏味道早就出现了。
核心观点:坏味道不是 bug,但它是 bug 的温床。闻到味道,就该重构了。
今天我们先讲五个最常见的坏味道。嗯,一个一个来。
1. 重复代码
重复代码,说白了就是「Ctrl+C / Ctrl+V」的后遗症。你想想看,同一个逻辑,出现在三个不同的地方。改一个,忘了改另外两个——线上事故就这么来的。
我在项目中遇到过最夸张的一次:一个订单金额计算逻辑,在五个服务里各写了一遍。后来需求变了,改漏了一个,线上对账差了三百多万。嗯,那晚的月亮特别圆。
重复代码的常见形式:
- 完全相同的代码块——直接提取成方法
- 结构相似、细节不同——用参数化方法或模板方法解决
- 不同类中的相似逻辑——考虑提取父类或工具类
我的习惯:写代码时如果发现自己在复制粘贴,立刻停下来。先提取方法,再继续写。别想着「回头再改」——回头永远不会来。
// 坏味道:重复代码
public void saveUser(User user) {
// 校验逻辑
if (user.getName() == null || user.getName().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (user.getAge() < 0 || user.getAge() > 150) {
throw new IllegalArgumentException("年龄不合法");
}
// ... 保存逻辑
}
public void updateUser(User user) {
// 完全相同的校验逻辑
if (user.getName() == null || user.getName().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (user.getAge() < 0 || user.getAge() > 150) {
throw new IllegalArgumentException("年龄不合法");
}
// ... 更新逻辑
}
// 重构后:提取校验方法
private void validateUser(User user) {
if (user.getName() == null || user.getName().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (user.getAge() < 0 || user.getAge() > 150) {
throw new IllegalArgumentException("年龄不合法");
}
}
2. 过长函数
一个函数几百行,你见过吗?我见过。一个函数里塞了数据校验、业务计算、日志打印、发送邮件、更新缓存……什么都干。
为什么会这样?因为写的时候觉得「这样方便啊,不用跳来跳去」。但等你三个月后再看,你根本不知道这个函数到底在干什么。
过长函数的判断标准:
- 一眼看不到函数结束的大括号——太长了
- 函数需要滚动屏幕才能看完——太长了
- 你无法用一句话说清这个函数做了什么——太长了
我曾经踩过的坑:一个 400 行的函数,里面嵌套了 6 层 if-else。我改了一个条件分支,结果引发了三个不相关的 bug。从那以后,我给自己定了个规矩——一个函数超过 30 行,必须拆。
// 坏味道:过长函数
public void processOrder(Order order) {
// 验证订单
if (order == null) throw new IllegalArgumentException("订单不能为空");
if (order.getItems() == null || order.getItems().isEmpty()) throw new IllegalArgumentException("订单项不能为空");
// ... 20行验证逻辑
// 计算价格
double total = 0;
for (OrderItem item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
// ... 还有折扣计算、税费计算
}
// ... 30行计算逻辑
// 保存订单
// ... 40行持久化逻辑
// 发送通知
// ... 20行通知逻辑
}
// 重构后:每个方法只做一件事
public void processOrder(Order order) {
validateOrder(order);
double total = calculateTotal(order);
saveOrder(order, total);
sendNotification(order);
}
private void validateOrder(Order order) { /* ... */ }
private double calculateTotal(Order order) { /* ... */ }
private void saveOrder(Order order, double total) { /* ... */ }
private void sendNotification(Order order) { /* ... */ }
3. 过大的类
过大的类和过长函数是「亲戚」。一个类里塞了太多职责,就成了「上帝类」——什么都知道,什么都管。
我记得有个项目,一个 OrderService 类,里面包含了订单创建、支付处理、库存扣减、物流发货、售后处理……整整 3000 行。每次改这个类,我都要深呼吸三下。
过大的类有什么问题?
- 难以理解——你根本不知道这个类到底负责什么
- 难以测试——一个类依赖太多东西,单元测试写到你崩溃
- 难以复用——你想用其中一部分功能,却不得不依赖整个庞然大物
判断标准:如果你无法用一句话说清这个类的职责,那它一定太大了。一个类应该只有一个「改变的理由」——这就是单一职责原则。
// 坏味道:过大的类
public class OrderService {
public void createOrder() { /* ... */ }
public void processPayment() { /* ... */ }
public void deductInventory() { /* ... */ }
public void arrangeShipping() { /* ... */ }
public void handleReturn() { /* ... */ }
public void sendEmail() { /* ... */ }
public void generateReport() { /* ... */ }
// ... 还有 20 个方法
}
// 重构后:拆分成多个小类
public class OrderCreator { /* 只负责创建订单 */ }
public class PaymentProcessor { /* 只负责支付 */ }
public class InventoryManager { /* 只负责库存 */ }
public class ShippingService { /* 只负责物流 */ }
4. 过长参数列表
一个方法有七八个参数,你写过吗?我写过。当时觉得「这样调用方可以一次性传完所有数据,多方便」。结果呢?调用方每次都要翻文档,看看第几个参数是什么类型。
过长参数列表的问题:
- 调用方容易搞错参数顺序
- 新增参数要改所有调用处
- 方法签名太长,可读性极差
我的建议:参数超过 3 个,就考虑封装成对象。别觉得「多写一个类麻烦」——后面改代码的时候你就知道值了。
// 坏味道:过长参数列表
public void createUser(String name, int age, String email, String phone,
String address, String city, String province, String zipCode) {
// ...
}
// 重构后:封装参数对象
public class UserInfo {
private String name;
private int age;
private String email;
private String phone;
private Address address;
// getter/setter
}
public class Address {
private String street;
private String city;
private String province;
private String zipCode;
// getter/setter
}
public void createUser(UserInfo userInfo) {
// ...
}
5. 发散式变化
这个名字有点绕,但意思很简单:一个类,因为不同的原因被频繁修改。
举个例子:你的 ReportGenerator 类,今天因为数据源变了要改,明天因为报表格式变了要改,后天因为输出方式变了又要改。这就是发散式变化——一个类承担了太多「变化轴」。
发散式变化的危害:
- 改一个功能可能影响其他不相关的功能
- 代码冲突频繁——多人同时改同一个类
- 测试回归成本高——每次改完都要跑全量测试
我曾经踩过的坑:一个数据导出类,同时处理数据库查询、Excel 格式生成、邮件发送。后来邮件服务换了供应商,我改了一行配置,结果 Excel 的列顺序全乱了。为什么?因为三个逻辑混在一起,改邮件配置不小心动了全局变量。
// 坏味道:发散式变化
public class ReportService {
public void generateReport() {
// 1. 从数据库查询数据
// 2. 生成 Excel 格式
// 3. 发送邮件
// 4. 记录日志
}
}
// 重构后:每个变化轴独立
public class DataFetcher { /* 只负责数据查询 */ }
public class ExcelFormatter { /* 只负责格式生成 */ }
public class EmailSender { /* 只负责邮件发送 */ }
public class ReportService {
private DataFetcher fetcher;
private ExcelFormatter formatter;
private EmailSender sender;
public void generateReport() {
var data = fetcher.fetch();
var file = formatter.format(data);
sender.send(file);
}
}
知识体系总览
下面这张图,帮你把今天讲的五个坏味道串起来。你可以看到它们之间的关联,以及对应的重构方向。
小结
今天这五个坏味道,你可以在自己的代码里找找看。不用一次改完,但至少要知道——闻到味道了,就别假装没闻到。
我个人习惯是:每次提交代码前,快速扫一眼自己改过的文件。如果发现重复代码、过长函数,顺手就重构了。别等「以后」——以后只会更糟。
下一节我们继续讲剩下的五个坏味道。嗯,到时候你会发现,很多问题其实是「连锁反应」——一个坏味道会引出另一个。
一句话总结:代码和人一样,有味道就要处理。拖得越久,代价越大。