曾用名:移除对参数的赋值(Remove Assignments to Parameters)
曾用名:分解临时变量(Split Temp)
let temp = 2 * (height + width);
console.log(temp);
temp = height * width;
console.log(temp);
const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width;
console.log(area);动机
变量有各种不同的用途,其中某些用途会很自然地导致临时变量被多次赋值。“循环变量”和“结果收集变量”就是两个典型例子:循环变量(loop variable)会随循环的每次运行而改变(例如 for(let i=0; i<10; i++)语句中的 i);结果收集变量(collecting variable)负责将“通过整个函数的运算”而构成的某个值收集起来。
除了这两种情况,还有很多变量用于保存一段冗长代码的运算结果,以便稍后使用。这种变量应该只被赋值一次。如果它们被赋值超过一次,就意味它们在函数中承担了一个以上的责任。如果变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。同一个变量承担两件不同的事情,会令代码阅读者糊涂。
做法
在待分解变量的声明及其第一次被赋值处,修改其名称。
如果稍后的赋值语句是“i=i+某表达式形式”,意味着这是一个结果收集变量,就不要分解它。结果收集变量常用于累加、字符串拼接、写入流或者向集合添加元素。
如果可能的话,将新的变量声明为不可修改。
以该变量的第二次赋值动作为界,修改此前对该变量的所有引用,让它们引用新变量。
测试。
重复上述过程。每次都在声明处对变量改名,并修改下次赋值之前的引用,直至到达最后一处赋值。
范例
下面范例中我要计算一个苏格兰布丁运动的距离。在起点处,静止的苏格兰布丁会受到一个初始力的作用而开始运动。一段时间后,第二个力作用于布丁,让它再次加速。根据牛顿第二定律,我可以这样计算布丁运动的距离:
function distanceTravelled (scenario, time) {
let result;
let acc = scenario.primaryForce / scenario.mass;
let primaryTime = Math.min(time, scenario.delay);
result = 0.5 * acc * primaryTime * primaryTime;
let secondaryTime = time - scenario.delay;
if (secondaryTime > 0) {
let primaryVelocity = acc * scenario.delay;
acc = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass;
result += primaryVelocity * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
}
return result;
}真是个丑陋的小东西。注意观察此例中的 acc 变量是如何被赋值两次的。acc 变量有两个责任:第一是保存第一个力造成的初始加速度;第二是保存两个力共同造成的加速度。这就是我想要分解的东西。
在尝试理解变量被如何使用时,如果编辑器能高亮显示一个符号(symbol)在函数内或文件内出现的所有位置,会相当便利。大部分现代编辑器都可以轻松做到这一点。
首先,我在函数开始处修改这个变量的名称,并将新变量声明为 const。接着,我把新变量声明之后、第二次赋值之前对 acc 变量的所有引用,全部改用新变量。最后,我在第二次赋值处重新声明 acc 变量:
function distanceTravelled (scenario, time) {
let result;
const primaryAcceleration = scenario.primaryForce / scenario.mass;
let primaryTime = Math.min(time, scenario.delay);
result = 0.5 * primaryAcceleration * primaryTime * primaryTime;
let secondaryTime = time - scenario.delay;
if (secondaryTime > 0) {
let primaryVelocity = primaryAcceleration * scenario.delay;
let acc = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass;
result += primaryVelocity * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
}
return result;
}新变量的名称指出,它只承担原先 acc 变量的第一个责任。我将它声明为 const,确保它只被赋值一次。然后,我在原先 acc 变量第二次被赋值处重新声明 acc。现在,重新编译并测试,一切都应该没有问题。
然后,我继续处理 acc 变量的第二次赋值。这次我把原先的变量完全删掉,代之以一个新变量。新变量的名称指出,它只承担原先 acc 变量的第二个责任:
function distanceTravelled (scenario, time) {
let result;
const primaryAcceleration = scenario.primaryForce / scenario.mass;
let primaryTime = Math.min(time, scenario.delay);
result = 0.5 * primaryAcceleration * primaryTime * primaryTime;
let secondaryTime = time - scenario.delay;
if (secondaryTime > 0) {
let primaryVelocity = primaryAcceleration * scenario.delay;
const secondaryAcceleration = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass;
result += primaryVelocity * secondaryTime +
0.5 * secondaryAcceleration * secondaryTime * secondaryTime;
}
return result;
}现在,这段代码肯定可以让你想起更多其他重构手法。尽情享受吧。(我敢保证,这比吃苏格兰布丁强多了——你知道他们都在里面放了些什么东西吗?1 )
范例:对输入参数赋值
另一种情况是,变量是以输入参数的形式声明又在函数内部被再次赋值,此时也可以考虑拆分变量。例如,下列代码:
function discount (inputValue, quantity) {
if (inputValue > 50) inputValue = inputValue - 2;
if (quantity > 100) inputValue = inputValue - 1;
return inputValue;
}这里的 inputValue 有两个用途:它既是函数的输入,也负责把结果带回给调用方。(由于 JavaScript 的参数是按值传递的,所以函数内部对 inputValue 的修改不会影响调用方。)
在这种情况下,我就会对 inputValue 变量做拆分。
function discount (originalInputValue, quantity) {
let inputValue = originalInputValue;
if (inputValue > 50) inputValue = inputValue - 2;
if (quantity > 100) inputValue = inputValue - 1;
return inputValue;
}然后用变量改名(137)给两个变量换上更好的名字。
function discount (inputValue, quantity) {
let result = inputValue;
if (inputValue > 50) result = result - 2;
if (quantity > 100) result = result - 1;
return result;
}我修改了第二行代码,把 inputValue 作为判断条件的基准数据。虽说这里用 inputValue 还是 result 效果都一样,但在我看来,这行代码的含义是“根据原始输入值做判断,然后修改结果值”,而不是“根据当前结果值做判断”——尽管两者的效果恰好一样。
Footnotes
-
苏格兰布丁(haggis)是一种苏格兰菜,把羊心等内脏装在羊胃里煮成。由于它被羊胃包成一个球体,因此可以像球一样踢来踢去,这就是本例的由来。“把羊心装在羊胃里煮成……”,呃,有些人难免对这道菜恶心,Martin Fowler 想必是其中之一。——译者注 ↩