反向重构:以查询取代参数(324)

  targetTemperature(aPlan)
 
function targetTemperature(aPlan) {
  currentTemperature = thermostat.currentTemperature;
  // rest of function...
 
 
  targetTemperature(aPlan, thermostat.currentTemperature)
 
function targetTemperature(aPlan, currentTemperature) {
  // rest of function...

动机

在浏览函数实现时,我有时会发现一些令人不快的引用关系,例如,引用一个全局变量,或者引用另一个我想要移除的元素。为了解决这些令人不快的引用,我需要将其替换为函数参数,从而将处理引用关系的责任转交给函数的调用者。

需要使用本重构的情况大多源于我想要改变代码的依赖关系——为了让目标函数不再依赖于某个元素,我把这个元素的值以参数形式传递给该函数。这里需要注意权衡:如果把所有依赖关系都变成参数,会导致参数列表冗长重复;如果作用域之间的共享太多,又会导致函数间依赖过度。我一向不善于微妙的权衡,所以“能够可靠地改变决定”就显得尤为重要,这样随着我的理解加深,程序也能从中受益。

如果一个函数用同样的参数调用总是给出同样的结果,我们就说这个函数具有“引用透明性”(referential transparency),这样的函数理解起来更容易。如果一个函数使用了另一个元素,而后者不具引用透明性,那么包含该元素的函数也就失去了引用透明性。只要把“不具引用透明性的元素”变成参数传入,函数就能重获引用透明性。虽然这样就把责任转移给了函数的调用者,但是具有引用透明性的模块能带来很多益处。有一个常见的模式:在负责逻辑处理的模块中只有纯函数,其外再包裹处理 I/O 和其他可变元素的逻辑代码。借助以参数取代查询,我可以提纯程序的某些组成部分,使其更容易测试、更容易理解。

不过以参数取代查询并非只有好处。把查询变成参数以后,就迫使调用者必须弄清如何提供正确的参数值,这会增加函数调用者的复杂度,而我在设计接口时通常更愿意让接口的消费者更容易使用。归根到底,这是关于程序中责任分配的问题,而这方面的决策既不容易,也不会一劳永逸——这就是我需要非常熟悉本重构(及其反向重构)的原因。

做法

对执行查询操作的代码使用提炼变量(119),将其从函数体中分离出来。

现在函数体代码已经不再执行查询操作(而是使用前一步提炼出的变量),对这部分代码使用提炼函数(106)。

给提炼出的新函数起一个容易搜索的名字,以便稍后改名。

使用内联变量(123),消除刚才提炼出来的变量。

对原来的函数使用内联函数(115)。

对新函数改名,改回原来函数的名字。

范例

我们想象一个简单却又烦人的温度控制系统。用户可以从一个温控终端(thermostat)指定温度,但指定的目标温度必须在温度控制计划(heating plan)允许的范围内。

class HeatingPlan…

get targetTemperature() {
  if (thermostat.selectedTemperature > this._max) return this._max;
  else if (thermostat.selectedTemperature < this._min) return this._min;
  else return thermostat.selectedTemperature;
}

调用方…

if (thePlan.targetTemperature > thermostat.currentTemperature) setToHeat();
else if (thePlan.targetTemperature<thermostat.currentTemperature)setToCool();
else setOff();

系统的温控计划规则抑制了我的要求,作为这样一个系统的用户,我可能会感到很烦恼。不过作为程序员,我更担心的是 targetTemperature 函数依赖于全局的 thermostat 对象。我可以把需要这个对象提供的信息作为参数传入,从而打破对该对象的依赖。

首先,我要用提炼变量(119)把“希望作为参数传入的信息”提炼出来。

class HeatingPlan…

get targetTemperature() {
 const selectedTemperature = thermostat.selectedTemperature;
 if      (selectedTemperature > this._max) return this._max;
 else if (selectedTemperature < this._min) return this._min;
 else return selectedTemperature;
}

这样可以比较容易地用提炼函数(106)把整个函数体提炼出来,只剩“计算参数值”的逻辑还在原地。

class HeatingPlan…

get targetTemperature() {
 const selectedTemperature = thermostat.selectedTemperature;
 return this.xxNEWtargetTemperature(selectedTemperature);
}
 
xxNEWtargetTemperature(selectedTemperature) {
 if      (selectedTemperature > this._max) return this._max;
 else if (selectedTemperature < this._min) return this._min;
 else return selectedTemperature;
}

然后把刚才提炼出来的变量内联回去,于是旧函数就只剩一个简单的调用。

class HeatingPlan…

get targetTemperature() {
  return this.xxNEWtargetTemperature(thermostat.selectedTemperature);
}

现在可以对其使用内联函数(115)。

调用方…

if (thePlan.xxNEWtargetTemperature(thermostat.selectedTemperature) >
   thermostat.currentTemperature)
 setToHeat();
else if (thePlan.xxNEWtargetTemperature(thermostat.selectedTemperature) <
     thermostat.currentTemperature)
 setToCool();
else
 setOff();

再把新函数改名,用回旧函数的名字。得益于之前给它起了一个容易搜索的名字,现在只要把前缀去掉就行。

调用方…

if (thePlan.targetTemperature(thermostat.selectedTemperature) >
   thermostat.currentTemperature)
 setToHeat();
else if (thePlan.targetTemperature(thermostat.selectedTemperature) <
     thermostat.currentTemperature)
 setToCool();
else
 setOff();

class HeatingPlan…

targetTemperature(selectedTemperature) {
 if (selectedTemperature > this._max) return this._max;
 else if (selectedTemperature < this._min) return this._min;
 else return selectedTemperature;
}

调用方的代码看起来比重构之前更笨重了,这是使用本重构手法的常见情况。将一个依赖关系从一个模块中移出,就意味着将处理这个依赖关系的责任推回给调用者。这是为了降低耦合度而付出的代价。

但是,去除对 thermostat 对象的耦合,并不是本重构带来的唯一收益。HeatingPlan 类本身是不可变的——字段的值都在构造函数中设置,任何函数都不会修改它们。(不用费心去查看整个类的代码,相信我就好。)在不可变的 HeatingPlan 基础上,把对 thermostat 的依赖移出函数体之后,我又使 targetTemperature 函数具备了引用透明性。从此以后,只要在同一个 HeatingPlan 对象上用同样的参数调用 targetTemperature 函数,我会始终得到同样的结果。如果 HeatingPlan 的所有函数都具有引用透明性,这个类会更容易测试,其行为也更容易理解。

JavaScript 的类模型有一个问题:无法强制要求类的不可变性——始终有办法修改对象的内部数据。尽管如此,在编写一个类的时候明确说明并鼓励不可变性,通常也就足够了。尽量让类保持不可变通常是一个好的策略,以参数取代查询则是达成这一策略的利器。