第一组重构(最有用的一组)
提炼函数
反向重构
[[#内联函数]]
动机
[!info] “将意图与实现分开”:如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,你一眼就能看到函数的用途,大多数时候根本不需要关心函数如何达成其用途(这是函数体内干的事)。
范例
重构前:
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(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
重构后:
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
function printBanner() {
console.log("***********************");
console.log("**** Customer Owes ****");
console.log("***********************");
}
function calculateOutstanding(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() {
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
内联函数
反向重构
[[#提炼函数]]
动机
[!info] 有时候你会遇到某些函数,其内部代码和函数名称同样清晰易读。也可能你重构了该函数的内部实现,使其内容和其名称变得同样清晰。 我手上有一群组织不甚合理的函数。可以将它们都内联到一个大型函数中,再以我喜欢的方式重新提炼出小函数。
范例
重构前
function getRating(driver) {
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(driver) {
return driver.numberOfLateDeliveries > 5;
}
重构后
function getRating(driver) {
return (driver.numberOfLateDeliveries > 5) ? 2 : 1;
}
提炼变量
反向重构
[[#内联变量]]
动机
[!info] 表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。
范例1
重构前:
function price(order) {
//price is base price - quantity discount + shipping
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;
}
范例2
重构前
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);
}
}
内联变量
反向重构
[[#提炼变量|提炼变量]]
动机
[!info] 在一个函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东西。但有时候,这个名字并不比表达式本身更具表现力。还有些时候,变量可能会妨碍重构附近的代码。若果真如此,就应该通过内联的手法消除变量。
范例
重构前:
let basePrice = anOrder.basePrice;
return (basePrice > 1000);
重构后:
return anOrder.basePrice >1000
改变函数声明
反向重构
动机
[!info] 修改函数声明,修改参数,修改参数列表
范例
重构前:
addReservation(customer) {
this._reservations.push(customer);
}
重构后:
addReservation(customer) {
this.zz_addReservation(customer, false);
}
zz_addReservation(customer, isPriority) {
assert(isPriority === true || isPriority === false);
this._reservations.push(customer);
}
说明:
[!tip] 在 JavaScript 中,assert 函数通常用于在代码中进行调试和测试时进行断言检查。它的作用是在代码中检查某个条件是否为真,如果条件为假,则会抛出异常并中止程序的执行。如果条件为真,则程序会继续执行。
assert 函数通常用于在开发和测试过程中检查代码的正确性,以确保代码的行为符合预期。例如,开发人员可以使用 assert 函数来检查函数的输入和输出是否符合预期,以及确保代码中的变量具有正确的值。
在生产环境中,assert 函数通常会被禁用,因为它们会中止程序的执行,这可能会导致意外的错误和不必要的停机时间。
封装变量
反向重构
动机
[!info] 如果想要搬移一处被广泛使用的数据,最好的办法往往是先以函数形式封装所有对该数据的访问。这样,我就能把“重新组织数据”的困难任务转化为“重新组织函数”这个相对简单的任务。 封装能提供一个清晰的观测点,可以由此监控数据的变化和使用情况;我还可以轻松地添加数据被修改时的验证或后续逻辑。
范例
重构前:
let defaultOwner = { firstName: "Martin", lastName: "Fowler" };
重构1,通过返回数据副本保护数据
let defaultOwnerData = { firstName: "Martin", lastName: "Fowler" };
export function defaultOwner() {
return Object.assign({}, defaultOwnerData);
}
export function setDefaultOwner(arg) {
defaultOwnerData = arg;
}
重构2,通过封装记录保护数据
let defaultOwnerData = {firstName: "Martin", lastName: "Fowler"};
export function defaultOwner() { return new Person(defaultOwnerData);}
export function setDefaultOwner(arg) {defaultOwnerData = arg;}
class Person {
constructor(data) {
this.lastName = data.lastName;
this.firstName = data.firstName
}
get lastName() {return this.lastName;}
get firstName() {return this.firstName;}
// and so on for other properties
}
变量改名
反向重构
动机
[!info] 如果在另一个代码库中使用了该变量,这就是一个“已发布变量”(published variable),此时不能进行这个重构。如果变量值从不修改,可以将其复制到一个新名字之下,然后逐一修改使用代码,每次修改后执行测试。
范例
重构前:
let tpHd = "untitled";
result += `
# ${tpHd}
`;
tpHd = obj["articleTitle"];
重构后:
result += `
# ${title()}
`;
setTitle(obj["articleTitle"]);
function title() {
return tpHd;
}
function setTitle(arg) {
tpHd = arg;
}
引入参数对象
反向重构
动机
范例
重构前:
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);
}
alerts = readingsOutsideRange(
station,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling
);
重构后:
class NumberRange {
constructor(min, max) {
this._data = { min: min, max: max };
}
get min() {
return this._data.min;
}
get max() {
return this._data.max;
}
contains(arg) {
return (arg >= this.min || arg <= this.max);
}
}
function readingsOutsideRange(station, range) {
return station.readings
.filter(r => !range.contains(r.temp));
}
const range = new NumberRange(
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling
);
alerts = readingsOutsideRange(station, range);
函数组合成类
替代方案
[[#函数组合成变换|函数组合成变换]]
反向重构
动机
[!info] 如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),我就认为,是时候组建一个类了。类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他部分。 使用类有一大好处:客户端可以修改对象的核心数据,通过计算得出的派生数据则会自动与核心数据保持一致。
范例
重构前:
// const reading = { customer: "ivan", quantity: 10, month: 5, year: 2017 };
const aReading = acquireReading();
const basicChargeAmount = baseRate(aReading.month, aReading.year) * aReading.quantity;
const taxableCharge = Math.max(
0,
basicChargeAmount - taxThreshold(aReading.year)
);
重构后:
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));
}
}
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = aReading.baseCharge;
const taxableCharge = aReading.taxableCharge;
函数组合成变换
替代方案
[[#函数组合成类|函数组合成类]]
反向重构
动机
[!info] 在软件中,经常需要把数据“喂”给一个程序,让它再计算出各种派生信息。 一个方式是采用数据变换(transform)函数:这种函数接受源数据作为输入,计算出所有的派生数据,将派生数据以字段形式填入输出数据。有了变换函数,我就始终只需要到变换函数中去检查计算派生数据的逻辑。
范例
重构前:
// const reading = { customer: "ivan", quantity: 10, month: 5, year: 2017 };
const aReading = acquireReading();
const basicChargeAmount = baseRate(aReading.month, aReading.year) * aReading.quantity;
const taxableCharge = Math.max(
0,
basicChargeAmount - taxThreshold(aReading.year)
);
重构后:
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 taxableCharge = aReading.taxableCharge;
拆分阶段
反向重构
动机
[!info] 如果一段代码在同时处理两件不同的事,那么就可以把它拆分成各自独立的模块。 编译器是最典型的例子:首先对文本做词法分析,然后把token解析成语法树,然后再对语法树做几步转换(如优化),最后生成目标码。 每一步都有边界明确的范围,我可以聚焦思考其中一步,而不用理解其他步骤的细节。
范例
重构前:
const orderData = 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];
}