以查询取代派生变量(Replace Derived Variable with Query)
将引用对象改为值对象(Change Reference to Value)
将值对象改为引用对象(Change Value to Reference)
拆分变量
变量有各种不同的用途,其中某些用途会很自然地导致临时变量被多次赋值。"循环变量"和"结果收集变量"就是两个例子:循环变量会随循环的每次运行而改变;结果收集变量负责将"通过整个函数的运算"而构成的某个值收集起来。
除了以上两种情况,还有很多变量用于保存一段冗长代码的运算结果,以便稍后使用。这种变量应该只被赋值一次。如果被赋值超过一次,就意味着它们在函数中承担了一个以上的责任。如果变量承担多个责任,就应该被替换(分解)为多个变量,每个变量只承担一个责任。
例子
计算一个苏格兰布丁运动的距离
// 重构前
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 = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass;
result += primaryVelocity * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
}
return result;
}
这段代码中acc被赋值了两次。第一是保存第一个力造成的初始加速度;第二是保存两个力共同造成的加速度。
重构后的目的是让acc职责单一,因此把第一个acc重命名为primaryAccelaration;第二个acc重命名为secondAccelaration;同时将这两个变量都用const声明,保证只被赋值一次。
// 重构后
function distanceTravelled(scenario, time) {
let result;
const primaryAccelaration = scenario.primaryForce / scenario.mass;
let primaryTime = Math.min(time, scenario.delay);
result = 0.5 * primaryAccelaration * primaryTime * primaryTime;
let secondaryTIme = time - scenario.delay;
if (secondaryTime > 0) {
let primaryVelocity = acc * scenario.delay;
const secondAccelaration = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass;
result += primaryVelocity * secondaryTime + 0.5 * secondAccelaration * secondaryTime * secondaryTime;
}
return result;
}
例2,对输入参数赋值
变量是以输入参数的形式声明又在函数内部被再次赋值,此时也可以考虑拆分变量
// 重构前
function discount(inputValue, quantity) {
if (inputValue > 50) inputValue = inputValue - 2;
if (quantity > 100) inputValue = inputValue - 1;
return inputValue;
}
inputValue有两个用途:既是函数的输入,也负责把结果带回给调用方。先对inputValue变量做拆分,然后用变量改名给两个变量换上更好地名字
// 重构后
function discount(inputValue, quantity) {
let result = inputValue;
if (inputValue > 50) result = result - 2;
if (quantity > 100) result = result - 1;
return result;
}
字段改名
对于字段改名,如果记录的作用域较小,可以直接修改所有该字段的代码,直接测试,重构结束。
否则如果记录没有封装,先封装记录,然后在对象内部对私有字段改名,对应调整内部访问该字段的函数。
例子
// 构造前
const organization = { name: 'Acme Gooseberries', country: 'GB' };
重构要把原来的name改为title字段,且该对象被很多地方使用,有些代码会更新name字段。
首先用封装记录将记录封装起来
class Organization {
constructor(data) {
this._name = data.name;
this._country = data.country;
}
get name() { return this._name; }
set name(aString) { this._name = aString; }
get country() { return this._country; }
set country(aCountryCode) { this._country = aCountryCode; }
}
const organization = new Organization({ name: 'Acme Gooseberries', country: 'GB' });
记录已经被封装成了类,接下来字段改名,需要对4个地方进行修改:取值函数,设值函数,构造函数和内部数据结构。如果怕一次性修改会带来太多错误,可以小步修改。
先修改内部数据结构
class Organization {
constructor(data) {
this._title = data.name;
this._country = data.country;
}
get name() { return this._title; }
set name(aString) { this._title = aString; }
get country() { return this._country; }
set country(aCountryCode) { this._country = aCountryCode; }
}
然后在构造函数中增加对title的判断
class Organization {
constructor(data) {
this._title = data.title ? data.title : data.name;
this._country = data.country;
}
get name() { return this._title; }
set name(aString) { this._title = aString; }
get country() { return this._country; }
set country(aCountryCode) { this._country = aCountryCode; }
}
接下来排查所有调用构造函数的地方,修改所有的name改为title,确保修改全部完成
const organization = new Organization({ title: 'Acme Gooseberries', country: 'GB' });
修改全部完成后,可以直接去除构造函数里对name的支持
class Organization {
constructor(data) {
this._title = data.title;
this._country = data.country;
}
get name() { return this._title; }
set name(aString) { this._title = aString; }
get country() { return this._country; }
set country(aCountryCode) { this._country = aCountryCode; }
}
最后对每个访问函数运用函数改名,end
// 重构后
class Organization {
constructor(data) {
this._title = data.title;
this._country = data.country;
}
get title() { return this._title; }
set title(aString) { this._title = aString; }
get country() { return this._country; }
set country(aCountryCode) { this._country = aCountryCode; }
}
const organization = new Organization({ name: 'Acme Gooseberries', country: 'GB' });
上面的例子重构过程,是对广泛使用的数据结构才用得上。如果该数据结构只在较小的范围中用到,直接一步到位地完成改名动作即可,不需要提前做封装。
以查询取代派生变量
有些变量其实可以很容易地随时计算出来。如果能去掉这些变量,也能消除可变性。计算常能更清晰地表达数据的含义,而且也避免了"源数据修改时忘了更新派生变量"的错误。
例子
// 重构前
class ProductionPlan {
get production() { return this._production; }
applyAdjustment(anAdjustment) {
this._adjustment.push(anAdjustmnet);
this.production += anAdjustment.amount;
}
}
这里在对生产计划做调整(修改 adjustment)时,不仅要把调整的信息修改保存下来,还要更据调整信息修改一个累计值—而这个累计值其实可以即时计算,而不必每次更新。
// 重构后
class ProductionPlan {
get production() {
return this._adjustment.reduce((sum, a) => sum + a.amount, 0);
}
applyAdjustment(anAdjustment) {
this._adjustment.push(anAdjustmnet);
}
}
将引用对象改为值对象
反向重构:将值对象改为引用对象
在把一个对象(或数据结构)嵌入另一个对象时,位于内部的对象可以被视为引用对象,也可以被视为值对象。
对于将内部对象视为引用对象,在更新其属性时,会保留原对象不动,更新内部对象的属性。
如果视为值对象,可以把内部对象的类也变成值对象。值对象是不可变的。因此可以放心地把不可变的数据值传给程序的其他部分,而不必担心对象中包装的数据被偷偷修改。可以在程序各处复制值对象,而不必操心维护内存链接。
如果想在几个对象之间共享一个对象,以便几个对象都能看见对共享对象的修改,那么这个共享的对象就应该是引用。
例子
// 重构前
class Person {
constructor() {
this._telephoneNumber = new TelephoneNumber();
}
get officeAreaCode() { return this._telephoneNumber.areaCode; }
set officeAreaCode(arg) { this._telephoneNumber.areaCode = arg; }
get officeNumber() { return this._telephoneNumber.number; }
set officeNumber(arg) { this._telephoneNumber.number = arg; }
}
class TelephoneNumber {
get areaCode() { return this._areaCode; }
set areaCode(arg) { this._areaCode = arg; }
get number() { return this._number; }
set number(arg) { this._number = arg; }
}
例子中存在一个指向新类的引用,可以使用将引用对象改为值对象将其变为值对象。
首先把TelephoneNumber类变为不可变。对它的字段运用移除设值函数。移除设值函数的第一步是,用改变函数声明把这两个字段的初始值加到构造函数中,并迫使构造函数设值函数。
class TelephoneNumber {
constructor(areaCode, number) {
this._areaCode = areaCode;
this._number = number;
}
get areaCode() { return this._areaCode; }
get number() { return this._number; }
}
接着查看设值函数的调用者,将它修改为重新赋值整个对象。
// 重构后
class Person {
get officeAreaCode() { return this._telephoneNumber.areaCode; }
set officeAreaCode(arg) { this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber); }
get officeNumber() { return this._telephoneNumber.number; }
set officeNumber(arg) { this._telephoneNumber = new TelephoneNumber(this.officeAreaCode, arg); }
}
将值对象改为引用对象
反向重构:将引用对象改为值对象
正如上面的重构方法所知,把数据作为值对象和引用对象都可以。但需要根据具体需求而定。
如果共享的数据需要更新,将其复制多份的做法就会遇到巨大的问题。对于这种情况,可以考虑将多分数据副本变成单一的引用,这样对一处数据的修改就会立即反映到所有引用的数据中。
如果要将值对象转换成引用对象,可以创建一个仓库对象,仓库对象存储所有唯一的值,在需要使用到值的构造函数中将仓库引用进来就可以了。
例子
// 重构前
class Order {
constructor(data) {
this._number = data.number;
this._customer = new Customer(data.customer);
}
get customer() { return this._customer; }
}
class Customer {
constructor(id) {
this._id = id;
}
get id() { return this._id; }
}
以上例子创建的Customer对象是值对象。也就是如果对5个相同的订单用Order创建后,其中一个做了修改,剩下的几个是不会有更新的反映的。
如果想每次都使用同一个Customer对象,就需要有一个地方存储这个对象。最简单的是建立一个仓库(即全局状态)。
let _repositoryData;
export function initialize() {
_repositoryData = {};
_repositoryData.customer = new Map();
}
export function registerCustomer(id) {
if (! _repositoryData.customer.has(id)) {
_repositoryData.customer.set(id, new Customer(id));
}
return findCustomer(id);
}
export function findCustomer(id) {
return _repositoryData.customer.get(id);
}
通过以上的仓库对象,允许根据ID注册顾客,并且对于一个ID只会创建一个Customer对象。
最后,只要修改Order中引用Customer对象的方式即可
// 重构后
class Order {
constructor(data) {
this._number = data.number;
this._customer = registerCustomer(data.customer);
}
get customer() { return this._customer; }
}