介绍重构名录
重构的记录格式
- 名称(name)
- 速写(sketch)
- 动机(motivation)
- 做法(mechanics)
- 范例(examples)
第一组重构
改变函数声明(Change Function Declaration)
引入参数对象(Introduce Parameter Object)
函数组合成类(Combine Functions into Class)
函数组合成变换(Combine Functions into Transform)
提炼函数
动机
提炼函数的最佳观点是"将意图与实现分开",如果需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。
写非常小的函数— 通常只有几行的长度。作者认为,一个函数一旦超过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;
}