get discountedTotal() {return this._discountedTotal;}
set discount(aNumber) {
const old = this._discount;
this._discount = aNumber;
this._discountedTotal += old - aNumber;
}
get discountedTotal() {return this._baseTotal - this._discount;}
set discount(aNumber) {this._discount = aNumber;}动机
可变数据是软件中最大的错误源头之一。对数据的修改常常导致代码的各个部分以丑陋的形式互相耦合:在一处修改数据,却在另一处造成难以发现的破坏。很多时候,完全去掉可变数据并不现实,但我还是强烈建议:尽量把可变数据的作用域限制在最小范围。
有些变量其实可以很容易地随时计算出来。如果能去掉这些变量,也算朝着消除可变性的方向迈出了一大步。计算常能更清晰地表达数据的含义,而且也避免了“源数据修改时忘了更新派生变量”的错误。
有一种合理的例外情况:如果计算的源数据是不可变的,并且我们可以强制要求计算的结果也是不可变的,那么就不必重构消除计算得到的派生变量。因此,“根据源数据生成新数据结构”的变换操作可以保持不变,即便我们可以将其替换为计算操作。实际上,这是两种不同的编程风格:一种是对象风格,把一系列计算得出的属性包装在数据结构中;另一种是函数风格,将一个数据结构变换为另一个数据结构。如果源数据会被修改,而你必须负责管理派生数据结构的整个生命周期,那么对象风格显然更好。但如果源数据不可变,或者派生数据用过即弃,那么两种风格都可行。
做法
识别出所有对变量做更新的地方。如有必要,用拆分变量(240)分割各个更新点。
新建一个函数,用于计算该变量的值。
用引入断言(302)断言该变量和计算函数始终给出同样的值。
如有必要,用封装变量(132)将这个断言封装起来。
测试。
修改读取该变量的代码,令其调用新建的函数。
测试。
用移除死代码(237)去掉变量的声明和赋值。
范例
下面这个例子虽小,却完美展示了代码的丑陋。
class ProductionPlan…
get production() {return this._production;}
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
this._production += anAdjustment.amount;
}丑与不丑,全在观者。我看到的丑陋之处是重复——不是常见的代码重复,而是数据的重复。如果我要对生产计划(production plan)做调整(adjustment),不光要把调整的信息保存下来,还要根据调整信息修改一个累计值——后者完全可以即时计算,而不必每次更新。
但我是个谨慎的人。“可以即时计算”只是我的猜想——我可以用引入断言(302)来验证这个猜想。
class ProductionPlan…
get production() {
assert(this._production === this.calculatedProduction);
return this._production;
}
get calculatedProduction() {
return this._adjustments
.reduce((sum, a) => sum + a.amount, 0);
}放上这个断言之后,我会运行测试。如果断言没有失败,我就可以不再返回该字段,改为返回即时计算的结果。
class ProductionPlan…
get production() {
assert(this._production === this.calculatedProduction);
return this.calculatedProduction;
}然后用内联函数(115)把计算逻辑内联到 production 函数内。
class ProductionPlan…
get production() {
return this._adjustments
.reduce((sum, a) => sum + a.amount, 0);
}再用移除死代码(237)扫清使用旧变量的地方。
class ProductionPlan…
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
this._production += anAdjustment.amount;
}范例:不止一个数据来源
上面的例子处理得轻松愉快,因为 production 的值很明显只有一个来源。但有时候,累计值会受到多个数据来源的影响。
class ProductionPlan…
constructor (production) {
this._production = production;
this._adjustments = [];
}
get production() {return this._production;}
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
this._production += anAdjustment.amount;
}如果照上面的方式运用引入断言(302),只要 production 的初始值不为 0,断言就会失败。
不过我还是可以替换派生数据,只不过必须先运用拆分变量(240)。
constructor (production) {
this._initialProduction = production;
this._productionAccumulator = 0;
this._adjustments = [];
}
get production() {
return this._initialProduction + this._productionAccumulator;
}现在我就可以使用引入断言(302)。
class ProductionPlan…
get production() {
assert(this._productionAccumulator === this.calculatedProductionAccumulator);
return this._initialProduction + this._productionAccumulator;
}
get calculatedProductionAccumulator() {
return this._adjustments
.reduce((sum, a) => sum + a.amount, 0);
}接下来的步骤就跟前一个范例一样了。不过我会更愿意保留 calculatedProduction Accumulator 这个属性,而不把它内联消去。