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