22、软件维护之代码重构:坏味道识别、重构手法、依赖分析与解耦
代码重构这事儿,说白了就是给软件“做体检”。我干了这么多年逆向和软件维护,见过太多项目因为不敢动代码,最后烂成一锅粥。你想想看,一个系统跑了五六年,没人敢改,没人愿意碰——这就是典型的“技术债”爆了。
今天咱们聊聊重构的核心三板斧:坏味道识别、重构手法、依赖分析与解耦。这三样东西,你掌握了,维护老项目就不慌了。
一、坏味道识别——先得知道哪里臭了
重构的第一步,不是动手改代码,而是先闻一闻。代码坏味道,就像厨房里的馊味,你不找到源头,光喷空气清新剂没用。
我个人习惯把坏味道分成几类,咱们挑最常见的说:
1. 过长函数
一个函数超过50行,我就开始警惕了。超过100行,基本可以断定它做了太多事。我在项目中遇到过一段300行的函数,里面既有数据库查询,又有业务逻辑,还夹杂着日志打印——这玩意儿谁敢动?
典型特征:
- 函数名无法概括其功能(比如叫 processData,实际干了十件事)
- 存在大量注释来解释“这段在干什么”
- 嵌套超过三层 if/for
2. 重复代码
重复代码是万恶之源。我曾经在一个老项目中看到,同样的支付校验逻辑,在三个不同的类里各写了一遍。后来需求变了,改了一个地方,另外两个没改——线上出了事故。
重复代码的识别其实很简单:复制粘贴的痕迹。如果你看到两段代码结构相似,只是变量名不同,那就是坏味道。
3. 过大的类
一个类超过500行,就该拆了。我见过一个“上帝类”,两千多行,什么功能都往里塞。你想加一个新功能,得先花三天搞清楚这个类到底干了什么。
4. 发散式变化
一个类因为不同的原因而频繁修改。比如一个类既处理订单逻辑,又处理用户权限——订单规则变了要改它,权限模型变了也要改它。这就是典型的“耦合过紧”。
5. 霰弹式修改
跟发散式变化相反:一个需求变更,需要修改十几个不同的类。这说明功能分散得太厉害,没有内聚。
我的经验:识别坏味道,别靠肉眼硬看。用工具辅助——SonarQube、PMD、FindBugs 都能帮你扫出大部分问题。但有些坏味道,比如“命名糟糕”、“逻辑混乱”,工具扫不出来,得靠人审。
二、重构手法——动手改,但要稳
识别出坏味道之后,怎么改?我总结了几种最常用的手法,都是实战中反复验证过的。
1. 提取函数(Extract Method)
这是最基础、最安全的重构手法。把一段逻辑独立的代码,提取成一个单独的函数。
// 重构前
public void processOrder(Order order) {
// 验证订单
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("订单金额无效");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("订单项为空");
}
// 处理订单...
}
// 重构后
public void processOrder(Order order) {
validateOrder(order);
// 处理订单...
}
private void validateOrder(Order order) {
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("订单金额无效");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("订单项为空");
}
}
你看,这样一拆,主函数的逻辑就清晰了。验证逻辑单独维护,以后改规则也方便。
2. 移动函数(Move Method)
当一个函数用另一个类的数据比用自己类的数据还多,就该把它搬过去。说白了,函数应该待在它“最亲近”的类里。
3. 引入参数对象(Introduce Parameter Object)
如果一个函数有超过3个参数,而且这些参数经常一起出现,就把它们封装成一个对象。
// 重构前
public void createUser(String name, int age, String email, String phone) {
// ...
}
// 重构后
public void createUser(UserInfo userInfo) {
// ...
}
嗯,这里要注意:别为了封装而封装。如果参数之间没有逻辑关联,硬凑成一个对象反而更糟。
4. 用多态取代条件表达式
看到 switch 或者一堆 if-else 判断类型的时候,就该考虑用多态了。
避坑指南:我曾经在一个项目中,把所有的 if-else 都改成了多态,结果类数量翻了三倍,代码反而更难懂了。多态不是银弹,只有当你确实有“不同类型不同行为”的需求时,才值得用。
三、依赖分析与解耦——老项目的命门
老项目最怕什么?怕牵一发而动全身。你改了一个类,结果十个类跟着报错。这就是依赖关系太复杂了。
1. 依赖分析工具
别靠脑子记依赖关系,人脑记不住。用工具:
| 工具 | 用途 | 我的评价 |
|---|---|---|
| JDepend | 分析包之间的依赖 | 老牌工具,够用 |
| Structure101 | 可视化依赖图 | 收费但好用,我团队一直在用 |
| Dependency-Check | 第三方库依赖安全 | 安全团队必装 |
2. 解耦的核心原则
解耦说白了就一句话:依赖倒置。高层模块不应该依赖低层模块,两者都应该依赖抽象。
我举个例子。你有一个订单服务,直接依赖了 MySQL 数据库。哪天你想换成 PostgreSQL,得改订单服务的代码——这就是耦合。
// 耦合的写法
public class OrderService {
private MySqlDatabase db = new MySqlDatabase();
public void saveOrder(Order order) {
db.insert(order);
}
}
// 解耦的写法
public class OrderService {
private Database db; // 依赖抽象
public OrderService(Database db) {
this.db = db;
}
public void saveOrder(Order order) {
db.insert(order);
}
}
interface Database {
void insert(Order order);
}
你看,这样一改,换数据库只需要换实现类,OrderService 不用动。
3. 循环依赖——必须干掉
A 依赖 B,B 依赖 C,C 又依赖 A——这就是循环依赖。我在项目中遇到过最离谱的循环依赖,涉及7个模块,改一个模块要重新编译整个项目。
解决循环依赖的方法:
- 提取公共依赖:把 A 和 B 都依赖的部分,提取成独立的模块
- 引入接口:让一方依赖接口,而不是具体实现
- 使用事件机制:用发布-订阅模式打破直接依赖
四、知识体系总览
下面这张图,是我自己总结的代码重构知识体系。你看一眼,心里就有谱了。
五、实战建议
说了这么多,最后给几条实在的建议:
- 小步重构:别想着一次把整个项目翻新。每次改一点点,改完就跑测试。我见过太多人想“一步到位”,结果改到一半项目崩了,回退都来不及。
- 测试先行:重构之前,先把测试用例写好。没有测试保护的重构,就是裸奔。我曾经在一个没有单元测试的项目里重构,改完一个函数,三个功能挂了——血的教训。
- 代码评审:重构后的代码,一定要让同事 review 一遍。自己写的代码,自己看不出问题。换个视角,往往能发现隐藏的坑。
- 不要过度设计:重构的目的是让代码更好维护,不是让代码更“优雅”。为了用设计模式而用设计模式,反而把简单问题复杂化了。
我的习惯:每次重构前,我会先画一张依赖图,搞清楚当前模块的上下游关系。然后从“最臭”的模块开始改,改完一个,跑一遍全量测试。这样虽然慢,但稳。
代码重构这事儿,说白了就是“还债”。你欠下的技术债,迟早要还。早还早轻松,拖得越久,利息越高。希望今天聊的这些,能帮你少走一些弯路。