theme: github
重构的第一步:确保重构代码有一组可靠的测试!
重构的第一步:确保重构代码有一组可靠的测试!
重构的第一步:确保重构代码有一组可靠的测试!
基本概念
重构:对软件内部结构对一种调整,在不改变软件可观察行为对前提下,提高可理解性,降低修改成本
目的
- 改进软件的设计
- 使软件更易理解
- 帮助寻找隐性bug
- 提高编程速度
重构时机
- 预备性重构:添加新功能更加容易
- 帮助理解的重构:使代码更加易懂
- 捡垃圾式重构:每次清理都让代码更好一点
- 有计划的重构
- 替换性的重构:替换一些依赖模块
代码的坏味道
- 神秘命名:一个好的名字,清晰的表明自己的功能和用法(改变函数声明、变量改名、字段改名)
- 重复代码:在一个以上的地方看到相同的代码(提炼函数、移动语句、函数上移)
- 过长函数:函数越长,阅读复杂度越高(提炼函数、以查询取代临时变量、引入参数对象、以命令取代函数)
- 过长参数列表:参数过长容易使人迷惑(以查询去取代临时变量、参数对象)
- 全局数据:在项目中任何一处都可能改变它,且无法定位哪里发生改变(封装变量)
- 可变数据:数据修改容易发生难以预料的bug(封装变量、拆分变量)
- 发散式的变化:一个函数只负责一种类型的上下文状态(提炼函数)
- 霰弹式修改:发生变化时需要在不同的类中做修改(内联函数、内联类)
- 依恋情节:减少模块间的交互(搬移函数)
- 数据泥团:数据聚合在一起,拆分成粒度更小的方式(提炼类、参数对象)
- 基本类型偏执:使用错误的数据类型处理数据(以对象取代基本类型)
- 重复的switch:当你想增加一个选择分支时需要找到所有的分支确认(多态取代条件表达式)
- 循环语句:需要通读代码才明白循环内的语义(以管道代替循环)
- 冗赘的元素:程序元素增加代码结构,从而支持变化、促进复用,但有时候只是简单的函数(内联函数、内联类)
- 夸夸其谈通用性:放弃用不到的情况,比如各种各样的钩子,只做有限开发(移除死代码)
- 临时字段:仅仅为了某种特殊情况创建的字段
- 过长的消息链:A对象访问B对象,B对象访问C对象…直到E对象(隐藏委托关系)
- 中间人:过度委托(移除中间人)
- 内幕交易:模块之间交换数据(搬移函数、搬移字段、隐藏委托关系)
- 过大的类:单个类做了太多事情(提炼类)
- 异曲同工的类:相同类型的类应该保持接口一致(改变函数声明、搬移函数、提炼超类)
- 纯数据类:拥有一些字段以及用于访问字段的函数(封装记录)
- 被拒绝的馈赠:子类并不需要超类的大部分字段/函数(以委托取代子类)
- 注释:当你需要写注释的时候,先尝试重构把所有的注释变得多余
重构技术
提炼函数(内联函数)
将代码提炼到一个独立的函数中,当你需要花时间浏览一段代码才能弄清楚它在干什么的时候,就应该提炼函数
1
2
3
4function printOwing() {
... // printBanner
... // printDetails
}1
2
3
4
5
6
7function printOwing() {
printBanner()
printDetails()
return
function printBanner(){ ... }
function printDetails(){ ... }
}做法:
- 创建一个新函数,根据函数意图命名
- 将代码从源函数复制到新建函数中
- 仔细查看提炼代码、作用域引用的变量,判断是否需要通过参数传入
- 所有变量处理完毕后编译
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
34
35const invoices = [
{
customer: 'BigCo',
outstanding: [
{
amount: 10
},
{
amount: 20
}
]
},
{
customer: 'Helle',
outstanding: [
{
amount: 30
},
{
amount: 40
}
]
}
]
function printOwing(invoice) {
console.log('--- ---- ---')
console.log('--- Owes ---')
console.log('--- ---- ---')
let amounts = 0
for(let { amount } of invoice.outstanding) {
amounts += amount
}
console.log(`name: ${invoice.customer}`)
console.log(`amount: ${amounts}`)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function printOwing(invoice) {
printBanner()
let outstanding = amountFor(invoice)
printDetails(invoice, outstanding)
return
function printBanner(){
console.log('--- ---- ---')
console.log('--- Owes ---')
console.log('--- ---- ---')
}
function amountFor(invoice){
return invoice.outstanding.reduce((prev, item) => prev + item.amount, 0)
}
function printDetails(invoice, outstanding){
console.log(`name: ${invoice.customer}`)
console.log(`amount: ${outstanding}`)
}
}
invoices.forEach(printOwing)内联函数(提炼函数)
一些本来就很易读代码可以直接内联在源函数内,可以减少间接层
1
2
3
4
5
6function reportLines(aCustomer){
...
gatherCustomerData(...)
return
function gatherCustomerData(){...}
}1
2
3
4
5function reportLines(aCustomer){
...
... // gatherCustomerData
return lines
}做法
- 检查函数,确定其不具备多态性
- 找出这个函数的所有调用点
- 把调用点替换成函数本体
- 替换之后执行测试
1
2
3
4
5
6
7
8
9
10
11
12
13const customer = {
name: 'BigCo',
location: 'sz'
}
function reportLines(aCustomer){
const lines = []
gatherCustomerData(lines, aCustomer)
return lines
function gatherCustomerData(out, aCustomer){
out.push(['name', aCustomer.name])
out.push(['location', aCustomer.location])
}
}1
2
3
4
5
6function reportLines(aCustomer){
const lines = []
lines.push(['name', aCustomer.name])
lines.push(['location', aCustomer.location])
return lines
}提炼变量(内联变量)
将复杂冗长的表达式拆分成变量更加易读
1
2
3function price(order){
return ...
}1
2
3
4function price(order){
let basePrice
return ...
}做法
- 确认要提炼的表达式没有副作用
- 声明一个不可修改的变量,把想要提炼的表达式复制一份,以表达式的结果赋值给变量
- 用新变量取代原来的表达式
1
2
3
4
5
6function price(order){
// price is base price - quantity discount + shipping
return order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100)
}1
2
3
4
5
6
7function price(order){
// price is base price - quantity discount + shipping
let basePrice = order.quantity * order.itemPrice
let quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05
let shipping = Math.min(basePrice * 0.1, 100)
return basePrice - quantityDiscount + shipping
}内联变量(内联变量)
可以通过内联变量的方法消除局部变量
1
2
3
4function price(order){
let basePrice
return ...
}1
2
3function price(order){
return ...
}做法
- 检查确认变量赋值语句的右侧表达式是否存在副作用
- 找到第一处使用该变量的地方,将其替换成赋值语句右侧的表达式
1
2
3
4function price(order){
let basePrice = order.basePrice;
return (basePrice > 1000)
}1
2
3function price(order){
return order.basePrice > 1000
}改变函数声明
将函数、变量修改成语义更好的名称
1
2// before
function circum (x){...}1
2
3
4
5
6
7// 简单做法
function circumference (radius){...}
// 复杂做法
function circum (radius){
return circumference(radius)
function circumference (radius){...}
}简单做法
- 迁移一个参数,需要确定函数体内没有使用参数
- 修改函数声明
- 找出引用处,替换
迁移做法
- 如果有必要,先对函数体内部进行重构,使得后面提炼步骤易于展开
- 使用提炼函数将函数体提炼成一个函数
- 如果提炼出的函数需要新增参数,则参考简单做法
- 对旧函数使用内联函数
1
2
3function circum (radius){
return 2 * Math.PI * radius
}1
2
3
4
5
6
7
8
9
10
11// 简单做法
function circumference (radius){
return 2 * Math.PI * radius
}
// 复杂做法
function circum (radius){
return circumference(radius)
function circumference (radius){
return 2 * Math.PI * radius
}
}1
2
3function isNewEndLand(aCustomer){
return ['MA', 'CT', 'ME', 'VT', 'NH', 'NH', 'RI'].includes(aCustomer.address.state)
}1
2
3
4
5
6function isNewEndLand(aCustomer){
return checkState(aCustomer.address.state)
function checkState(state){
return ['MA', 'CT', 'ME', 'VT', 'NH', 'NH', 'RI'].includes(state)
}
}封装变量
将变量封装成函数调用的方式,方便修改和数据监控
1
2
3
4
5
6
7let defaultOwnerData = {...}
function defaultOwner(){
return Object.assign({}, defaultOwnerData)
}
function setDefaultOwner(newOwner){
return defaultOwnerData = newOwner
}做法
- 创建封装函数,在其中访问和更新变量
- 执行静态检查
- 逐一修改使用变量的代码,将其改成调用合适的封装函数
- 限制变量的可变性
1
2
3
4
5
6
7
8
9
10let defaultOwnerData = {
firstName: 'Mt',
lastName: 'Fl'
}
function defaultOwner(){
return Object.assign({}, defaultOwnerData)
}
function setDefaultOwner(newOwner){
return defaultOwnerData = newOwner
}变量改名
取一个好的名字,是好的开始
1
2let a = 'xy'
a = 'dq'1
2
3let _name = 'xy'
function name(){ return _name }
function setName(name){ _name = name }做法
- 如果变量被广泛引用,则运用封装变量将其封装
- 找到使用该变量的代码,注意修改
1
2
3
4
5let tpHd = '111';
let result = ''
result += `title: ${tpHd}\n`
tpHd = '222';
result += `title: ${tpHd}`1
2
3
4
5
6
7
8let _title = '111';
let result = ''
result += `title: ${title()}\n`
tpHd = setTitle('222');
result += `title: ${title()}`
function title() { return _title }
function setTitle(title) { _title = title }引入参数对象
使用参数对象代替多个参数的情况
1
2
3function readingsOutsideRange(station, min, max){
return ...
}1
2
3
4class Range { ... }
function readingsOutsideRange(station, range){
return ...
}做法
- 如果暂时没有一个合适的数据结构,那就创建一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let station = {
name: 'ZB1',
readings: [
{ temp: 47 },
{ temp: 53 },
{ temp: 28 },
{ temp: 53 },
{ temp: 61 },
],
}
const min = 30
const max = 60
function readingsOutsideRange(station, min, max){
return station.readings.filter((r) => r.temp < min || r.temp > max)
}
let list = readingsOutsideRange(station, min, max)1
2
3
4
5
6
7
8
9
10
11
12
13
14class Range {
constructor(min, max) {
this.min = min
this.max = max
}
contains(arg){
return arg >= this.min && arg <= this.max
}
}
function readingsOutsideRange(station, range){
return station.readings.filter(r => !range.contains(r.temp))
}
let range = new Range(min, max)
let list = readingsOutsideRange(station, range)函数组合成类
一组函数形影不离的操作同一块数据,则这时候就可以组成一个类,这样可以少传递参数,简化调用
1
2
3
4const reading = {...}
function baseRate(month, year) { ... }
const baseCharge = ...
const base = ...1
2
3
4class Reading {
...
}
const aReading = new Reading(reading)做法
- 通过封装记录对多个函数共用的数据进行封装
- 对于使用该记录结构的每个函数,通过搬移函数将其移入新类
- 用于处理该数据记录的逻辑可以用提炼函数提炼出来移动到新类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const reading = {
customer: 'xy',
quantity: 10,
month: 5,
year: 2017
}
function baseRate(month, year) {
return month * 0.1 + year * 0.15
}
function taxThreshold(year) {
return year * 10
}
const baseCharge = baseRate(reading.month, reading.year) * reading.quantity
const base = baseRate(reading.month, reading.year) * reading.quantity
const taxableCharge = Math.max(0, base - taxThreshold(reading.year))
const amount = calculateBaseCharge(reading)
function calculateBaseCharge(aReading) {
return baseRate(aReading.month, aReading.year) * aReading.quantity
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class 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(){
return baseRate(this.month, this.year) * this.quantity
}
get taxableCharge(){
return Math.max(0, this.baseCharge - taxThreshold(this.year))
}
}
const aReading = new Reading(reading)
const baseCharge = aReading.baseCharge
const base = aReading.baseCharge
const taxableCharge = aReading.taxableCharge
const amount = aReading.baseCharge函数组合变换
接受源数据作为输入,并派生出数据,将派生数据以字段形式填入输入输出数据
1
2
3
4const reading = {...}
function baseRate(month, year) { ... }
const baseCharge = ...
const base = ...1
2
3
4
5
6
7function clone(obj){ ... }
function enrichReading(aReading){
let result = clone(aReading)
...
return result
}
const aReading = enrichReading(reading)做法
- 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值(注意这里最好是深复制)
- 提炼函数,将结果作为字段加入到增强对象中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const reading = {
customer: 'xy',
quantity: 10,
month: 5,
year: 2017
}
function baseRate(month, year) {
return month * 0.1 + year * 0.15
}
function taxThreshold(year) {
return year * 10
}
const baseCharge = baseRate(reading.month, reading.year) * reading.quantity
const base = baseRate(reading.month, reading.year) * reading.quantity
const taxableCharge = Math.max(0, base - taxThreshold(reading.year))
const amount = calculateBaseCharge(reading)
function calculateBaseCharge(aReading) {
return baseRate(aReading.month, aReading.year) * aReading.quantity
}1
2
3
4
5
6
7
8
9
10
11
12
13
14function clone(obj){
return JSON.parse(JSON.stringify(obj))
}
function enrichReading(aReading){
let result = clone(aReading)
result.baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity
result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(aReading.year))
return result
}
const aReading = enrichReading(reading)
const baseCharge = aReading.baseCharge
const base = aReading.baseCharge
const taxableCharge = aReading.taxableCharge
const amount = aReading.baseCharge拆分阶段
一段代码在同时处理两件不同的事情,将其拆分成各自独立的模块
1
2
3
4function priceOrder() {
...
return price
}1
2
3
4
5
6
7
8
9
10function priceOrder() {
const priceData = calculatePricingData()
return applyShipping(priceData)
function calculatePricingData(){
return { ... }
}
function applyShipping(priceData){
...
}
}做法
- 将二阶段的代码提炼成独立的函数
- 引入一个中转的数据结构,将其作为参数添加提炼出新函数的参数列表
- 逐一检查提炼出“第二阶段的每个参数”,如果某个参数被第一阶段用到,就将其移入中转数据结构
- 对第一阶段的代码运用提炼函数,让提炼出的函数返回中转数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
*
* @param {{ basePrice: number, discountThreshold: number }} product
* @param {number} quantity
* @param {{discountThreshold: number, discountedFee: number, feePerCase: number}} shippingMethod
*/
function priceOrder(product, quantity, shippingMethod) {
const basePrice = product.basePrice * quantity
const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice
const shippingPerCase = (basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase
const shippingCost = quantity * shippingPerCase
const price = basePrice - discount + shippingCost
return price
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
*
* @param {{ basePrice: number, discountThreshold: number }} product
* @param {number} quantity
* @param {{discountThreshold: number, discountedFee: number, feePerCase: number}} shippingMethod
*/
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
return { basePrice, quantity, 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
}
}