🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
反向重构:以查询取代参数(324) ![](https://box.kancloud.cn/b8cd553aa1a39f45f32023f6de5155a9_512x237.jpeg) ``` targetTemperature(aPlan) function targetTemperature(aPlan) { currentTemperature = thermostat.currentTemperature; // rest of function... ``` ![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg) ``` 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的类模型有一个问题:无法强制要求类的不可变性——始终有办法修改对象的内部数据。尽管如此,在编写一个类的时候明确说明并鼓励不可变性,通常也就足够了。尽量让类保持不可变通常是一个好的策略,以参数取代查询则是达成这一策略的利器。