function getTotalOutstandingAndSendBill() {
const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
sendBill();
return result;
}
function totalOutstanding() {
return customer.invoices.reduce((total, each) => each.amount + total, 0);
}
function sendBill() {
emailGateway.send(formatBill(customer));
}
如果某个函数只是提供一个值,没有任何看得到的副作用,那么这是一个很有价值的东西。我可以任意调用这个函数,也可以把调用动作搬到调用函数的其他地方。这种函数的测试也更容易。
明确表现出“有副作用”与“无副作用”两种函数之间的差异,是个很好的想法。下面是一条好规则:任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离(Command-Query Separation)[mf-cqs]。
完成重构之后,查询函数与原函数之间常会有重复代码,可以做必要的清理。
曾用名:令函数携带参数(Parameterize Method)
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);
}
如果发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。这个重构可以使函数更有用,因为重构后的函数还可以用于处理其他的值。
曾用名:已明确函数取代参数(Replace Parameter with Explicit Methods)
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;}
“标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部分逻辑。这类参数让人难以理解到底有哪些函数可以调用、应该怎么调用。
如果明确用一个函数来完成一项单独的任务,其含义会清晰得多。
标记参数区分:如果调用者传入的是程序中流动的数据,这样的参数不算标记参数;只有调用者直接传入字面量值,这才是标记参数。在函数实现内部,如果参数值只是作为数据传给其他函数,这就不是标记参数;只有参数值影响了函数内部的控制流,这才是标记参数。
针对参数的每一种可能值,新建一个明确函数。
如果主函数有清晰的条件分发逻辑,可以用分解条件表达式(260)创建明确函数;否则,可以在原函数之上创建包装函数。
对于“用字面量值作为参数”的函数调用者,将其改为调用新建的明确函数。
const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (aPlan.withinRange(low, high))
if (aPlan.withinRange(aRoom.daysTempRange))
“传递整个记录”的方式能更好地应对变化:如果将来被调的函数需要从记录中导出更多的数据,就不用为此修改参数列表。如果有很多函数都在使用记录中的同一组数据,处理这部分数据的逻辑常会重复,此时可以把这些处理逻辑搬移到完整对象中去。
曾用名:以函数取代参数(Replace parameter with Method)
反向重构:以参数取代查询(Replace Query with Parameter)
availableVacation(anEmployee, anEmployee.grade);
function availableVacation(anEmployee, grade) {
// calculate vacation...
}
availableVacation(anEmployee)
function availableVacation(anEmployee) {
const grade = anEmployee.grade;
// calculate vacation...
}
函数的参数列表应该总结该函数的可变性,标示出函数可能体现出行为差异的主要方式。和任何代码中的语句一样,参数列表应该尽量避免重复,并且参数列表越短就越容易理解。
如果调用函数时传入了一个值,而这个值由函数自己来获得也是同样容易,这就是重复。
不使用以查询取代参数最常见的原因是,移除参数可能会给函数体增加不必要的依赖关系。
如果想要去除的参数值只需要向另一个参数查询就能得到,这是使用以查询取代参数最安全的场景。如果可以从一个参数推导出另一个参数,那么几乎没有任何理由要同时传递这两个参数。
反向重构:以查询取代参数(Replace parameter with Query)
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() {/*...*/}
}
如果为某个字段提供了设值函数,这就暗示这个字段可以被改变。如果不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数。
如果构造函数尚无法得到想要设入字段的值,就使用改变函数声明(124)将这个值以参数的形式传入构造函数。在构造函数中调用设值函数,对字段设值。
如果想移除多个设值函数,可以一次性把它们的值都传入构造函数,这能简化后续步骤。
移除所有在构造函数之外对设值函数的调用,改为使用新的构造函数。每次修改之后都要测试。
如果不能把“调用设值函数”替换为“创建一个新对象”(例如需要更新一个多处共享引用的对象),请放弃本重构。
使用内联函数(115)消去设值函数。如果可能的话,把字段声明为不可变。
测试。
曾用名:以工厂函数取代构造函数(Replace Constructor with Factory Method)
leadEngineer = new Employee(document.leadEngineer, 'E');
leadEngineer = createEngineer(document.leadEngineer);
很多面向对象语言都有特别的构造函数,专门用于对象的初始化。需要新建一个对象时,客户端通常会调用构造函数。但与一般的函数相比,构造函数又常有一些丑陋的局限性。
工厂函数的实现内部可以调用构造函数,但也可以换成别的方式实现。
曾用名:以函数对象取代函数(Replace Method with Method Object)
反向重构:以函数取代命令(Replace Command with Function)
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
}
}
function score(candidate, medicalExam, scoringGuide) {
return new Scorer().execute(candidate, medicalExam, scoringGuide);
}
命令对象为处理复杂计算提供了强大的机制。借助命令对象,可以轻松地将原本复杂的函数拆解为多个方法,彼此之间通过字段共享状态;拆解后的方法可以分别调用;开始调用之前的数据状态也可以逐步构建。
为想要包装的函数创建一个空的类,根据该函数的名字为其命名。
使用搬移函数(198)把函数移到空的类里。
遵循编程语言的命名规范来给命令对象起名。如果没有合适的命名规范,就给命令对象中负责实际执行命令的函数起一个通用的名字,例如“execute”或者“call”。
可以考虑给每个参数创建一个字段,并在构造函数中添加对应的参数。
反向重构:以命令取代函数(Replace Function with Command)
class ChargeCalculator {
constructor (customer, usage){
this._customer = customer;
this._usage = usage;
}
execute() {
return this._customer.rate * this._usage;
}
}
function charge(customer, usage) {
return customer.rate * usage;
}
大多数时候,只是想调用一个函数,让它完成自己的工作就好。如果这个函数不是太复杂,那么命令对象可能显得费而不惠,就应该考虑将其变回普通的函数。