6重构(第2版)

第一组重构

封装与模块化

Extract FunctionMove Function

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第6章:第一组重构(First Group of Refactorings)

一、导读

1.1 本章学习目标

第6章开始介绍具体的重构手法。Martin Fowler 从最基础、最常用的手法开始:Extract Function(提炼函数)、Inline Function(内联函数)、Rename Variable(重命名变量)、Extract Variable(提炼变量)和 Move Function(搬移函数)。这些手法是重构的"基本功",掌握了它们,你就能改善大部分代码的结构。

本章的核心学习目标包括:

  • 掌握 Extract Function:将一段代码提取为独立函数
  • 理解 Inline Function:何时应该将函数内联到调用处
  • 学会 Rename Variable:好的命名是代码清晰的基础
  • 掌握 Extract Variable:用变量名解释复杂的表达式
  • 理解 Move Function:将函数移到更合适的模块

1.2 为什么本章如此重要

本章介绍的手法是日常开发中使用频率最高的。据统计,Extract Function 和 Rename Variable 是程序员最常用的两个重构手法。掌握它们,你就能立即改善大量代码的可读性。

这些手法之所以重要,是因为它们解决的是代码中最基本的问题:命名不清晰和职责不单一。这两个问题是代码难以理解的根源,而本章的手法正是针对这两个问题的直接解决方案。

1.3 核心问题引导

Extract Function 的关键是什么?如何判断一段代码是否应该被提取?

Inline Function 与 Extract Function 是矛盾的,如何判断何时使用哪个?

好的命名应该具备什么特征?

Extract Variable 与 Inline Variable 如何取舍?

Move Function 的决策依据是什么?如何判断代码应该在哪个模块?


二、核心概念详解

2.1 Extract Function(提炼函数)

动机:当你看到一段代码可以放在一起被一个名称注释时,就把它提取到一个独立的函数中。

做法:

创建一个新函数,以它的意图命名

将需要提取的代码从原函数复制到新函数中

如果提取的代码引用了局部变量,将它们作为参数传递

如果提取的代码修改了局部变量,考虑返回修改后的值

在原位置用函数调用替换提取的代码

运行测试

javascript
// 重构前
function printOwing(invoice) {
  printBanner();
  
  let outstanding = 0;
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
  
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
  
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}

// 重构后
function printOwing(invoice) {
  printBanner();
  const outstanding = calculateOutstanding(invoice);
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
}

function calculateOutstanding(invoice) {
  return invoice.orders.reduce((sum, o) => sum + o.amount, 0);
}

function recordDueDate(invoice) {
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}

function printDetails(invoice, outstanding) {
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}

关键决策:如何判断一段代码是否应该被提取?

  • 这段代码是否可以被一个清晰的名称描述?
  • 这段代码是否与函数中其他代码的抽象层次不同?
  • 这段代码是否可能被其他地方复用?

2.2 Inline Function(内联函数)

动机:有时你会遇到一个函数,它的内部实现和名称一样清晰。此时,函数名带来的间接性是不必要的,应该将函数体内联到调用处。

做法:

找到所有调用该函数的地方

将函数体替换到每个调用处

删除原函数

运行测试

javascript
// 重构前:函数名没有增加清晰度
function getRating(driver) {
  return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}

function calculateBonus(driver) {
  const rating = getRating(driver); // 函数名没有比函数体更清晰
  return driver.baseSalary * rating * 0.1;
}

// 重构后:内联函数
function calculateBonus(driver) {
  return driver.baseSalary * (driver.numberOfLateDeliveries > 5 ? 2 : 1) * 0.1;
}

何时使用 Inline Function:

  • 函数体与函数名一样清晰
  • 函数没有增加任何抽象价值
  • 函数只有一行且逻辑简单
  • 函数只被一个地方调用且不太可能被复用

2.3 Rename Variable(重命名变量)

动机:好的命名是代码清晰的基础。如果变量的名称不能清晰地表达其含义,就应该重命名它。

做法:

如果变量被广泛使用,考虑先将其封装为函数

找到所有使用该变量的地方

逐一修改名称

运行测试

javascript
// 重构前:模糊的命名
const t = items.reduce((s, i) => s + i.p * i.q, 0);
const d = t > 100 ? 0.1 : 0;
const f = t * (1 - d);

// 重构后:清晰的命名
const totalAmount = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const discountRate = totalAmount > 100 ? 0.1 : 0;
const finalAmount = totalAmount * (1 - discountRate);

好的命名应该:

  • 揭示意图:让人一看就知道变量代表什么
  • 避免缩写:除非是广泛认可的缩写(如 id, url)
  • 使用领域术语:与业务语言一致
  • 避免误导:不要让人产生错误的联想

2.4 Extract Variable(提炼变量)

动机:复杂的表达式难以理解。将表达式的中间结果赋值给一个命名良好的变量,可以让表达式的意图更清晰。

javascript
// 重构前:复杂的表达式
if (platform.toUpperCase().indexOf('MAC') > -1 && 
    browser.toUpperCase().indexOf('IE') > -1 && 
    wasInitialized() && resize > 0) {
  // do something
}

// 重构后:提炼变量
const isMacPlatform = platform.toUpperCase().indexOf('MAC') > -1;
const isIEBrowser = browser.toUpperCase().indexOf('IE') > -1;
const isInitialized = wasInitialized();
const hasValidResize = resize > 0;

if (isMacPlatform && isIEBrowser && isInitialized && hasValidResize) {
  // do something
}

2.5 Move Function(搬移函数)

动机:当一个函数与它所在的模块关系不大,而与其他模块关系更密切时,应该将它移到更合适的模块。

判断依据:

  • 函数更频繁地使用另一个模块的数据
  • 函数的逻辑更适合另一个模块的抽象层次
  • 函数被另一个模块中的多个函数调用
javascript
// 重构前:函数位置不合适
class Account {
  // ...
  
  // 这个函数更关心 AccountType 的信息
  calculateInterest() {
    let principal = this.balance;
    let rate = this.type.interestRate; // 频繁访问 type 的数据
    let period = this.type.getPeriod();
    return principal * rate * period;
  }
}

// 重构后:移到更合适的类
class AccountType {
  // ...
  
  interestForAccount(account) {
    let principal = account.balance;
    let rate = this.interestRate;
    let period = this.getPeriod();
    return principal * rate * period;
  }
}

class Account {
  // ...
  
  calculateInterest() {
    return this.type.interestForAccount(this);
  }
}

三、实战案例

3.1 案例:综合使用多种基础重构手法

javascript
// 初始代码
class ShoppingCart {
  constructor(items) {
    this.items = items;
  }
  
  checkout() {
    let t = 0;
    for (let i = 0; i < this.items.length; i++) {
      t += this.items[i].p * this.items[i].q;
    }
    if (t > 100) t = t * 0.95;
    let s = t > 50 ? 0 : 9.99;
    return t + s;
  }
}

// 重构步骤:
// 1. Rename Variable:重命名模糊的变量
// 2. Extract Variable:提炼复杂表达式
// 3. Extract Function:提取计算逻辑

class ShoppingCart {
  constructor(items) {
    this.items = items;
  }
  
  checkout() {
    const subtotal = this.calculateSubtotal();
    const discount = this.calculateDiscount(subtotal);
    const shipping = this.calculateShipping(subtotal);
    return subtotal - discount + shipping;
  }
  
  calculateSubtotal() {
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }
  
  calculateDiscount(subtotal) {
    return subtotal > 100 ? subtotal * 0.05 : 0;
  }
  
  calculateShipping(subtotal) {
    return subtotal > 50 ? 0 : 9.99;
  }
}

四、本章小结

本章介绍了 5 个最基础的重构手法:

Extract Function:将一段代码提取为独立函数,提高可读性和复用性

Inline Function:当函数名没有增加清晰度时,将函数体内联

Rename Variable:好的命名是代码清晰的基础

Extract Variable:用变量名解释复杂的表达式

Move Function:将函数移到更合适的模块

这些手法是日常开发中使用频率最高的,掌握它们就能改善大部分代码的结构。


五、思考与练习

思考题

Extract Function 的粒度如何把握?函数应该多短?

在团队中如何统一命名的标准?

Move Function 与微服务拆分有什么关系?

这些基础手法如何组合使用来解决复杂问题?

编程练习

选择一段你写的代码,使用 Extract Function 和 Rename Variable 进行改善。

找到一个应该被内联的函数,使用 Inline Function 进行简化。

找到一个位置不合适的函数,使用 Move Function 将它移到更合适的模块。


6/12第一组重
0%