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 都依赖的部分,提取成独立的模块
  • 引入接口:让一方依赖接口,而不是具体实现
  • 使用事件机制:用发布-订阅模式打破直接依赖

四、知识体系总览

下面这张图,是我自己总结的代码重构知识体系。你看一眼,心里就有谱了。

代码重构知识体系 坏味道识别 • 过长函数(>50行) • 重复代码 • 过大的类(>500行) • 发散式变化 • 霰弹式修改 • 命名糟糕 • 逻辑混乱 • 过长参数列表 • 基本类型偏执 重构手法 • 提取函数 • 移动函数 • 引入参数对象 • 多态取代条件 • 提取类 • 内联函数 • 拆分循环 • 以查询取代临时变量 • 引入断言 依赖分析与解耦 • 依赖倒置原则 • 接口隔离 • 消除循环依赖 • 提取公共模块 • 事件驱动解耦 • 依赖注入 • 工具:JDepend • 工具:Structure101 • 工具:Dependency-Check 核心目标:提高可维护性,降低修改成本,消除技术债

五、实战建议

说了这么多,最后给几条实在的建议:

  1. 小步重构:别想着一次把整个项目翻新。每次改一点点,改完就跑测试。我见过太多人想“一步到位”,结果改到一半项目崩了,回退都来不及。
  2. 测试先行:重构之前,先把测试用例写好。没有测试保护的重构,就是裸奔。我曾经在一个没有单元测试的项目里重构,改完一个函数,三个功能挂了——血的教训。
  3. 代码评审:重构后的代码,一定要让同事 review 一遍。自己写的代码,自己看不出问题。换个视角,往往能发现隐藏的坑。
  4. 不要过度设计:重构的目的是让代码更好维护,不是让代码更“优雅”。为了用设计模式而用设计模式,反而把简单问题复杂化了。

我的习惯:每次重构前,我会先画一张依赖图,搞清楚当前模块的上下游关系。然后从“最臭”的模块开始改,改完一个,跑一遍全量测试。这样虽然慢,但稳。

代码重构这事儿,说白了就是“还债”。你欠下的技术债,迟早要还。早还早轻松,拖得越久,利息越高。希望今天聊的这些,能帮你少走一些弯路。

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