《重构:改善既有的代码设计》读书笔记(三)

98 阅读7分钟

第一组重构(最有用的一组)

提炼函数

反向重构

[[#内联函数]]

动机

[!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];
}