临时字段(Temporary Field):坏味道识别与重构实战指南
24种代码坏味道系列 · 第16篇
1. 开篇场景
你是否遇到过这样的类:某些字段只在特定方法中使用,大部分时间为空或无效,就像房间里有一些"临时家具",只在特定场合使用,平时占地方?
cpp
class BadExample {
std::string name;
int age;
// 临时字段:只在 calculateTax 方法中使用
double monthlyIncome;
double monthlyExpense;
bool isTaxCalculationMode;
double calculateTax(double income, double expense) {
isTaxCalculationMode = true;
monthlyIncome = income;
monthlyExpense = expense;
// ... 计算逻辑
isTaxCalculationMode = false;
monthlyIncome = 0;
monthlyExpense = 0;
}
};
这就是临时字段的典型症状。类的某些字段只在特定情况下使用,大部分时间为空或无效,增加了类的复杂度,使得类的职责不清晰。
当你需要理解类时,你必须理解这些临时字段的用途。当你需要修改类时,你必须在多个字段中查找。这种设计使得代码变得复杂,增加了维护的难度。
2. 坏味道定义
临时字段是指类的某些字段只在特定情况下使用,大部分时间为空或无效。
就像房间里的临时家具,只在特定场合使用,平时占地方。
核心问题:字段应该始终有效。如果字段只在特定情况下使用,应该提取到独立的类或使用参数对象。这样可以减少类的复杂度,提高代码的可读性和可维护性。
3. 识别特征
🔍 代码表现:
- 特征1:类的某些字段只在特定方法中使用
- 特征2:字段大部分时间为空或无效
- 特征3:字段需要在使用前设置,使用后重置
- 特征4:字段之间有逻辑关联,但只在特定情况下使用
- 特征5:删除字段后,只有特定方法受影响
🎯 出现场景:
- 场景1:快速开发时,将临时数据存储在类字段中
- 场景2:重构不彻底,只修改了部分代码
- 场景3:缺乏设计,没有考虑字段的生命周期
- 场景4:从过程式编程迁移到面向对象时,没有重构字段
💡 快速自检:
- 问自己:这个字段是否只在特定方法中使用?
- 问自己:这个字段是否大部分时间为空或无效?
- 工具提示:使用代码分析工具检测临时字段的使用情况
4. 危害分析
🚨 维护成本:需要理解临时字段的用途,时间成本增加40%
⚠️ 缺陷风险:临时字段可能被误用,bug风险增加30%
🧱 扩展障碍:添加新功能时不知道应该使用哪些字段
🤯 认知负担:需要理解临时字段的存在原因,增加了心理负担
5. 重构实战
步骤1:安全准备
- ✅ 确保有完整的单元测试覆盖
- ✅ 创建重构分支:
git checkout -b refactor/extract-temporary-fields - ✅ 使用版本控制,便于回滚
步骤2:逐步重构
重构前(问题代码)
cpp
// 坏味道:某些字段只在特定方法中使用
class BadExample {
private:
std::string name;
int age;
std::string address;
// 临时字段:只在 calculateTax 方法中使用
double monthlyIncome;
double monthlyExpense;
bool isTaxCalculationMode; // 标记是否在计算税务
public:
BadExample(const std::string& n, int a, const std::string& addr)
: name(n), age(a), address(addr),
monthlyIncome(0), monthlyExpense(0), isTaxCalculationMode(false) {}
void displayInfo() {
// 这些方法不使用临时字段
std::cout << "Name: " << name << ", Age: " << age
<< ", Address: " << address << std::endl;
}
// 只有这个方法使用临时字段
double calculateTax(double income, double expense) {
isTaxCalculationMode = true;
monthlyIncome = income;
monthlyExpense = expense;
double taxable = monthlyIncome - monthlyExpense;
double tax = taxable * 0.2;
// 计算完后,这些字段又变成无效的了
isTaxCalculationMode = false;
monthlyIncome = 0;
monthlyExpense = 0;
return tax;
}
};
问题分析:
monthlyIncome、monthlyExpense、isTaxCalculationMode只在calculateTax方法中使用- 这些字段大部分时间为空或无效
- 增加了类的复杂度,使得类的职责不清晰
重构后(清洁版本)
cpp
// ✅ 提取到独立的类
class TaxCalculator {
private:
double monthlyIncome;
double monthlyExpense;
public:
TaxCalculator(double income, double expense)
: monthlyIncome(income), monthlyExpense(expense) {}
double calculate() {
double taxable = monthlyIncome - monthlyExpense;
return taxable * 0.2;
}
};
class GoodExample {
private:
std::string name;
int age;
std::string address;
public:
GoodExample(const std::string& n, int a, const std::string& addr)
: name(n), age(a), address(addr) {}
void displayInfo() {
std::cout << "Name: " << name << ", Age: " << age
<< ", Address: " << address << std::endl;
}
// ✅ 使用独立的计算器类,不需要临时字段
double calculateTax(double income, double expense) {
TaxCalculator calculator(income, expense);
return calculator.calculate();
}
};
关键变化点:
-
提取类(Extract Class):
- 将临时字段提取到
TaxCalculator类中 - 临时字段变成类的正常字段
- 将临时字段提取到
-
简化类:
GoodExample类不再包含临时字段- 类的职责更清晰
-
提高可维护性:
- 临时字段的生命周期更清晰
- 代码更易读、更易理解
步骤3:重构技巧总结
使用的重构手法:
- 提取类(Extract Class):将临时字段提取到独立的类中
- 引入参数对象(Introduce Parameter Object):将临时字段组合成参数对象
注意事项:
- ⚠️ 确保提取的类有清晰的职责
- ⚠️ 如果临时字段只在局部使用,考虑使用局部变量
- ⚠️ 重构后要更新所有使用处,确保行为一致
6. 预防策略
🛡️ 编码时:
-
即时检查:
- 字段是否只在特定方法中使用?
- 字段是否大部分时间为空或无效?
- 使用IDE的代码分析工具,检测临时字段的使用情况
-
小步提交:
- 发现临时字段时,立即提取到独立的类
- 使用"提取类"重构,保持类的职责清晰
🔍 Code Review清单:
-
重点检查:
- 是否有只在特定方法中使用的字段?
- 字段是否大部分时间为空或无效?
- 是否可以提取到独立的类?
-
拒绝标准:
- 字段只在特定方法中使用
- 字段需要在使用前设置,使用后重置
- 字段增加了类的复杂度但没有带来价值
⚙️ 自动化防护:
-
IDE配置:
- 使用代码分析工具检测临时字段的使用情况
- 启用字段使用警告
-
CI/CD集成:
- 在CI流水线中集成代码分析工具
- 检测临时字段使用情况,生成警告报告
下一篇预告:过长的消息链(Message Chains)- 如何简化过长的调用链