《重构 改善既有代码的设计》(第五,六章)【介绍重构名录,第一组重构】

介绍重构名录

重构的记录格式

  • 名称(name)
  • 速写(sketch)
  • 动机(motivation)
  • 做法(mechanics)
  • 范例(examples)

第一组重构

提炼函数(Extract Function)

内联函数(Inline Function)

提炼变量(Extract Variable)

内联变量(Inline Variable)

改变函数声明(Change Function Declaration)

封装变量(Encapsulate Variable)

变量改名(Rename Variable)

引入参数对象(Introduce Parameter Object)

函数组合成类(Combine Functions into Class)

函数组合成变换(Combine Functions into Transform)

拆分阶段(Split Phase)

提炼函数

动机

提炼函数的最佳观点是"将意图与实现分开",如果需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。

写非常小的函数— 通常只有几行的长度。作者认为,一个函数一旦超过6行,就开始散发臭味。短函数常常能让编译器的优化功能运转更良好,因为短函数可以更容易地被缓存。

做法

命名很重要,要根据函数的意图来对它命名(以它"做什么"来命名)。

内联函数

提炼函数的相反操作,一般在代码中嵌套太多的间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,造成在这些委托动作之间晕头转向,通常会使用内联函数。

还有一种情况,代码一组函数不合理,可以先内联到一个大函数中,然后在用喜欢的方式提炼出小函数。

提炼变量

反向重构是内联变量。

局部变量可以帮助我们将表达式分解为比较容易管理的形式。

要提炼变量,以为着要给代码中一个表达式命名。如果这个名字只在当前的函数中有意义,那么提炼变量是个不错的选择;如果这个变量名在更宽的上下文中也有意义,将其暴露出来,通常以函数的形式。

内联变量

反向重构是提炼变量。

有时候,变量名并不比表达式本身更具表现力。且变量可能会妨碍重构附近的代码。若果真如此,就应该通过内联的方法消除变量。

改变函数声明

函数是将程序拆分成小块的主要方式。函数声明则展现了如何将这些小块组合在一起工作。

函数的参数列表阐述了函数如何与外部世界共处。修改参数列表不仅能增加函数的应用范围,还能改变连接一个模块所需的条件,从而去除不必要的耦合。

如果要重构的函数属于一个具有多态性的类,那么对于该函数的每个实现版本,都需要通过"提炼出一个新函数"的方式添加一层间接,并把就函数的调用转发给新函数。如果函数的多态性是在一个类继承体系中体现,那么只需要在超类上转发即可;如果各个实现类之间并没有一个共同的超类,那么久需要在每个实现类上做转发。

如果要重构一个已对外发布的API,在提炼出新函数之后,可以暂停重构,将原来的函数声明为"不推荐使用"(deprecated),然后给客户端一点时间转为使用新函数。

封装变量

如果想要搬移一处被广泛使用的数据,最好的办法往往是先以函数形式封装所有对该数据的访问。就能把"重新组织数据"的困难任务转化为"重新组织函数"这个相对较简单的任务。

封装数据能提供一个清晰的观测点,可以由此监控数据的变化和使用情况。对于所有可变的数据,只要它的作用于超出单个函数,就可以将其封装起来,只允许通过函数访问。数据的作用域越大,封装就越重要。

例子

// 重构前
let defaultOwner = { firstName: "Martin", lastName: "Fowler" };
spaceship.owner = defaultOwner;
// 更新这段数据
defaultOwner = { firstName: "Rebecca", lastName: "Parsons" };

// 开始重构
// 定义读取和写入这段数据的函数,做基础的封装
function getDefaultOwner() { return defaultOwner; }
function setDefaultOwner(arg) { defaultOwner = arg; }
// 取值
spaceship.owner = getDefaultOwner();
// 赋值
setDefaultOwner({ firstName: "Rebecca", lastName: "Parsons" });

封装能控制对该数据结构的访问和重新赋值,但并不能控制对结构内部数据项的修改。如果需要把封装做的更深入,不仅控制对变量引用的修改,还要控制对变量内容的修改。可以有两种做法:

  • 修改取值函数,使其返回该数据的副本

    let defaultOwner = { firstName: "Martin", lastName: "Fowler" };
    export function getDefaultOwner() { return Object.assign({}, defaultOwner); }
    export function setDefaultOwner(arg) { defaultOwner = arg; }
    
  • 阻止对数据的修改,比如通过封装记录实现效果

    let defaultOwner = { firstName: "Martin", lastName: "Fowler" };
    export function getDefaultOwner() { return new Person(defaultOwner) };
    export function setDefaultOwner(arg) { defaultOwner = arg; };
    
    class Person {
      constructor(data) {
        this._lastName = data.lastName;
        this._firstName = data.firstName;
      }
      
      get lastName() { return this._lastName; }
      get firstName() { return this._firstName; }
    }
    

变量改名

如果要改名的变量只作用于一个函数(临时变量或者参数),对其改名是最简单的,找到变量的所有引用,修改过来就行。

如果变量的作用于不止于单个函数,问题就很容易出现。通常的做法是运用封装变量。在封装的变量函数内就可以随意的修改变量的名称。

如果要改名的变量是一个常量(或者在客户端看来就像是常量的元素),可以复制这个常量,这样既不需要封装,又可以逐步完成改名。直到全部修改完成后,再删掉旧的常量。

引入参数对象

一组数据项如果总是结伴同行,出没于一个又一个函数。这样一组数据就是所谓的数据泥团,可以用数据结构来代替。

将数据组织成数据结构很有价值。使用新的数据结构,参数的参数列表也能缩短。并且经过重构之后,所有使用该数据结构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一致性。

而除了上面的好处,更有意义的是,一旦识别出新的数据结构,就可以重组程序的行为来使用这些结构。这个过程会使代码发生巨大的变化,将数据结构提升为新的抽象概念,可以帮助更好地理解问题域。

例子

// 重构前
const station = {
  name: "ZB1",
  readings: [
    { temp: 47, time: '2016-11-10 09:10' },
    { temp: 53, time: '2016-11-10 09:10' },
    { temp: 58, time: '2016-11-10 09:10' },
    { temp: 53, time: '2016-11-10 09:10' },
    { temp: 51, time: '2016-11-10 09:10' }
  ]
};
function readingOutsideRange(station, min, max) {
  return station.readings.filter(r => r.temp < min || r.temp > max);
}

alerts = readingOutsideRange(station, operationPlan.temperatureFloor, operationPlan.temperatureCeiling);

上面将operationPlan对象拆开分两个字段传入,实际上很不方便扩展。较好的方式是合成为一个对象使用,而最好的方式是组合的数据声明为一个类:

class NumberRange {
	constructor(min, max) {
    this._data = { min: min, max: max };
  }
  get min() { return this._data[min]; }
  get max() { return this._data[max]; }
}

实际上演变会先在readingOutsideRange函数中增加一个range参数,该参数实际是NumberRange实例。然后把min、max的实现分别替换成实例里的实现,再去掉min、max参数。具体演变后的结果会变为

function readingOutsideRange(station, range) {
  return station.readings.filter(r => r.temp < range.min || r.temp >range. max);
}

而这个还不是最终的,我们还可以把return里对范围的实现转移到NumberRange类中,抽象出一个关于范围的具体类。演变后的结果为

function readingOutsideRange(station, range) {
  return station.readings.filter(r => !range.contains(r.temp));
}

// NumberRange完整类实现
class NumberRange {
	constructor(min, max) {
    this._data = { min: min, max: max };
  }
  get min() { return this._data[min]; }
  get max() { return this._data[max]; }
  
  contains(arg) {
    return arg >= this.min && arg <= this.max;
  }
}

函数组合成类

类把数据与函数捆绑到同一个环境中,将一部分数据与函数暴露给其他程序元素以便协作。**如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),就可以将这些函数和参数组建成一个类。**类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数。

例子

// 重构前
reading = { customer: 'ivan', quantity: 10, mounth: 5, year: 2017 };
// 客户端1通过reading对象,计算基础费用(base charge)
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
// 客户端2在计算基础费用后,还要判断税收
const aReading = acquireReading();
const base = baseRate(aReading.month, aReading.year) * aReading.quantity;
const texableCharge = Math.max(0, base - texThreshold(aReading.year));

通过上面的例子,大部分的人会将计算基础费用的提炼成函数,改版后

const aReading = acquireReading();
const baseCharge = calculateBaseCharge(aReading);

function calculateBaseCharge(aReading) {
  reutrn baseRate(aReading.month, aReading.year) * aReading.quantity;
}

然而这并不是最好的方式,最好的方式是运用封装记录将记录变成类

class Reading {
  constructor(data) {
    this._customer = data.customer;
    this._quantity = data.quantity;
    this._month = data.month;
    this._year = data.year;
  }
  get customer() { return this._customer; }
  get quantity() { return this._quantity; }
  get month() { return this._month; }
  get year() { return this._year; }
}

const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = calculateBaseCharge(aReading); 

function calculateBaseCharge(aReading) {
  reutrn baseRate(aReading.month, aReading.year) * aReading.quantity;
}

同样,我们可以把calculateBaseCharge搬到新类中,同时改名为baseCharge

class Reading {
  constructor(data) {
    this._customer = data.customer;
    this._quantity = data.quantity;
    this._month = data.month;
    this._year = data.year;
  }
  get customer() { return this._customer; }
  get quantity() { return this._quantity; }
  get month() { return this._month; }
  get year() { return this._year; }
  
  get baseCharge() {
    reutrn baseRate(this.month, this.year) * this.quantity;
  }
}

const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseChargeAmount = aReading.baseCharge; 

作者在编写类时,把原本应该设置为计算的方法改为了字段的表示,如getBaseCharge()改为使用get baseCharge(),他强烈建议后者方式,符合"统一访问原则"(Uniform Access Principle)。

函数组合成变换

函数组合成变换的替代方案是函数组合成类。函数组合成变换实际就是把所有计算派生数据的逻辑收拢到一处,这样始终可以在固定的地方找到和更新这些逻辑,避免到处重复。

一个方式是采用数据变换函数:接受源数据作为输入,计算出所有的派生数据,将派生数据以字段形式填入输出数据。有了变换函数,就始终只需要到变换函数中去检查计算派生数据的逻辑。

如果只是为了避免计算派生数据的逻辑到处重复,用提炼函数也能避免重复。用变换的目的是为了将孤立存在的函数统一放在一起,这样用起来更方便。引入变换(或者类)都是为了让相关的逻辑找起来方便。

例子

还是以函数组合成类的例子为例

// 重构前
reading = { customer: 'ivan', quantity: 10, mounth: 5, year: 2017 };
// 客户端1通过reading对象,计算基础费用(base charge)
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
// 客户端2在计算基础费用后,还要判断税收
const aReading = acquireReading();
const base = baseRate(aReading.month, aReading.year) * aReading.quantity;
const texableCharge = Math.max(0, base - texThreshold(aReading.year));

通过上面的例子,大部分的人会将计算基础费用的提炼成函数,改版后

const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);

function calculateBaseCharge(aReading) {
  reutrn baseRate(aReading.month, aReading.year) * aReading.quantity;
}

上例到这后使用合成类达到重构,这边我们用合成变换的方式处理。把所有这些计算派生数据的逻辑搬移到一个变换函数中,该函数接受原始的"读数"作为输入,输出则是增强的"读数"记录,其中包含所有共用的派生数据。

function enrichReading(original) {
  const result = _.cloneDeep(original);
  return result;
}

作者在这介绍了他的命名方式,如果函数返回的本质上仍是原来的对象,只是添加了更多的信息在上面,会用"enrich"(增强)这个词来给它命名。如果它生成的时跟原来完全不同的对象,会用"transform"(变换)来命名它。

首先,只是用enrichReading函数来增强"读数"记录

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);

接下来把calculateBaseCharge搬移到增强函数中:

function enrichReading(original) {
  const result = _.cloneDeep(original);
  result.baseCharge = calculateBaseCharge(result);
  return result;
}

拆分阶段

每当看见一段代码在同时处理两件不同的事,就可以考虑拆分成各自独立的模块,这样到了需要修改的时候,就可以单独处理每个主题,而不必同时在脑子里考虑两个不同的主题。

例子

// 重构前
function priceOrder(product, quantity, shippingMethod) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * priduct.discountRate;
  const shippingPerCase = (basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  const shippingCost = quantity * shippingPerCase;
  const price = basePrice - discount + shippingCost;
  return price;
}

重构前的代码可以看出,其实函数中包括了两个阶段,上部分阶段根据商品(product)信息计算订单中与商品相关的价格,随后两个根据配送(shipping)信息计算配送成本。

首先提炼函数把计算配送成本的逻辑提炼出来

function priceOrder(product, quantity, shippingMethod) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * priduct.discountRate;
  const price = applyShipping(basePrice, shippingMethod, quantity, discount);
  return price;
}

function applyShipping(basePrice, shippingMethod, quantity, discount) {
  const shippingPerCase = (basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  const shippingCost = quantity * shippingPerCase;
  const price = basePrice - discount + shippingCost;
  return price;
}

接下来考虑到参数的数量可能会很多,可以消除参数。将basePrice、quantity、discount放在参数对象中,更改后

function priceOrder(product, quantity, shippingMethod) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * priduct.discountRate;
  const priceData = { basePrice: basePrice, quantity: quantity, discount: discount };
  const price = applyShipping(priceData, shippingMethod);
  return price;
}

function applyShipping(priceData, shippingMethod) {
  const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  const shippingCost = priceData.quantity * shippingPerCase;
  const price = priceData.basePrice - priceData.discount + shippingCost;
  return price;
}

接着将第一阶段代码提炼成独立的函数

function priceOrder(product, quantity, shippingMethod) {
  const priceData = calculatePricingData(product, quantity);
  const price = applyShipping(priceData, shippingMethod);
  return price;
}

function calculatePricingData(product, quantity) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * priduct.discountRate;
  return { basePrice: basePrice, quantity: quantity, discount: discount };
}

function applyShipping(priceData, shippingMethod) {
  const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  const shippingCost = priceData.quantity * shippingPerCase;
  const price = priceData.basePrice - priceData.discount + shippingCost;
  return price;
}

最后,内联函数

function priceOrder(product, quantity, shippingMethod) {
  const priceData = calculatePricingData(product, quantity);
  return applyShipping(priceData, shippingMethod);
}

function calculatePricingData(product, quantity) {
  const basePrice = product.basePrice * quantity;
  const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * priduct.discountRate;
  return { basePrice: basePrice, quantity: quantity, discount: discount };
}

function applyShipping(priceData, shippingMethod) {
  const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase;
  const shippingCost = priceData.quantity * shippingPerCase;
  return priceData.basePrice - priceData.discount + shippingCost;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值