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 这个属性,而不把它内联消去。