3. 重构基础手法
重构这件事,说白了就是「在不改变外部行为的前提下,改善内部结构」。听起来简单,做起来全是细节。我这些年带团队,发现很多新人一上来就想搞大重构,结果改得面目全非。其实真正的高手,都是从最基础的六种手法练起的。
今天我就把这六招拆开揉碎了讲给你听。它们分别是:提炼函数、内联函数、提炼变量、内联变量、改变函数声明、封装变量。嗯,这六招学会了,你就能应付80%的重构场景。
核心原则:每次只做一种重构,做完立刻编译测试。别想着一步到位,那是给自己挖坑。
3.1 提炼函数:把代码块变成有名字的「小工具」
这是我最常用的手法,没有之一。你想想看,一个函数如果超过20行,读起来就开始费劲了。超过50行?那基本就是「面条代码」了。
什么时候该提炼? 当你看到一段代码需要写注释才能看懂的时候,就该提炼了。注释的名字,就是新函数的名字。
// 重构前:一个函数干了三件事
void processOrder(Order& order) {
// 验证订单
if (order.items.empty()) throw std::runtime_error("空订单");
if (order.total <= 0) throw std::runtime_error("金额无效");
// 计算折扣
double discount = 0.0;
if (order.total > 1000) discount = 0.1;
else if (order.total > 500) discount = 0.05;
// 生成发票
Invoice inv;
inv.id = generateId();
inv.amount = order.total * (1 - discount);
inv.customer = order.customer;
saveInvoice(inv);
}
// 重构后:每个函数只做一件事
void processOrder(Order& order) {
validateOrder(order);
double discount = calculateDiscount(order);
generateInvoice(order, discount);
}
void validateOrder(const Order& order) {
if (order.items.empty()) throw std::runtime_error("空订单");
if (order.total <= 0) throw std::runtime_error("金额无效");
}
double calculateDiscount(const Order& order) {
if (order.total > 1000) return 0.1;
if (order.total > 500) return 0.05;
return 0.0;
}
我的习惯:提炼函数时,我一般会先写调用代码,再写函数实现。这样能保证新函数的接口是「好用」的。另外,函数名用动词开头,比如 validateOrder、calculateDiscount,读起来就像在讲故事。
3.2 内联函数:有时候「拆太碎」也是病
提炼函数的反向操作就是内联函数。你可能会问:「好不容易拆出来的,为什么要合并回去?」
我在项目中遇到过这种情况:一个函数体只有一行代码,而且只被调用了一次。这种函数其实没有存在的必要,反而增加了调用开销和阅读跳转。这时候就该内联。
// 重构前:过度提炼
int getRating() {
return moreThanFiveLateDeliveries() ? 2 : 1;
}
bool moreThanFiveLateDeliveries() {
return _numberOfLateDeliveries > 5;
}
// 重构后:直接内联
int getRating() {
return _numberOfLateDeliveries > 5 ? 2 : 1;
}
注意:内联函数只适用于「函数体简单且调用点少」的情况。如果函数有多个调用点,或者函数体逻辑复杂,强行内联反而会降低可读性。
3.3 提炼变量:给「魔法数字」和「复杂表达式」起名字
代码里最让人头疼的就是「魔法数字」和「长表达式」。比如 if (price * 0.95 > 1000),这 0.95 是什么?95折?还是什么别的?
提炼变量就是给这些「无名值」起个有意义的变量名。
// 重构前:看不懂的表达式
double total = quantity * itemPrice;
double finalPrice = total - max(0, quantity - 500) * itemPrice * 0.05;
finalPrice = min(finalPrice, total * 0.9);
// 重构后:一目了然
double basePrice = quantity * itemPrice;
double quantityDiscount = max(0, quantity - 500) * itemPrice * 0.05;
double discountedPrice = basePrice - quantityDiscount;
double seasonalDiscount = basePrice * 0.1;
double finalPrice = min(discountedPrice, basePrice - seasonalDiscount);
避坑指南:我曾经在重构一个金融计算模块时,提炼了十几个变量。结果发现有些变量只在中间用了一次,反而增加了阅读负担。后来我总结:变量提炼的原则是「要么多次使用,要么能显著提升可读性」。一次性的中间变量,不如直接内联。
3.4 内联变量:去掉「多余的中间人」
内联变量是提炼变量的反向操作。当一个变量只被赋值一次,而且表达式本身比变量名更清晰时,就该内联。
// 重构前:多余的变量
bool isEligible = customer.age >= 18 && customer.creditScore > 700;
if (isEligible) { ... }
// 重构后:直接使用表达式
if (customer.age >= 18 && customer.creditScore > 700) { ... }
你想想看,isEligible 这个名字其实没有比原表达式提供更多信息。而且多了一层变量,读代码时还得跳转一下。这种变量就该内联掉。
3.5 改变函数声明:给函数「改名」或「改参数」
函数名就是最好的注释。如果函数名不能准确表达其功能,那就是在误导读者。我见过一个叫 processData 的函数,里面居然在发送网络请求。这名字起得也太「敷衍」了。
改变函数声明包括:改名、增删参数、改变参数顺序。
// 重构前:名字太模糊
void process(const std::string& input);
// 重构后:名字说清楚做什么
void validateEmail(const std::string& email);
| 重构场景 | 做法 | 示例 |
|---|---|---|
| 函数名不准确 | 改名 | doStuff() → calculateTax() |
| 参数过多 | 封装为结构体 | func(a,b,c,d,e) → func(Config) |
| 参数顺序不合理 | 调整顺序 | 先输入后输出 |
| 返回值不明确 | 使用输出参数或返回结构体 | int func() → Result func() |
注意:改变函数声明会影响所有调用方。如果是公共API,建议先标记为 [[deprecated]],给调用方一个过渡期。我吃过这个亏,一次改了十几个调用点,结果漏了一个,编译过了但运行时报错。
3.6 封装变量:把「裸数据」藏起来
全局变量、公有成员变量,这些都是代码里的「定时炸弹」。你永远不知道谁在什么时候修改了它。封装变量就是给这些变量加上 getter/setter,把访问控制权收回来。
// 重构前:直接暴露
class Employee {
public:
std::string name;
double salary;
};
// 重构后:封装起来
class Employee {
public:
const std::string& getName() const { return name_; }
void setName(const std::string& name) { name_ = name; }
double getSalary() const { return salary_; }
void setSalary(double salary) {
if (salary < 0) throw std::invalid_argument("工资不能为负");
salary_ = salary;
}
private:
std::string name_;
double salary_;
};
封装的好处不仅仅是「隐藏数据」。你可以在 setter 里加校验逻辑、触发事件、记录日志。这些在裸数据上是做不到的。
我的建议:不要一上来就写 getter/setter。先让变量是公有的,等确实需要控制访问时再封装。过早封装反而会增加不必要的代码量。这叫「按需封装」,不是「为了封装而封装」。
小结:六招怎么选?
这六种手法其实是一对一对的:
- 提炼函数 ↔ 内联函数:控制函数的粒度
- 提炼变量 ↔ 内联变量:控制变量的粒度
- 改变函数声明:优化接口设计
- 封装变量:保护数据安全
实际项目中,我一般会先看函数长度。超过20行就提炼。然后看变量命名。看不懂就提炼。最后看接口设计。不合理就改声明。嗯,这套流程走下来,代码质量至少提升一个档次。
记住:重构不是一次性的「大扫除」,而是日常的「小整理」。每次改一点,积少成多,代码就会越来越干净。