多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
![](https://box.kancloud.cn/a512746efb5c8919128504565ae906e2_575x172.jpeg) ``` get discountedTotal() {return this._discountedTotal;} set discount(aNumber) {  const old = this._discount;  this._discount = aNumber;  this._discountedTotal += old - aNumber; } ``` ![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg) ``` 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`这个属性,而不把它内联消去。