如何重构包含大量全局变量的遗留代码?
60
0
0
0
问题:老模块全局变量满天飞,修改外部状态如履薄冰,怎么办?
最近在重构一个老旧的业务模块,发现代码里充斥着各种全局变量和函数对外部状态的直接修改。每次改动都如履薄冰,生怕引入新的 bug。有什么具体的方法能让这种“牵一发而动全身”的情况得到改善吗?我需要的是实打实的代码实践指导,而不是泛泛而谈的理论。
回答:逐步拆解全局状态,拥抱函数式编程思想
你的问题非常典型,很多老项目都存在类似的问题。全局变量和副作用是代码维护的噩梦,重构时需要特别小心。下面我提供一些实战指导,希望能帮助你逐步改善这种情况:
1. 识别和隔离全局变量:
- 第一步:梳理。花时间梳理模块中的所有全局变量,列出它们的用途、影响范围以及被哪些函数访问和修改。可以使用代码搜索工具(如
grep)来辅助查找。 - 第二步:封装。将全局变量封装成类的静态成员变量或单例模式。这样至少可以控制访问方式,避免随意修改。
```java
// 原始代码
public static int globalCounter = 0;
public static void incrementCounter() {
globalCounter++;
}
// 封装后的代码
public class Counter {
private static int counter = 0;
public static int getCounter() {
return counter;
}
public static void incrementCounter() {
counter++;
}
}
```
2. 消除副作用:
- 识别副作用。找出所有修改外部状态的函数。这些函数通常会修改全局变量、写入文件、发送网络请求等。
- 尽可能使用纯函数。纯函数是指没有副作用的函数,即给定相同的输入,总是返回相同的输出,并且不会修改任何外部状态。将带有副作用的函数重构为纯函数。
```java
// 原始代码(有副作用)
public static void updateUserStatus(User user, String status) {
user.setStatus(status); // 修改了 user 对象的状态
database.update(user); // 修改了数据库
}
// 重构后的代码(尽可能消除副作用)
public static User createUpdatedUser(User user, String status) {
User newUser = user.clone(); // 克隆 user 对象
newUser.setStatus(status);
return newUser;
}
// 调用方负责更新数据库
User updatedUser = createUpdatedUser(user, "active");
database.update(updatedUser);
```
- 使用不可变数据结构。如果必须修改状态,尽量使用不可变数据结构。每次修改都创建一个新的对象,而不是修改原始对象。这可以避免意外的副作用。Java 中可以使用
ImmutableList、ImmutableMap等 Guava 库提供的不可变集合。
3. 依赖注入:
- 避免硬编码依赖。不要在函数内部直接创建依赖的对象,而是通过参数传递进来。这可以提高代码的可测试性和可维护性。
```java
// 原始代码(硬编码依赖)
public class UserService {
private Database database = new Database();
public User getUser(int id) {
return database.getUserById(id);
}
}
// 重构后的代码(依赖注入)
public class UserService {
private Database database;
public UserService(Database database) {
this.database = database;
}
public User getUser(int id) {
return database.getUserById(id);
}
}
// 使用时注入依赖
Database database = new Database();
UserService userService = new UserService(database);
```
4. 控制状态变化:
- 使用状态管理模式。如果状态变化比较复杂,可以考虑使用状态模式、有限状态机等设计模式来控制状态变化。
- 事件驱动架构。将状态变化抽象成事件,使用事件总线来传递事件。这可以解耦状态变化和业务逻辑。
5. 测试先行:
- 编写单元测试。在重构之前,先为现有的代码编写单元测试。这可以确保重构后的代码仍然能够正常工作。
- 持续集成。使用持续集成工具,每次提交代码都自动运行单元测试。这可以尽早发现问题。
重要提示:
- 循序渐进:不要试图一次性解决所有问题。每次只重构一小部分代码,并确保每次重构后代码仍然能够正常工作。
- 代码审查:请同事帮忙审查你的代码。这可以帮助你发现潜在的问题。
- 文档记录:记录你的重构过程和遇到的问题。这可以帮助你和其他人理解代码。
重构是一个漫长的过程,需要耐心和毅力。希望这些建议能帮助你成功地重构你的老模块。记住,关键在于逐步拆解全局状态,拥抱函数式编程思想,并始终保持测试先行。