《重构 改善既有代码的设计》(第九章)【重新组织数据】

拆分变量(Split Variable)

字段改名(Rename Field)

以查询取代派生变量(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; }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值