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行就提炼。然后看变量命名。看不懂就提炼。最后看接口设计。不合理就改声明。嗯,这套流程走下来,代码质量至少提升一个档次。

记住:重构不是一次性的「大扫除」,而是日常的「小整理」。每次改一点,积少成多,代码就会越来越干净。