相对较为频繁可用到的重构方法
1、提炼函数
将意图和实现分开。如果你需要写上一大段注释去说明代码用途时,说明改代码需要被提炼到独立函数,并且注释便是一个函数的好名字
// bad
function printOwing(invoice){
let outstanding = 0;
console.log("*******************")
console.log("***Customer Owes***")
console.log("*******************")
// calculate outstanding
for(const of of invoice.orders){
outstanding += o.amount
}
// record due date
const today = Clock.today
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30)
// print details
console.log(`name: ${invoice.customer}`)
console.log(`amount: ${outstanding}`)
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`)
}
// good
// 根据注释提取独立函数
function printOwing(invoice){
printBanner()
calculateOutstanding(invoice)
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
function printBanner() {
console.log("*******************")
console.log("***Customer Owes***")
console.log("*******************")
}
function printDetails(invoice, outstanding) {
// print details
console.log(`name: ${invoice.customer}`)
console.log(`amount: ${outstanding}`)
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`)
}
fucntion recordDueDate(invoice) {
const today = Clock.today
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30)
}
function calculateOutstanding(invoice) {
let result = 0;
for(const of of invoice.orders){
result += o.amount
}
return result
}
短函数的大量调用会影响性能的问题,在如今已经非常罕见
短函数常常能让编译器的优化功能运转更良好,因为短函数可以更容易地被缓存
2、内联函数
如果函数内部代码和名称同样清晰易读,可直接使用其代码,没必要加多一层嵌套
function getRating(driver){
return moreThanFiveLateDeliveries(driver) ? 2: 1
}
function moreThanFiveLateDeliveries(driver) {
return driver.numberOfLateDeliveries > 5
}
function getRating(driver){
return driver.numberOfLateDeliveries > 5 ? 2: 1
}
3、提炼变量
局部变量可以帮助我们将表达式分解为比较容易管理的形式
// bad
{
return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 + Math.min(order.quantity * order.itemPrice * 0.1, 100)
}
// good
{
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05
const shipping = Math.min(order.quantity * order.itemPrice * 0.1, 100)
return basePrice - quantityDiscount + shipping
}
4、内联变量
变量能给表达式提供有意义的名字,但是有的时候,名字并不比表达式本身更具有表现力
// bad
let basePrice = anOrder.basePrice;
return basePrice > 200
// good
return anOrder.basePricce > 200
5、改变函数声明
- 简单做法,直接更改函数名字
// bad
function circum(radius) { return 2 * Math.PI * radius }
// good
function circummFerence(radius){ return 2 * Math.PI * radius }
- 迁移式做法,建立中间函数,再一步步替换,可以减少重构带来的危险
// bad
function circum(radius) { return 2 * Math.PI * radius }
// good
function circum(radius) { return circummFerence() }
function circummFerence(radius){ return 2 * Math.PI * radius }
6、封装变量
对于所有可变的数据,只要它的作用于超出单个函数,可以考虑封装起来,只允许通过函数访问
// bad
let defaultOwner = { first:'Martin', lastName: 'Fowler' }
spaceShip.owner = defaultOwner
defaultOwner = { first:'Rebecca', lastName: 'Lee' }
// good
// defaultOwner.js
let defaultOwner = { first:'Martin', lastName: 'Fowler' }
export function getDefaultOwner() { return defaultOwner }
export function setDefaultOwner(arg) { defaultOwner = arg }
spaceShip.owner = getDefaultOwner()
setDefaultOwner({first:'Rebecca', lastName: 'Lee'})
// 如果需要阻止对数据的更改,可以考虑通过封装成类来实现
class Person {
constrcutor(data) {
this._lastName = data.lastName;
this._firstName = data.firstName;
}
get lastName() { return this._lastName }
get firstName() { return this._firstName }
}
// 由于没有设置set 方法,外部只能获取lastName 和 firstName,无法对进行修改
7、变量改名
如果需要改名的变量只作用于一个函数(临时变量或者参数),可对其直接修改
// bad
let a = height * width
// good
let area = height * width
如果变量的作用于不止于单个函数,比如有些地方是读取变量,有些地方是设置变量,则建议使用第6点 “封装变量”方法
// bad
// 定义
let tpHd = 'untitled'
// 读取
return += `<h1>${tpHd}</h1>`
// 赋值
tpHd = obj['articleTitle']
// good
// 读取
return += `<h1>${title()}</h1>`
//赋值
setTitle(obj['articleTitle'])
// 定义
let _title = 'untitled'
function title() { return _title }
function setTitile(arg) { _title = arg}
8、引入参数对象
如果一组数据项总是结伴而行,出没于一个又一个函数,可以代之以一个数据结构
// bad
function amountInvoiced(startDate, endDate) {}
function amountReviced(startDate, endDate) {}
function amountOverdue(startDate, endDate) {}
// good
function amountInvoiced(aDateRange) {}
function amountReviced(aDateRange) {}
function amountOverdue(aDateRange) {}
下面要展示的代码会查看一组温度读数,检查是否有任何一条读数超出了指定的运作温度范围
// bad
const station = {
name: 'ZB1',
reading: [
{ temp: 47, time: '2016-11-10 09:10' },
{ temp: 53, time: '2016-11-10 09:20' },
{ temp: 58, time: '2016-11-10 09:30' },
{ temp: 53, time: '2016-11-10 09:40' },
{ temp: 51, time: '2016-11-10 09:50' },
],
};
// 负责找到超出指定范围的温度读数
function readingOutsideRange(station, max, min) {
return station.readins.filter((r) => r.temp < min || r.temp > max);
}
// 调用
alerts = readingOutsideRange(
station,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling
);
// good
class NumberRange {
constructor(min, max) {
this._data = { min, max };
}
get min() {
return this._data.min;
}
get max() {
return this._data.max;
}
contains(arg) {
return arg >= this.min && arg <= this.max;
}
}
function readingOutsideRange(station, range) {
return station.readins.filter((r) => !range.contains(r.temp));
}
alerts = readingOutsideRange(station, range);
9、函数组合成类
如果发现一组函数影形不离地操作同一块数据(通常是将这块数据座位参数传递给函数),就可以考虑组件一个类了。
类能明确地给这些函数提供一个公用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用
const reading = {
customer: 'ivan',
quantity: 10,
month: 5,
year: 2017,
};
// bad
// 客户端1
const baseCharge = baseRate(reading.month, reading.year) * reading.quantity;
// 客户端2
const base = baseRate(reading.month, reading.year) * reading.quantity;
const taxableCharge = Math.max(0, base - taxThreehold(reading.year));
// 客户端3
const baseChargeAmount = calculateBaseCharge(reading);
function calculateBaseCharge(reading) {
return baseRate(reading.month, reading.year) * reading.quantity;
}
const reading = {
customer: 'ivan',
quantity: 10,
month: 5,
year: 2017,
};
// 计算基础费用的公示被用了两遍,而客户端3的调用虽然封装了一层,但是从结构上看不出来数据的关联性,
// 如果把关联数据和方法封装成类,则结构会更清晰明了
// good
class Reading {
constructor(data) {
this._customer = data.customer;
this._quantity = data.quantity;
this._month = data.month;
this._year = data.year;
}
get customer() {
return this._customer;
}
get quantity() {
return this._quantity;
}
get month() {
return this._month;
}
get year() {
return this._year;
}
get baseCharge() {
return baseRate(this.month, this.year) * this.quantity;
}
get taxableCharge() {
return Math.max(0, this.baseCharge - taxThreehold(this.year));
}
}
// 客户端1
const baseCharge = new Reading(reading).baseCharge;
// 客户端2
const base = new Reading(reading).baseCharge;
const taxableCharge = new Reading(reading).taxableCharge;
// 客户端3
const baseChargeAmount = new Reading(reading).baseCharge;
10、函数组合成变换
在软件中,经常需要把数据 “喂” 给一个程序,让它可以计算出各种派生信息。而这些派生数据可能会在几个不同的地方用到,因此有些计算逻辑也会在用到派生数据的地方重复用到。
通过做法可以考虑使用“数据变换函数”:这种函数接受元数据作为输入,计算出所有的派生数据,将派生数据以字段形式填入输出数据。
函数组成变换的替代方案是“函数组合成类”,两者的区别在于,如果代码中会对源数据做更新,使用类会好得多;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,就会遭遇数据不一致。
const reading = {
customer: 'ivan',
quantity: 10,
month: 5,
year: 2017
}
// bad
// 客户端1
const baseCharge = baseRate(reading.month, reading.year) * reading.quantity
// 客户端2
const base = baseRate(reading.month, reading.year) * reading.quantity
const taxableCharge = Math.max(0, base - taxThreehold(reading.year))
// 客户端3
const baseChargeAmount = calculateBaseCharge(reading)
function calculateBaseCharge(reading) {
return baseRate(reading.month, reading.year) * reading.quantity
}
// good
// 通过enrichReading 把数据和方法集中在一块处理
function enrichReading(aReading) {
let result = _deepClone(aReading)
result.baseCharge = calculateBaseCharge(aReading)
result.taxableCharge = Math.max(0, aReading.baseCharge - taxThreehold(reading.year))
return result
}
// 客户端1
const baseCharge = enrichReading(reading).baseCharge
// 客户端2
const base = enrichReading(reading).baseCharge
const taxableCharge = enrichReading(reading).taxableCharge
// 客户端3
const baseChargeAmount = enrichReading(reading).baseCharge
11、拆分阶段
当一段代码在同时处理两件不同的事,就应该把它拆分成各自独立的模块,这样可以单独处理每个主题。
// bad
function priceOrder(product, quantity, shippingMethod) {
// 根据商品信息,计算商品相关的价格
const basePrice = product.basePrice * quantity;
const discount =
Math.max(quantity - product.discountThreshold, 0) *
product.basePrice *
product.discountRate;
// 根据配送信息,计算配算成本
const shippingPerCase =
basePrice > shippingMethod.discountThreshold
? shippingMethod.discountedFee
: shippingMethod.feePerCase;
const shippingCost = quantity * shippingPerCase;
const price = basePrice - discount + shippingCost;
return price;
}
// good
function priceOrder(product, quantity, shippingMethod) {
const priceData = calculatePriceData(product, quantity);
return applyShipping(priceData);
}
function calculatePriceData(product, quantity) {
const basePrice = product.basePrice * quantity;
const discount =
Math.max(quantity - product.discountThreshold, 0) *
product.basePrice *
product.discountRate;
return { basePrice, quantity, discount };
}
function applyShipping(priceData, shippingMethod) {
const shippingPerCase =
priceData.basePrice > shippingMethod.discountThreshold
? shippingMethod.discountedFee
: shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippingPerCase;
const price = priceData.basePrice - priceData.discount + shippingCost;
}