9.1 拆分变量(Split Variable)
何时该拆分
查看某个变量在一段逻辑内被多次赋值的情况(累加收集结果这种不算)
常见于某个复杂运算需要多个临时变量,图省事就让一个临时变量担起所有大任
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 function distanceTravelled (scenario, time) { let result; let acc = scenario.primaryForce / scenario.mass; let primaryTime = Math.min(time, scenario.delay); result = 0.5 * acc * primaryTime * primaryTime; //上面这段还很正常,下面开始加料了 let secondaryTime = time - scenario.delay; if (secondaryTime > 0) { let primaryVelocity = acc * scenario.delay; // 对acc重复使用并设置为其他用途 acc = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass; result += primaryVelocity * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime; } return result; } /////////////////////////////////////////// function distanceTravelled (scenario, time) { let result; const primaryAcceleration = scenario.primaryForce / scenario.mass; let primaryTime = Math.min(time, scenario.delay); result = 0.5 * primaryAcceleration * primaryTime * primaryTime; let secondaryTime = time - scenario.delay; if (secondaryTime > 0) { let primaryVelocity = primaryAcceleration * scenario.delay; const secondaryAcceleration = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass; result += primaryVelocity * secondaryTime + 0.5 * secondaryAcceleration * secondaryTime * secondaryTime; } return result; }
比如一些加速度运动公式、通过复杂公式拟合曲线结果等等,如果不把变量拆分(命名也很重要),后来阅读者在不了解公式的情况下,很难理解意图。
如果语言支持的话,可以将这些拆分的变量设置为静态变量,保证职责单一的同时也避免后来者随意修改。
9.2 字段改名(Rename Field)
何时该改名
数据结构是理解程序行为的关键,需要保持清晰、整洁
对数据的理解就越深,所以很有必要把加深的理解融入程序中,其中改名是一个很常见的行为
注意要点
如果要改名的字段作用域很小,直接一步到位全改了即可,ide也提供了方便的工具
如果作用域很广,优先考虑使用【封装变量】先让字段处于可控的范围,再来进行改名操作
封装虽然有很多额外成本,但却让改名这个操作不容易出错
比如lua这种弱类型语言,如果一个类的变量没有合理封装,而是随意外部拿取使用,都不敢想象你全局搜索这个变量名出来一堆结果时的绝望,还得小心其他变量是否采用了一样的命名。
9.3 以查询取代派生变量(Replace Derived Variable with Query)
1 2 3 4 5 6 7 8 9 10 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;}
为什么需要以查询取代
可变数据是软件中最大的错误源头之一(总有在你意料之外的地方修改了数据)
所以要尽量少使用可变数据(但不太现实)
一些变量能通过计算出来,可以考虑不缓存,这样也变相减少了可变数据的使用
同时尽量把可变数据的作用域限制在最小范围
在游戏还有一些常用情况,变量记录了一个有生命周期的对象,当对象被消耗时没有及时更新变量的引用。又或许使用了对象池技术,变量的记录指向已经不是预期的结果。
这时候如果想避免更新不到位的情况,可以以查询取代
注意要点
需要确保计算出来的值和原有变量值一致
可以保留两者,加一些断言机制跑一段时间
9.4 将引用对象改为值对象(Change Reference to Value)
1 2 3 4 5 6 7 8 class Product { applyDiscount(arg) {this._price.amount -= arg;} class Product { applyDiscount(arg) { this._price = new Money(this._price.amount - arg, this._price.currency); }
引用对象和值对象的差异
两者最明显的差异在于如何更新内部对象的属性
引用对象在更新其属性时,保留原对象不动,更新内部对象的属性
值对象替换整个内部对象,新换上的对象继承了原有属性值和修改的属性值
何时要使用值对象
数据是否可变,不可变的数据可以放心传给程序各个模块,不用担心源数据被修改
程序各处复制值对象,而不必操心维护内存链接
值对象在分布式系统和并发系统中尤为有用
注意要点
值变量如果值相等时被认为是同一个变量
值对象也需要同样的机制,意味着需要重写equal函数,不能去比较引用地址
如果值对象里面有对象属性,也需要改成值对象
9.5 将值对象改为引用对象(Change Value to Reference)
1 2 3 let customer = new Customer(customerData); let customer = customerRepository.get(customerData.id);
何时要使用引用对象
从上节的讨论可知,如果数据不可变,使用值对象和引用对象都可以,值对象可以避免出错(但也有存在很多份实例的内存问题)
但如果是数据可变,使用值对象需要考虑该对象属性被修改时需不需要同步所有的值对象
比如很多个系统缓存了玩家单位,玩家的属性发生变更时,所有系统需不需要拿到最新版本。引用对象可以很简单处理这种更新,但如果是值对象,则要逐一更新,很难避免遗漏。
使用引用对象需要有个全局中心(管理器)去获取这个引用对象,保证获取的是同一个对象。常用做法是把这个全局中心设置为单例,但如果不想过渡使用全局对象,也可以在系统构造的时候传入这个全局中心。
参考链接