一、根底重构
1. 提炼函数 (Extract Function)
1.1 应用场景
如果你须要花工夫浏览一段代码能力弄清它到底在干什么,那么就应该将其提炼到一个函数中,并依据它所做的事为其命名。
1.2 应用步骤
- 发明一个新函数,依据这个函数的用意来对它命名
- 将待提炼的代码从源函数复制到新建的指标函数中。
- 仔细检查提炼出的代码,看看其中是否援用了作用域限于源函数、在提炼出的新函数中拜访不到的变量。若是,以参数的模式将它们传递给新函数。
- 所有变量都解决完之后,编译。
- 在源函数中,将被提炼代码段替换为对指标函数的调用。
- 测试。
- 查看其余代码是否有与被提炼的代码段雷同或类似之 处。如果有,思考应用以函数调用取代内联代码,令其调用提炼出的新函数。
1.3 范例
function printOwing(invoice) {
let outstanding = 0;
// 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);
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
// 重构
function printOwing(invoice) {let outstanding = calculateOutstanding(invoice)
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
function calculateOutstanding(invoice) {
let outstanding = 0;
for (const o of invoice.orders) {outstanding += o.amount;}
return outstanding;
}
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. 内联函数 (Inline Function)
2.1 应用场景
如果代码中有太多间接层,使得零碎中的所有函数都仿佛只是对另一个函数的简略委托,造成我在这些委托动作之间昏头昏脑,那么我通常都会应用内联函数。
2.2 应用步骤
- 查看函数,确定它不具多态性。
- 找出这个函数的所有调用点。
- 将这个函数的所有调用点都替换为函数本体。
- 每次替换之后,执行测试。
- 删除该函数的定义。
2.3 范例
function reportLines(aCustomer) {const lines = [];
gatherCustomerData(lines, aCustomer);
return lines;
}
function gatherCustomerData(out, aCustomer) {out.push(["name", aCustomer.name]);
out.push(["location", aCustomer.location]);
}
// 重构
function reportLines(aCustomer) {const lines = [];
lines.push(["name", aCustomer.name]);
lines.push(["location", aCustomer.location]);
return lines;
}
3. 提炼变量 (Extract Variable)
3.1 应用场景
表达式有可能非常复杂而难以浏览。
3.2 应用步骤
- 确认要提炼的表达式没有副作用。
- 申明一个不可批改的变量,把你想要提炼的表达式复制 一份,以该表达式的后果值给这个变量赋值。
- 用这个新变量取代原来的表达式。
- 测试。
3.3 范例
在一个函数中,将它们提炼成变量
function price(order) {return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 + Math.min(order.quantity * order.itemPrice * 0.1, 100);
}
// 重构
function price(order) {
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0 .05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
在一个类中,将它们提炼成办法
class Order {constructor(aRecord) {this._data = aRecord;}
get quantity() {return this._data.quantity;}
get itemPrice() {return this._data.itemPrice;}
get price() {return this.quantity * this.itemPrice - Math.max(0, this.quantity - 500) * this.itemPrice * 0.05 + Math.min(this.quantity * this.itemPrice * 0.1, 100);
}
}
// 重构
class Order {constructor(aRecord) {this._data = aRecord;}
get quantity() {return this._data.quantity;}
get itemPrice() {return this._data.itemPrice;}
get price() {return this.basePrice - this.quantityDiscount + this.shipping;}
get basePrice() {return this.quantity * this.itemPrice;}
get quantityDiscount() {return Math.max(0, this.quantity - 500) * this.itemPrice * 0.05;
}
get shipping() {return Math.min(this.basePrice * 0.1, 100);
}
}
4. 内联变量 (Inline Variable)
4.1 应用场景
有些时候,变量并不比表达式自身更具表现力,可能会障碍重构左近的代码。若果真如此,就应该通过内联的手法打消变量。
4.2 应用步骤
- 查看确认变量赋值语句的右侧表达式没有副作用。
- 如果变量没有被申明为不可批改,先将其变为不可批改,并执行测试。
- 找到第一处应用该变量的中央,将其替换为间接应用赋值语句的右侧表达式。
- 测试。
- 反复后面两步,逐个替换其余所有应用该变量的中央。
- 删除该变量的申明点和赋值语句。
- 测试。
4.3 范例
let basePrice = anOrder.basePrice;
return (basePrice > 1000);
// 重构
return anOrder.basePrice > 1000;
5. 扭转函数申明 (Change Function Declaration)
5.1 应用场景
如果我一个函数、函数的参数的名字不能一眼看出它的用处,一旦发现了就得尽快给它改名。
5.2 应用步骤
- 先写一句正文形容这个函数的用处。
- 再把这句正文变成函数的名字。
- 先实现函数改名。
- 测试。
- 而后增加参数。
- 测试。
5.3 范例
function inNewEngland(aCustomer) {return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(aCustomer.address.state);
}
const newEnglanders = someCustomers.filter(c => inNewEngland(c));
// 重构
function inNewEngland(stateCode) {return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
const newEnglanders = someCustomers.filter(c => inNewEngland(c.address.state));
6. 封装变量 (Encapsulate Variable)
6.1 应用场景
对于所有可变的数据,只有它的作用域超出单个函数,就将其封装起来,只容许通过函数拜访。
6.2 应用步骤
- 创立封装函数,在其中拜访和更新变量值。
- 执行动态查看。
- 逐个批改应用该变量的代码,将其改为调用适合的封装函数。每次替换之后,执行测试。
- 限度变量的可见性。
- 测试。
- 如果变量的值是一个记录,思考应用封装记录
6.3 范例
7. 变量改名 (Rename Variable)
7.1 应用场景
变量要能够很好地解释一段程序在干什么,对于作用域超出一次函数调用的字段,则须要更用心命名。
7.2 应用步骤
- 如果变量被宽泛应用,思考使用封装变量将其封装起来。
- 找出所有应用该变量的代码,逐个批改。
- 测试。
7.3 范例
8. 引入参数对象 (Introduce Parameter Object)
8.1 应用场景
变量要能够很好地解释一段程序在干什么,对于作用域超出一次函数调用的字段,则须要更用心命名。
8.2 应用步骤
- 如果临时还没有一个适合的数据结构,就创立一个。
- 测试。
- 应用扭转函数申明给原来的函数新增一个参数,类型是新建的数据结构。
- 测试。
- 调整所有调用者,传入新数据结构的适当实例。每批改 一处,执行测试。
- 用新数据结构中的每项元素,逐个取代参数列表中与之 对应的参数项,而后删除原来的参数。
- 测试。
8.3 范例
const station = {
name: "ZB1",
readings: [{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 readingsOutsideRange(station, min, max) {return station.readings.filter(r => r.temp < min || r.temp > max);
}
const alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
// 重构
function readingsOutsideRange(station, range) {return station.readings.filter(r => !range.contains(r.temp));
}
function contains(arg) {return (arg >= this.min && arg <= this.max);
}
const alerts = readingsOutsideRange(station, range);
9. 函数组合成类 (Combine Functions into Class)
9.1 应用场景
如果发现一组函数如影随行地操作同一块数据 (通常是将这块数据作为参数传递给函数),就须要组建一个类了。
9.2 应用步骤
- 使用封装记录对多个函数共用的数据记录加以封装。
- 对于应用该记录构造的每个函数,使用搬移函数将其移入新类。
- 用以解决该数据记录的逻辑能够用提炼函数提炼进去,并移入新类。
9.3 范例
const reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
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;}
get calculateBaseCharge() {return baseRate(this.month, this.year) * this.quantity;
}
get baseCharge() {return baseRate(this.month, this.year) * this.quantity;
}
get taxableCharge() {return Math.max(0, this.baseCharge - taxThreshold(this.year));
}
}
10. 函数组合成变换 (Combine Functions into Transform)
10.1 应用场景
如果代码中会对源数据做更新,那么应用类要好得多; 如果应用变换,派生数据会被存储在新生成的记录中,一旦源数据被批改,我就会遭逢数据不统一。
10.2 应用步骤
- 创立一个变换函数,输出参数是须要变换的记录,并间接返回该记录的值。
- 筛选一块逻辑,将其主体移入变换函数中,把后果作为字段增加到输入记录中。批改客户端代码,令其应用这个新字段。
- 测试。
- 针对其余相干的计算逻辑,反复上述步骤。
10.3 范例
const reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
const basicChargeAmount = calculateBaseCharge(aReading);
function calculateBaseCharge(aReading) {return baseRate(aReading.month, aReading.year) * aReading.quantity;
}
// 重构
function enrichReading(original) {const result = _.cloneDeep(original);
result.baseCharge = calculateBaseCharge(result);
result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
return result;
}
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = aReading.baseCharge;
const taxableCharge = aReading.taxableCharge;
11. 拆分阶段 (Split Phase)
11.1 应用场景
一段代码在同时解决两件不同的事,就要把它拆分成各自独立的模块。可能你有一段解决逻辑,其输出数据的格局不合乎计算逻辑的要求,所以你得先对输出数据做一番调整,使其便于解决。也可能是你把数据处理逻辑分成程序执行的多个步骤,每个步骤负责的工作全然不同。
11.2 应用步骤
- 将第二阶段的代码提炼成独立的函数。
- 测试。
- 引入一个直达数据结构,将其作为参数增加到提炼出的新函数的参数列表中。
- 测试。
- 逐个查看提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用到,就将其移入直达数据结构。每次搬移之后都要执行测试。
- 对第一阶段的代码使用提炼函数,让提炼出的函 数返回直达数据结构。
11.3 范例
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 priceData = calculatePricingData(product, quantity);
return applyShipping(priceData, shippingMethod);
}
function calculatePricingData(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
}
};
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;
}