《重构:改善既有代码的设计》6. 第一组重构
6.1 提炼函数(Extract Function)
如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,你一眼就能看到函数的用途,大多数时候根本不需要关心函数如何达成其用途(这是函数体内干的事)。
这个重构建议的核心是将一组代码块提炼到一个函数中,并为该函数命名以表明其用途。这样可以提高代码可读性和可维护性。
下面是一个示例,假设我们有以下代码块:
const orders = [
{ productName: 'apple', price: 1.2, quantity: 3 },
{ productName: 'orange', price: 0.8, quantity: 2 }
];
let total = 0;
for (let i = 0; i < orders.length; i++) {
total += orders[i].price * orders[i].quantity;
}
console.log(`Total: $${total.toFixed(2)}`);
这段代码计算了订单中商品的总价格并输出结果。但是当代码变得更加复杂时,这段代码会变得越来越难以理解。因此,我们可以将其重构为一个名为calculateTotalPrice的函数,如下所示:
function calculateTotalPrice(orders) {
let total = 0;
for (let i = 0; i < orders.length; i++) {
total += orders[i].price * orders[i].quantity;
}
return total;
}
const orders = [
{ productName: 'apple', price: 1.2, quantity: 3 },
{ productName: 'orange', price: 0.8, quantity: 2 }
];
const total = calculateTotalPrice(orders);
console.log(`Total: $${total.toFixed(2)}`);
通过这种方式,我们可以轻松地理解代码的用途,并且在以后需要使用该功能时,只需要调用calculateTotalPrice函数即可。
需要注意的是,这个重构建议并不适用于所有情况。对于某些代码块,提炼成一个函数可能会引入更多的复杂性和不必要的抽象层级。因此,在进行重构之前,请确保代码块确实可以从概念上独立,并且被其他部分所复用。
6.2 内联函数(Inline Function)
在代码中,有时会出现一些看似有用但实际上并没有什么作用的间接层。这些间接层可能会导致代码变得复杂难以理解,同时也会增加维护的成本。
具体来说,内联是指将一个函数的内容直接嵌入到调用它的函数中,从而消除函数间的调用关系。这样可以让代码更加简洁明了,也更容易被理解和维护。
以下是一个简短的 JavaScript 代码示例,展示了如何使用内联手法进行重构:
// Before Refactoring
function getFullName(firstName, lastName) {
return firstName + ' ' + lastName;
}
function greetUser(firstName, lastName) {
var fullName = getFullName(firstName, lastName);
console.log('Hello, ' + fullName + '!');
}
greetUser('John', 'Doe');
// After Refactoring
function greetUser(firstName, lastName) {
console.log('Hello, ' + firstName + ' ' + lastName + '!');
}
greetUser('John', 'Doe');
在这个例子中, getFullName 函数实际上并没有起到太大的作用,因为它只是简单地将两个字符串拼接在一起。因此,我们可以使用内联手法将它嵌入到 greetUser 函数中,从而消除函数间的调用关系,使代码更加简洁明了。
6.3 提炼变量(Extract Variable)
在代码中,有些表达式可能会非常复杂,难以一眼看出其含义和作用。这时候,我们可以使用提炼变量重构技巧来将这些表达式拆分成易于阅读和维护的局部变量。
具体来说,在 JavaScript 中,我们可以通过 let 或 const 关键字来定义局部变量,然后将复杂的表达式赋值给该变量。
以下是一个简短的 JavaScript 代码示例,展示了如何使用提炼变量重构技巧:
// Before Refactoring
function calculateTotalPrice(quantity, price) {
return quantity * price + (quantity > 10 ? 0.1 * price * (quantity - 10) : 0);
}
console.log(calculateTotalPrice(5, 100)); // Output: 500
console.log(calculateTotalPrice(15, 100)); // Output: 1400
// After Refactoring
function calculateTotalPrice(quantity, price) {
const basePrice = quantity * price;
const discount = quantity > 10 ? 0.1 * price * (quantity - 10) : 0;
return basePrice + discount;
}
console.log(calculateTotalPrice(5, 100)); // Output: 500
console.log(calculateTotalPrice(15, 100)); // Output: 1400
在这个例子中,原始的 calculateTotalPrice 函数中包含了一个比较复杂的表达式,它计算了商品的总价,其中包含了一个数量大于 10 的折扣。为了让代码更加易于阅读和维护,我们使用提炼变量的技巧,将这个复杂表达式拆分成两个局部变量 basePrice 和 discount,然后在返回值中使用它们计算商品的总价。这样可以使代码更加清晰明了,同时也更容易被理解和维护。
6.4 内联变量(Inline Variable)
在代码中,有时候我们会定义一些变量来辅助计算或者提高代码可读性。但是有些情况下,这些变量实际上并没有什么作用,只是增加了代码的复杂度和维护成本。这时候,我们可以使用内联变量重构技巧来消除这些无用的变量。
具体来说,在 JavaScript 中,内联变量就是将一个变量的值直接替换到它所出现的地方,从而消除变量在代码中的存在。需要注意的是,内联变量只适用于那些没有任何副作用的变量。
以下是一个简短的 JavaScript 代码示例,展示了如何使用内联变量重构技巧:
// Before Refactoring
function calculateTotalPrice(quantity, price) {
const basePrice = quantity * price;
const discount = basePrice > 100 ? 0.1 * basePrice : 0;
const finalPrice = basePrice - discount;
return finalPrice;
}
console.log(calculateTotalPrice(5, 100)); // Output: 500
console.log(calculateTotalPrice(15, 100)); // Output: 1350
// After Refactoring
function calculateTotalPrice(quantity, price) {
const basePrice = quantity * price;
return basePrice > 100 ? basePrice * 0.9 : basePrice;
}
console.log(calculateTotalPrice(5, 100)); // Output: 500
console.log(calculateTotalPrice(15, 100)); // Output: 1350
在这个例子中,原始的 calculateTotalPrice 函数中定义了三个变量 basePrice、discount 和 finalPrice,分别用于计算商品的基础价格、折扣和最终价格。但是实际上,我们可以将它们合并为一个表达式,并使用内联变量的技巧消除无用的变量。这样可以使代码更加简洁明了,同时也更容易被理解和维护。
6.5 改变函数声明(Change Function Declaration)
一个好名字能让我一眼看出函数的用途,而不必查看其实现代码。 在代码中,函数的名称是非常重要的,它能够传达函数的用途和作用,从而帮助开发人员快速地理解和使用代码。但是有时候,函数的名称可能并不太准确或者描述不清楚,这时候我们可以使用改变函数声明的重构技巧来修改函数的名称,使其更加准确、易于理解。
具体来说,在 JavaScript 中,改变函数声明通常分为两种情况:改变函数名称和改变函数参数个数。前者可以通过修改函数的名称来使函数表达意图更清晰;后者可以通过添加或删除函数参数来消除不必要的参数或增加必要的参数。
以下是一个简短的 JavaScript 代码示例,展示了如何使用改变函数声明重构技巧:
// Before Refactoring
function calc(x, y) {
return x + y;
}
console.log(calc(1, 2)); // Output: 3
// After Refactoring (Change Function Name)
function calculateSum(x, y) {
return x + y;
}
console.log(calculateSum(1, 2)); // Output: 3
// After Refactoring (Change Function Parameter)
function calculateTotalPrice(quantity, price, taxRate) {
const basePrice = quantity * price;
const taxAmount = basePrice * taxRate;
return basePrice + taxAmount;
}
console.log(calculateTotalPrice(5, 100, 0.1)); // Output: 550
在这个例子中,原始的 calc 函数名称并没有太多的意义,我们可以使用改变函数名称的重构技巧将其修改为 calculateSum,使函数更加易于理解。另外,在 calculateTotalPrice 函数中,我们添加了一个新的参数 taxRate,用于计算商品的税额,这样可以使函数更加通用、灵活,同时也更易于重用。
6.6 封装变量(Encapsulate Variable)
如果想要搬移一处被广泛使用的数据,最好的办法往往是先以函数形式封装所有对该数据的访问。这样,我就能把“重新组织数据”的困难任务转化为“重新组织函数”这个相对简单的任务。
在代码中,有时候会存在一些共享变量,这些变量可能被多个函数或模块使用,而且可能会对程序的正确性和可维护性产生影响。为了解决这个问题,我们可以使用封装变量的重构技巧来将共享变量包装成一个函数,并通过该函数来访问和修改变量的值,从而降低变量的耦合度,提高程序的可维护性。
具体来说,在 JavaScript 中,封装变量通常涉及到两个步骤:创建访问器函数和修改变量访问方式。访问器函数可以控制对变量的读写权限,从而保证变量的安全性和一致性;修改变量访问方式则可以避免直接访问变量,减少变量被误用的可能性。
以下是一个简短的 JavaScript 代码示例,展示了如何使用封装变量重构技巧:
// Before Refactoring
let salary = 1000;
function calculateBonus() {
return salary * 0.1;
}
console.log(calculateBonus()); // Output: 100
salary += 500;
console.log(calculateBonus()); // Output: 150
// After Refactoring
let _salary = 1000;
function getSalary() {
return _salary;
}
function setSalary(value) {
_salary = value;
}
function calculateBonus() {
return getSalary() * 0.1;
}
console.log(calculateBonus()); // Output: 100
setSalary(getSalary() + 500);
console.log(calculateBonus()); // Output: 150
在这个例子中,原始代码中存在一个共享变量 salary,它被多个函数使用,并且可能会被误用。为了解决这个问题,我们使用封装变量的技巧,将 salary 包装成两个访问器函数 getSalary 和 setSalary,并通过这两个函数来访问和修改变量的值,保证变量的安全性和一致性。这样可以使代码更加健壮、可维护,同时也更容易被理解和重用。
6.7 变量改名(Rename Variable)
在代码中,变量的名称和含义对于程序的可读性和可理解性有着至关重要的作用。但是有时候,变量的名称可能存在歧义、困惑或者不够准确,这时候我们可以使用变量改名重构技巧来修改变量的名称,使其更加清晰、易于理解。
具体来说,在 JavaScript 中,变量改名通常需要注意以下几个方面:修改变量名称、修改变量的声明位置、修改变量的使用位置等。
以下是一个简短的 JavaScript 代码示例,展示了如何使用变量改名重构技巧:
// Before Refactoring
function calculateTotalPrice(quantity, unitPrice) {
const tax = 0.1;
const totalPrice = quantity * unitPrice + (quantity > 10 ? 0 : 5);
return totalPrice + totalPrice * tax;
}
console.log(calculateTotalPrice(5, 100)); // Output: 550
// After Refactoring
function calculateTotalPrice(units, pricePerUnit) {
const TAX_RATE = 0.1;
const basePrice = units * pricePerUnit + (units > 10 ? 0 : 5);
return basePrice + basePrice * TAX_RATE;
}
console.log(calculateTotalPrice(5, 100)); // Output: 550
在这个例子中,原始的 calculateTotalPrice 函数中包含了两个变量 quantity 和 unitPrice,它们可能存在歧义和困惑。为了解决这个问题,我们使用变量改名的技巧,将它们修改为更加清晰、准确的名称 units 和 pricePerUnit。另外,在函数中定义了一个常量 taxRate,用于计算商品的税额,这样可以使代码更加易读、易懂,同时也更加具有可维护性。
6.8 引入参数对象(Introduce Parameter Object)
一组数据项总是结伴同行,出没于一个又一个函数。这样一组数据就是所谓的数据泥团,我喜欢代之以一个数据结构。
在代码中,有时候我们会使用多个独立的变量来表示某一个事物或者概念,这些变量之间可能存在着紧密的关系。为了解决这个问题,我们可以使用引入参数对象的重构技巧将它们包装成一个对象,并通过该对象来传递变量的值,从而降低变量之间的耦合度,提高程序的可维护性。
具体来说,在 JavaScript 中,引入参数对象通常需要注意以下几个方面:创建参数对象、修改函数签名、修改函数内部实现等。
以下是一个简短的 JavaScript 代码示例,展示了如何使用引入参数对象重构技巧:
// Before Refactoring
function printInvoice(quantity, pricePerUnit, discount) {
const basePrice = quantity * pricePerUnit;
const discountAmount = basePrice * discount;
const totalPrice = basePrice - discountAmount;
console.log(`Quantity: ${quantity}`);
console.log(`Price per unit: ${pricePerUnit}`);
console.log(`Discount: ${discount}`);
console.log(`Total price: ${totalPrice}`);
}
printInvoice(5, 100, 0.1);
// After Refactoring
function printInvoice(invoice) {
const basePrice = invoice.quantity * invoice.pricePerUnit;
const discountAmount = basePrice * invoice.discount;
const totalPrice = basePrice - discountAmount;
console.log(`Quantity: ${invoice.quantity}`);
console.log(`Price per unit: ${invoice.pricePerUnit}`);
console.log(`Discount: ${invoice.discount}`);
console.log(`Total price: ${totalPrice}`);
}
printInvoice({ quantity: 5, pricePerUnit: 100, discount: 0.1 });
在这个例子中,原始的 printInvoice 函数使用了三个独立的变量 quantity、pricePerUnit 和 discount,它们之间存在着紧密的关系。为了解决这个问题,我们使用引入参数对象的技巧,将它们包装成一个对象 invoice,并通过该对象来传递变量的值,降低变量之间的耦合度,提高程序的可维护性。同时,在函数内部也对原来的访问方式进行了相应的修改,使代码更加简洁明了,同时也更易于理解和重用。
6.9 函数组合成类(Combine Functions into Class)
如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),我就认为,是时候组建一个类了。类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他部分。
在代码中,有时候会存在一组函数操作同一块数据的情况,这些函数之间存在着紧密的关系,可能需要共享一些状态信息。为了解决这个问题,我们可以使用函数组合成类的重构技巧将这些函数封装进一个类中,并通过该类来管理和操作数据,从而提高程序的可维护性和可扩展性。
具体来说,在 JavaScript 中,函数组合成类通常需要注意以下几个方面:创建类、移动函数到类中、修改函数内部实现等。
以下是一个简短的 JavaScript 代码示例,展示了如何使用函数组合成类重构技巧:
// Before Refactoring
function calculateTotalPrice(units, pricePerUnit) {
const TAX_RATE = 0.1;
const basePrice = units * pricePerUnit + (units > 10 ? 0 : 5);
const taxAmount = basePrice * TAX_RATE;
return basePrice + taxAmount;
}
function printInvoice(units, pricePerUnit) {
const totalPrice = calculateTotalPrice(units, pricePerUnit);
console.log(`Quantity: ${units}`);
console.log(`Price per unit: ${pricePerUnit}`);
console.log(`Total price: ${totalPrice}`);
}
printInvoice(5, 100);
// After Refactoring
class Invoice {
constructor(units, pricePerUnit) {
this.units = units;
this.pricePerUnit = pricePerUnit;
this.TAX_RATE = 0.1;
}
calculateTotalPrice() {
const basePrice = this.units * this.pricePerUnit + (this.units > 10 ? 0 : 5);
const taxAmount = basePrice * this.TAX_RATE;
return basePrice + taxAmount;
}
printInvoice() {
const totalPrice = this.calculateTotalPrice();
console.log(`Quantity: ${this.units}`);
console.log(`Price per unit: ${this.pricePerUnit}`);
console.log(`Total price: ${totalPrice}`);
}
}
const invoice = new Invoice(5, 100);
invoice.printInvoice();
在这个例子中,原始的代码中有两个独立的函数 calculateTotalPrice 和 printInvoice,它们之间存在着紧密的关系,共享了同一组数据。为了解决这个问题,我们使用函数组合成类的技巧,将它们封装进一个类 Invoice 中,并通过该类来管理和操作数据。同时,在类中定义了一个属性 TAX_RATE,用于计算商品的税额,这样可以使代码更加易读、易懂,同时也更加具有可维护性。
6.10 函数组合成变换(Combine Functions into Transform)
把所有计算派生数据的逻辑收拢到一处,这样始终可以在固定的地方找到和更新这些逻辑,避免到处重复。如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,我就会遭遇数据不一致。
这个场景是重构的一个技巧,它可以把计算派生数据的逻辑从多个地方收集到一处,从而避免代码中的重复计算和逻辑错误。具体来说,对于那些需要生成新的数据或者转换现有数据的代码,我们可以将其中涉及到的同样的计算逻辑抽象出来,组合成为一个独立的变换函数,然后使用该函数来生成新的派生数据。
在 JavaScript 中,函数组合成变换包括以下步骤:
- 创建一个新的变换函数,用于接收原始数据作为输入,并根据其生成新的派生数据。
- 将原来的代码中涉及到同样计算逻辑的部分替换为调用新的变换函数。
- 根据需要修改新的变换函数的输入参数和返回值,以满足代码中的需求。
以下是一个简单的 JavaScript 代码示例,展示了如何使用函数组合成变换:
// Before refactoring
function calculateTotalPrice(units, pricePerUnit, taxRate) {
const basePrice = units * pricePerUnit;
const taxAmount = basePrice * taxRate;
const totalPrice = basePrice + taxAmount;
return totalPrice;
}
function printInvoice(units, pricePerUnit, taxRate) {
const totalPrice = calculateTotalPrice(units, pricePerUnit, taxRate);
console.log(`Quantity: ${units}, Price per unit: $${pricePerUnit}, Tax rate: ${taxRate}, Total price: $${totalPrice}`);
}
printInvoice(10, 15, 0.1);
// After refactoring
function calculateTotalPrice(units, pricePerUnit, taxRate) {
const basePrice = units * pricePerUnit;
const taxAmount = basePrice * taxRate;
const totalPrice = basePrice + taxAmount;
return totalPrice;
}
function transformInvoiceData(data) {
const { units, pricePerUnit, taxRate } = data;
const totalPrice = calculateTotalPrice(units, pricePerUnit, taxRate);
return { ...data, totalPrice };
}
function printInvoice(data) {
console.log(`Quantity: ${data.units}, Price per unit: $${data.pricePerUnit}, Tax rate: ${data.taxRate}, Total price: $${data.totalPrice}`);
}
const invoiceData = { units: 10, pricePerUnit: 15, taxRate: 0.1 };
const transformedData = transformInvoiceData(invoiceData);
printInvoice(transformedData);
在该示例中,我们将原来的 calculateTotalPrice 函数提取出来,并将其用作新的变换函数 transformInvoiceData 的一部分。通过这种方式,我们现在可以将计算总价的逻辑放在一个地方,并避免在代码中进行重复计算。同时,我们还修改了 printInvoice 函数的实现,使其使用新的派生数据来打印发票信息。
总体而言,函数组合成变换技巧可以帮助我们减少代码重复,提高程序的可读性和可维护性。
6.11 拆分阶段(Split Phase)
每当看见一段代码在同时处理两件不同的事,我就想把它拆分成各自独立的模块
拆分阶段(Split Phase)是《重构:改善既有代码的设计》书中第六章第一组重构中的一项。它的核心思想是将同时处理两件不同事情的代码拆分成各自独立的模块,以提高代码的可读性和可维护性。
下面是一个简短的 JavaScript 代码示例,展示了如何使用拆分阶段重构来改进代码:
function processOrder(order) {
// 处理订单
calculateTotal(order);
sendConfirmationEmail(order);
}
function calculateTotal(order) {
// 计算订单总额
order.total = order.items.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
}
function sendConfirmationEmail(order) {
// 发送确认邮件
console.log(`Confirmation email sent to ${order.email}`);
}
在这个例子中,原始的 processOrder() 函数同时包含计算订单总额和发送确认邮件的逻辑。使用拆分阶段重构,我们将这个函数拆分成两个独立的函数:calculateTotal() 和 sendConfirmationEmail()。现在每个函数只负责完成自己的任务,代码变得更加清晰易懂,并且可以更方便地进行单元测试和重用。