首先声明以下内容来自《重构》第二版的第六章
1. 提炼函数
function printOwing(invoice) {
let outstanding = 0;
console.log("***********************")
console.log("**** Customer Owes ****")
console.log("***********************")
// calculate outstanding
for (const o 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(`name: ${invoice.dueDate.toLocaleDateString()}`)
}
// =========== 重构 ==============
function printOwing(invoice) {
printBanner()
const outstanding = caculateOutstanding(invoice)
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
function printBanner() {
console.log("***********************")
console.log("**** Customer Owes ****")
console.log("***********************")
}
function caculateOutstanding(invoice) {
let result = 0;
for (const o of invoice.orders) {
result += o.amount
}
return result
}
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(`name: ${invoice.dueDate.toLocaleDateString()}`)
}
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. 提炼变量
表达式有可能复杂而难以阅读,这种情况下局部变量可以帮助我们将表达式分解为比较容易管理的方式。在面对一块复杂逻辑时,我们可以使用局部变量对其中一部分进行命名,这样可以让我们更好的理解这部分的逻辑要干什么
return (
order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity, order.itemPrice * 0.1, 100)
);
// 重构
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. 内联变量
动机:在函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东西。但是有时候,这个名字并不比表达式本身更具表现力。还有时候,变量可能会妨碍重构附近的代码。如果这样,就应该通过内联变量的手法消除变量
做法:
• 检查确认变量赋值语句的右侧表达式有没有副作用
• 如果变量没有被声明为不可修改,现将其变为不可修改,并执行测试
• 这是为了确保该变量只被赋值一次
• 找到第一处使用了该变量的地方,将其替换为直接使用赋值语句的右侧表达式
• 测试
let basePrice = anOrder.basePrice
return (basePrice > 1000)
// 重构
return anOrder.basePrice > 1000
5. 改变函数声明
简单做法:
- 如果想要移除一个参数,需要确定函数体内有没有使用该参数
- 修改函数声明,使其成为你期望的状态
- 找出所有使用旧的函数声明的地方,将他们改为使用新的函数声明
- 测试
迁移式做法 - 如果有必要的话,先对函数体内部加以重构,使后面的提炼步骤易于开展。
- 使用提炼函数将函数体提炼成一个新函数
- 如果你打算沿用旧函数的名字,可以先给新函数起一个易于搜索的临时名字
- 如果提炼出的函数需要新增参数,用前面的简单做法添加即可。
- 测试
- 对旧函数使用内联函数
- 如果新函数使用了临时的名字,再次使用改变函数声明将其改回原来的名字
- 测试
6. 封装变量
对于所有可变的数据,只要它的作用域超过单个函数,将其封装起来,只允许通过函数访问
let defaultOwner = {firstName:"Martin", lastName:"Fowler"}
// 重构
let defaultOwnerData = {firstName:"Martin", lastName:"Fowler"}
export function defaultOwner() {return defaultOwnerData}
export function setDefaultOwner(arg) {defaultOwnerData = arg}
做法:
- 创建封装函数,在其中访问和更新变量值
- 执行静态检查
- 逐一修改使用过该变量的代码,将其改为调用适合的封装函数。每次替换之后执行测试
- 限制变量的可见性
- 有时候没办法阻止直接访问变量。若真如此,可以将变量改名,再执行测试,找出仍在使用该变量的代码
- 测试
- 如果变量的值是一个记录,考虑使用封装记录
7. 变量改名
机制:
- 如果变量被广泛使用,考虑运用封装变量将其封装起来
- 找出所有使用该变量的代码,逐一修改
- 如果在另一个代码库中使用了该变量,这就是一个“已发布变量”,此时不能进行这个重构
- 如果变量值从不修改,可以将其复制到一个新名字之下,然后逐一修改使用代码,每次修改后执行测试
- 测试
8. 引入参数对象
做法:
- 如果暂时还没有一个合适的数据结构,就创建一个
- 作者倾向于使用类,因为稍后把行为放进来会比较容易。(作者)通常尽量确保这些新建的数据结构是值对象。
- 测试
- 使用改变函数声明给原来的函数新增一个参数,类型是新建的数据结构
- 测试
- 调整所有调用者,传入新数据结构的适当实例。每修改一处执行测试
- 用新数据结构中的每项元素,逐一取代参数列表中与之对应的参数项,然后删除原来的参数。测试
// 温度读取数据如下
const station = { name: "ZB1",
readings: [
{ temp: 47, time: "2020-12-31 12:31" },
{ temp: 48, time: "2020-12-31 12:31" },
{ temp: 49, time: "2020-12-31 12:31" },
{ temp: 50, time: "2020-12-31 12:31" },
{ temp: 51, time: "2020-12-31 12:31" },
]
}
// 下面的函数负责找到超出指定范围的温度读数
function readingOutsideRange( station, min, max ) {
return station.readings.filter(r => r.temp < min || r.temp > max)
}
// 调用处
alerts = readingsOutsideRange(station,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling)
// ================== 重构开始 ====================
// 1. 声明数据对象
class NumberRange {
construct(min, max) {
this._data = { min: min, max: max }
}
get min() {
return this._data.min
}
get max() {
return this._data.max
}
}
// 调用处
const range = new NumberRange(operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling)
alerts = readingsOutsideRange(station, range)
// 函数实现处
function readingOutsideRange( station, range ) {
return station.readings.filter(r => r.temp < range.min || r.temp > range.max)
}
将一堆参数替换成一个真正的对象只是长征的第一步。创建一个类是为了把行为搬进去。
function readingOutsideRange( station, range ) {
return station.readings.filter(r => !range.contains(r.temp))
}
// class NumberRange
contains(arg) { return (arg >= this.min || arg <= this.max) }
9. 函数组合成类
如果一组方法形影不离地操作同一块数据,(通常是将这块数据作为参数传递给函数),这时候应该组建一个类了。
做法:
- 运用 封装记录 对多个函数共用的数据记录加以封装
- 如果多个函数共用的数据还未组织成记录结构,则先运用 引入参数对象 ,将其组织成记录
- 对于使用该记录结构的每个函数,运用 搬移函数 将其移入新类
- 如果函数调用时传入的参数已经是新类的成员,则从参数列表中去除之
- 用以处理该数据记录的逻辑可以用 提炼函数 提炼出来,并移入新类
示例:需求 有一个向大家提供茶水的公共设施。每个月都有软件读取茶水计量器的数据,得到这样的数据
reading = { customer: "ivan", quantity: 10, month: 5, year: 2017}
处理数据记录的代码如下:
// 客户端1
const aReading = acquireReading()
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity
// 客户端2 只要不超过一定的量,就不用交税
const aReading = acquireReading()
const base = baseRate(aReading.month, aReading.year) * aReading.quantity
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year))
// 客户端3
const aReading = acquireReading()
const basicChargeAmount = calculateBaseCharge(aReading)
function calculateBaseCharge(aReading) {
return baseRate(aReading.month, aReading.year) * aReading.quantity
}
首先使用 封装记录 将记录变成类
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 }
}
//客户端3变成如下
const rawReading = acquireReading()
const aReading = new Reading(rawReading)
const basicChargeAmount = calculateBaseCharge(aReading)
然后使用 搬移函数 把 calculateBaseCharge
搬到新类中
// class Reading
get calculateBaseCharge() {
return baseRate(this.month, this.year) * this.quantity
}
// 这时候客户端3
const rawReading = acquireReading()
const aReading = new Reading(rawReading)
const basicChargeAmount = aReading.caculateBaseCharge
搬移的同时,运用 函数改名
get baseCharge() {
return baseRate(this.month, this.year) * this.quantity
}
// 客户端1
const rawReading = acquireReading()
const aReading = new Reading(rawReading)
const basicChargeAmount = aReading.baseCharge
// 客户端2
const rawReading = acquireReading()
const aReading = new Reading(rawReading)
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year))
// 客户端3
const rawReading = acquireReading()
const aReading = new Reading(rawReading)
const basicChargeAmount = aReading.baseCharge
运用 提炼函数 将就散应税费用(taxable charge) 的逻辑提炼成函数
function taxableChargeFn(aReading) {
return Math.max(0, aReading.baseCharge - taxThreshold(aReading.year))
}
运用 函数搬移 将其移入 Reading
类
// class Reading
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 - taxThreshold(this.year))
}
}
10. 函数组合成变换
把所有计算派生数据的逻辑放到一块,这样可以始终可以在固定的地方找到和更新这些逻辑,避免重复
function base(aReading) {...}
function taxableCharge(aReading){...}
// 重构
function enrichReading(argReading) {
const aReading = _.cloneDeep(argReading)
aReading.baseCharge = base(aReading)
aReading.taxableCharge = taxableCharge(aReading)
return aReading
}
采用数据变换函数:这种函数接收源数据作为输入,计算出所有的派生数据,将派生数据以字段的形式填入输出数据。有了变换函数,始终只需要到变换函数中检查计算派生数据的逻辑
函数组合成变换的替代方案是 函数组合成类 ,后者的做法是先用源数据创建一个类,再把相关的计算搬移到类中。 如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储到新的生生记录中,一旦数据源被修改,就会遭遇数据不一致。
做法:
- 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
- 这一步通常需要对输入的记录做深复制。此时应该写个测试确保变换不会被修改
- 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段。
- 如果计算逻辑比较复杂,先使用 提炼函数
11. 拆分阶段
const orderDate = orderString.split(/\s+/)
const productPrice = priceList[orderData[0].split("-")[1]]
const orderPrice = parseInt(orderData[1]) * productPrice
// ==== 重构 ====
const orderRecord = parseOrder(order)
const orderPrice = price(orderRecord, priceList)
function parseOrder(aString) {
const values = aString.split(/\s+/)
return ({
productID: values[0].split("-")[1],
quantity: parseInt(values[1])
})
}
function price(order, priceList) {
return order.quantity * priceList[order.productID]
}
一段代码在处理两件不同的事的时候,可以将它拆成各自独立的模块,因为这样到了需要修改的时候,就可以单独处理每个主题
例子:这里有一个 “计算订单价格” 的代码
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;
}
首先使用 提炼函数 把计算配送成本的逻辑提炼出来
function priceOrder(product, quantity, shippingMethod) {
const basePrice = product.basePrice * quantity;
const discount =
Math.Max(quantity - product.discountThreshold, 0) *
product.basePrice *
product.discountRate;
const price = applyShipping(basePrice, shippingMethod, quantity, discount);
return price;
}
function applyShipping(basePrice, shippingMethod, quantity, discount) {
const shippingPerCase =
basePrice > shippingMethod.discountThreshold
? shippingMethod.discountedFee
: shippingMethod.feePerCase;
const shippingCost = quantity * shippingPerCase;
const price = basePrice - discount + shippingCost;
return price;
}
使用一个中转数据结构,使其在两阶段之间沟通信息
function priceOrder(product, quantity, shippingMethod) {
const basePrice = product.basePrice * quantity;
const discount =
Math.Max(quantity - product.discountThreshold, 0) *
product.basePrice *
product.discountRate;
const priceData = { basePrice: basePrice, quantity: quantity, discount: discount };
const price = applyShipping(priceData, shippingMethod);
return price;
}
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;
return price;
}
提取完成后,中转数据得到了完整的填充,现在可以把第一阶段代码提炼成独立的函数
function priceOrder(product, quantity, shippingMethod) {
const priceData = caculatePricingData(product, quantity);
return applyShipping(priceData, shippingMethod);
}
function applyShipping(priceData, shippingMethod) {
const shippingPerCase =
priceData.basePrice > shippingMethod.discountThreshold
? shippingMethod.discountedFee
: shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippingPerCase;
return priceData.basePrice - priceData.discount + shippingCost;
}
function caculatePricingData(product, quantity) {
const basePrice = product.basePrice * quantity;
const discount =
Math.Max(quantity - product.discountThreshold, 0) *
product.basePrice *
product.discountRate;
return {
basePrice: basePrice,
quantity: quantity,
discount: discount
};
}