重构:改善既有代码的设计-笔记(一)


theme: github

重构:改善既有代码的设计-购书链接

重构的第一步:确保重构代码有一组可靠的测试!
重构的第一步:确保重构代码有一组可靠的测试!
重构的第一步:确保重构代码有一组可靠的测试!

基本概念

重构:对软件内部结构对一种调整,在不改变软件可观察行为对前提下,提高可理解性,降低修改成本

目的

  1. 改进软件的设计
  2. 使软件更易理解
  3. 帮助寻找隐性bug
  4. 提高编程速度

重构时机

  1. 预备性重构:添加新功能更加容易
  2. 帮助理解的重构:使代码更加易懂
  3. 捡垃圾式重构:每次清理都让代码更好一点
  4. 有计划的重构
  5. 替换性的重构:替换一些依赖模块

代码的坏味道

  1. 神秘命名:一个好的名字,清晰的表明自己的功能和用法(改变函数声明、变量改名、字段改名)
  2. 重复代码:在一个以上的地方看到相同的代码(提炼函数、移动语句、函数上移)
  3. 过长函数:函数越长,阅读复杂度越高(提炼函数、以查询取代临时变量、引入参数对象、以命令取代函数)
  4. 过长参数列表:参数过长容易使人迷惑(以查询去取代临时变量、参数对象)
  5. 全局数据:在项目中任何一处都可能改变它,且无法定位哪里发生改变(封装变量)
  6. 可变数据:数据修改容易发生难以预料的bug(封装变量、拆分变量)
  7. 发散式的变化:一个函数只负责一种类型的上下文状态(提炼函数)
  8. 霰弹式修改:发生变化时需要在不同的类中做修改(内联函数、内联类)
  9. 依恋情节:减少模块间的交互(搬移函数)
  10. 数据泥团:数据聚合在一起,拆分成粒度更小的方式(提炼类、参数对象)
  11. 基本类型偏执:使用错误的数据类型处理数据(以对象取代基本类型)
  12. 重复的switch:当你想增加一个选择分支时需要找到所有的分支确认(多态取代条件表达式)
  13. 循环语句:需要通读代码才明白循环内的语义(以管道代替循环)
  14. 冗赘的元素:程序元素增加代码结构,从而支持变化、促进复用,但有时候只是简单的函数(内联函数、内联类)
  15. 夸夸其谈通用性:放弃用不到的情况,比如各种各样的钩子,只做有限开发(移除死代码)
  16. 临时字段:仅仅为了某种特殊情况创建的字段
  17. 过长的消息链:A对象访问B对象,B对象访问C对象…直到E对象(隐藏委托关系)
  18. 中间人:过度委托(移除中间人)
  19. 内幕交易:模块之间交换数据(搬移函数、搬移字段、隐藏委托关系)
  20. 过大的类:单个类做了太多事情(提炼类)
  21. 异曲同工的类:相同类型的类应该保持接口一致(改变函数声明、搬移函数、提炼超类)
  22. 纯数据类:拥有一些字段以及用于访问字段的函数(封装记录)
  23. 被拒绝的馈赠:子类并不需要超类的大部分字段/函数(以委托取代子类)
  24. 注释:当你需要写注释的时候,先尝试重构把所有的注释变得多余

重构.png

重构技术

  1. 提炼函数(内联函数)

    将代码提炼到一个独立的函数中,当你需要花时间浏览一段代码才能弄清楚它在干什么的时候,就应该提炼函数

    1
    2
    3
    4
    function printOwing() {
    ... // printBanner
    ... // printDetails
    }
    1
    2
    3
    4
    5
    6
    7
    function printOwing() {
    printBanner()
    printDetails()
    return
    function printBanner(){ ... }
    function printDetails(){ ... }
    }

    做法:

    1. 创建一个新函数,根据函数意图命名
    2. 将代码从源函数复制到新建函数中
    3. 仔细查看提炼代码、作用域引用的变量,判断是否需要通过参数传入
    4. 所有变量处理完毕后编译
    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
    35
    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}`)
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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)
  2. 内联函数(提炼函数)

    一些本来就很易读代码可以直接内联在源函数内,可以减少间接层

    1
    2
    3
    4
    5
    6
    function reportLines(aCustomer){
    ...
    gatherCustomerData(...)
    return
    function gatherCustomerData(){...}
    }
    1
    2
    3
    4
    5
    function reportLines(aCustomer){
    ...
    ... // gatherCustomerData
    return lines
    }

    做法

    1. 检查函数,确定其不具备多态性
    2. 找出这个函数的所有调用点
    3. 把调用点替换成函数本体
    4. 替换之后执行测试
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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])
    }
    }
    1
    2
    3
    4
    5
    6
    function reportLines(aCustomer){
    const lines = []
    lines.push(['name', aCustomer.name])
    lines.push(['location', aCustomer.location])
    return lines
    }
  3. 提炼变量(内联变量)

    将复杂冗长的表达式拆分成变量更加易读

    1
    2
    3
    function price(order){
    return ...
    }
    1
    2
    3
    4
    function price(order){
    let basePrice
    return ...
    }

    做法

    1. 确认要提炼的表达式没有副作用
    2. 声明一个不可修改的变量,把想要提炼的表达式复制一份,以表达式的结果赋值给变量
    3. 用新变量取代原来的表达式
    1
    2
    3
    4
    5
    6
    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)
    }
    1
    2
    3
    4
    5
    6
    7
    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
    }
  4. 内联变量(内联变量)

    可以通过内联变量的方法消除局部变量

    1
    2
    3
    4
    function price(order){
    let basePrice
    return ...
    }
    1
    2
    3
    function price(order){
    return ...
    }

    做法

    1. 检查确认变量赋值语句的右侧表达式是否存在副作用
    2. 找到第一处使用该变量的地方,将其替换成赋值语句右侧的表达式
    1
    2
    3
    4
    function price(order){
    let basePrice = order.basePrice;
    return (basePrice > 1000)
    }
    1
    2
    3
    function price(order){
    return order.basePrice > 1000
    }
  5. 改变函数声明

    将函数、变量修改成语义更好的名称

    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. 修改函数声明
    3. 找出引用处,替换

    迁移做法

    1. 如果有必要,先对函数体内部进行重构,使得后面提炼步骤易于展开
    2. 使用提炼函数将函数体提炼成一个函数
    3. 如果提炼出的函数需要新增参数,则参考简单做法
    4. 对旧函数使用内联函数
    1
    2
    3
    function 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
    3
    function isNewEndLand(aCustomer){
    return ['MA', 'CT', 'ME', 'VT', 'NH', 'NH', 'RI'].includes(aCustomer.address.state)
    }
    1
    2
    3
    4
    5
    6
    function isNewEndLand(aCustomer){
    return checkState(aCustomer.address.state)
    function checkState(state){
    return ['MA', 'CT', 'ME', 'VT', 'NH', 'NH', 'RI'].includes(state)
    }
    }
  6. 封装变量

    将变量封装成函数调用的方式,方便修改和数据监控

    1
    2
    3
    4
    5
    6
    7
    let defaultOwnerData = {...}
    function defaultOwner(){
    return Object.assign({}, defaultOwnerData)
    }
    function setDefaultOwner(newOwner){
    return defaultOwnerData = newOwner
    }

    做法

    1. 创建封装函数,在其中访问和更新变量
    2. 执行静态检查
    3. 逐一修改使用变量的代码,将其改成调用合适的封装函数
    4. 限制变量的可变性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let defaultOwnerData = {
    firstName: 'Mt',
    lastName: 'Fl'
    }
    function defaultOwner(){
    return Object.assign({}, defaultOwnerData)
    }
    function setDefaultOwner(newOwner){
    return defaultOwnerData = newOwner
    }
  7. 变量改名

    取一个好的名字,是好的开始

    1
    2
    let a = 'xy'
    a = 'dq'
    1
    2
    3
    let _name = 'xy'
    function name(){ return _name }
    function setName(name){ _name = name }

    做法

    1. 如果变量被广泛引用,则运用封装变量将其封装
    2. 找到使用该变量的代码,注意修改
    1
    2
    3
    4
    5
    let tpHd = '111';
    let result = ''
    result += `title: ${tpHd}\n`
    tpHd = '222';
    result += `title: ${tpHd}`
    1
    2
    3
    4
    5
    6
    7
    8
    let _title = '111';
    let result = ''
    result += `title: ${title()}\n`
    tpHd = setTitle('222');
    result += `title: ${title()}`

    function title() { return _title }
    function setTitle(title) { _title = title }
  8. 引入参数对象

    使用参数对象代替多个参数的情况

    1
    2
    3
    function readingsOutsideRange(station, min, max){
    return ...
    }
    1
    2
    3
    4
    class Range { ... }
    function readingsOutsideRange(station, range){
    return ...
    }

    做法

    1. 如果暂时没有一个合适的数据结构,那就创建一个
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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)
  9. 函数组合成类

    一组函数形影不离的操作同一块数据,则这时候就可以组成一个类,这样可以少传递参数,简化调用

    1
    2
    3
    4
    const reading = {...}
    function baseRate(month, year) { ... }
    const baseCharge = ...
    const base = ...
    1
    2
    3
    4
    class Reading {
    ...
    }
    const aReading = new Reading(reading)

    做法

    1. 通过封装记录对多个函数共用的数据进行封装
    2. 对于使用该记录结构的每个函数,通过搬移函数将其移入新类
    3. 用于处理该数据记录的逻辑可以用提炼函数提炼出来移动到新类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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
  10. 函数组合变换

    接受源数据作为输入,并派生出数据,将派生数据以字段形式填入输入输出数据

    1
    2
    3
    4
    const reading = {...}
    function baseRate(month, year) { ... }
    const baseCharge = ...
    const base = ...
    1
    2
    3
    4
    5
    6
    7
    function clone(obj){ ... }
    function enrichReading(aReading){
    let result = clone(aReading)
    ...
    return result
    }
    const aReading = enrichReading(reading)

    做法

    1. 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值(注意这里最好是深复制)
    2. 提炼函数,将结果作为字段加入到增强对象中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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
  11. 拆分阶段

    一段代码在同时处理两件不同的事情,将其拆分成各自独立的模块

    1
    2
    3
    4
    function priceOrder() {
    ...
    return price
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function priceOrder() {
    const priceData = calculatePricingData()
    return applyShipping(priceData)
    function calculatePricingData(){
    return { ... }
    }
    function applyShipping(priceData){
    ...
    }
    }

    做法

    1. 将二阶段的代码提炼成独立的函数
    2. 引入一个中转的数据结构,将其作为参数添加提炼出新函数的参数列表
    3. 逐一检查提炼出“第二阶段的每个参数”,如果某个参数被第一阶段用到,就将其移入中转数据结构
    4. 对第一阶段的代码运用提炼函数,让提炼出的函数返回中转数据结构
    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
    }
    }

重构手法.png