第十一章:重构API
一、将查询函数和修改函数分离
动机:
如果某个函数只是提供一个值,没有任何看得到的副作用,那么这是一个很有价值的东西。因为我可以任意调用这个函数,也可以把调用动作搬到调用函数的其他地方。
这就对应一条好规则:任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离。
具体展现:
// 重构前
function getTotalOutstandingAndSendBill() {
const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
sendBill();
return result;
}
// 重构后
function totalOutstanding() {
const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
}
function sendBill() {
emailGateway.send(formatBill(customer));
}
二、函数参数化
动机:
如果发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。
具体展现:
// 重构前
function tenPercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.05);
}
// 重构后
function raise(aPerson, factor) {
aPerson.salary = aPerson.salary.multiply(1 + factor);
}
三、移除标记参数
动机:
“标记参数”是调用者用它来指示被调函数应该执行哪一部分逻辑。严格来说,只有参数值影响了函数内部的控制流,这才是标记参数。
移除标记参数不仅使代码更整洁,并且帮助开发工具更好的发挥作用。
具体展现:
// 重构前
function setDimension(name, value) {
if (name === "height") {
this._height = value;
return;
}
if (name === "width") {
this._width = value;
return;
}
}
// 重构后
function setHeight(value) { this._height = value; }
function setWidth (value) { this._width = value; }
四、保持对象完整
动机:
如果代码从一个记录结构中导出几个值,然后又把这几个值一起传递给一个函数,可以把整个记录传给这个函数,在函数体内部导出所需的值。
“传递整个记录”的方式能更好地应对变化:如果将来被调的函数需要从记录中导出更多的数据,我们就不用为此修改参数列表。传递整个记录也能缩短参数列表,让函数调用更容易看懂。
具体展现:
// 重构前
const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (aPlan.withinRange(low, high)) {
//
}
// 重构后
if (aPlan.withinRange(aRoom.daysTempRange)) {
//
}
五、以查询取代参数
动机:
参数列表应该尽量避免重复,并且参数列表越短就越容易理解。
如果调用函数时传入了一个值,而这个值由函数自己来获得也是同样容易,这就是重复。
有一种情况需要留意:如果正在处理的函数具有引用透明性,即:不论任何时候,只要传入相同的参数,该函数的行为永远一致。这样的函数即容易理解又容易测试,可以不去掉它的参数,而让它去访问一个可变的全局变量。
具体展现:
// 重构前
availableVacation(anEmployee, anEmployee.grade);
function availableVacation(anEmployee, grade) {
//
}
// 重构后
availableVacation(anEmployee);
function availableVacation(anEmployee) {
const grade = anEmployee.grade;
//
}
六、以参数取代查询
动机:
使用本重构的情况大多数源于我们想要改变代码的依赖关系——为了让目标不再依赖于某个元素,我把这个元素的指以参数的形式传递给该函数。
具体展现:
// 重构前
targetTemperature(aPlan)
function targetTemperature(aPlan) {
currentTemperature = thermostat.currentTemperature;
// rest of function
}
// 重构后
targetTemperature(aPlan, thermostat.currentTemperature)
function targetTemperature(aPlan, currentTemperature) {
// rest of function
}
七、移除设值函数
动机:
如果为某个字段提供了设值函数,这就暗示这个字段可以被改变。如果不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数(同时将该字段声明为不可变)。
具体展现:
// 重构前
class Person {
get name() { ... }
set name(aString) { ... }
}
// 重构后
class Person {
get name() { ... }
}
八、以工厂函数取代构造函数
动机:
需要新建一个对象时,客户端通常会调用构造函数。但与一般的函数相比,构造函数又常有一些局限性。
工厂函数不受这些限制。工厂函数的实现内部可以调用构造函数,但也可以换成别的方式实现。
具体展现:
// 重构前
leadEngineer = new Employee(document.leadEngineer, 'E');
// 重构后
leadEngineer = createEmployee(documet.leadEngineer);
九、以命令取代函数
动机:
将函数封装成自己的对象,有时也是一种有用的办法,这样的对象可以称为“命令对象”,或简称为“命令”。与普通函数相比,命令对象提供了更大的控制灵活性和更强的表达能力。除了函数调用本身,命令对象还可以支持附加的操作,例如撤销操作。
具体展现:
// 重构前
function score(candidate, medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
// long body code
}
// 重构后
class Scorer {
constructor(candidate, medicalExam, scoringGuide) {
this._candidate = candidate;
this._medicalExam = medicalExam;
this._scoringGuide = scoringGuide;
}
execute() {
this._result = 0;
this._healthLevel= 0;
// long body code
}
}
十、以函数取代命令
动机:
命令对象为处理复杂计算提供了强大的机制,但这种强大是以复杂性为代价的。
如果一个函数不是太复杂,那么命令对象就显得不太需要了。
具体展现:
// 重构前
class ChargeCalculator {
constructor(cutomer, usage) {
this._cutomer = cutomer;
this._usage = usage;
}
execute() {
return this._cutomer.rate * this._usage;
}
}
// 重构后
function charge(cutomer, usage) {
return cutomer.rate * usage;
}