让重构提升自己的生活质量

lxf2023-03-18 13:12:01

零、导读

这篇文章是对《重构》这本经典书籍的一个导读。或者说是对于这本书第一章的读书笔记。

有这么一句话,高质量代码是重构出来的。想要理解这句话,我们先来了解一下,开发工作期间,在摸鱼之外的时间分配。《奔跑吧,程序员》这个书正好有这样的饼状图:

pie title 开发人员时间分布
    "编写新代码": 0.9
    "更新现有代码": 3.6
    "阅读代码": 45.5
    "非编码任务": 50.0

从这个表可以清楚的看到,开发直接面对代码的时间为百分之50,而这百分之50中,却有百分之45.5%的时间是在阅读代码,基本可以说,写代码的时候,程序员基本都是在看代码。

如果这时候一直在看的是低质量的代码,不仅仅会拉长阅读代码的时间,还会影响自己的心情,心情不好的好,活也干不好,晚上还有可能睡得晚,睡得晚就会影响自己的健康,这是一种折寿的事情。也就是说:

低质量代码=浪费生命低质量代码 = 浪费生命

所以我们希望看到的是高质量的代码,而高质量的代码是由重构出来的。可想而知重构是多么的重要‼️

每次成功的重构一段代码,看着这出自自己手的优雅代码,成就感油然而生。工作也没有那么糟心了,晚上睡眠质量也高了。重构直接的提高了自己的生活质量。

下面有我总结一下《重构》这本书第一章的内容,希望能够让大家感受到重构的魅力。

一、重构目标

这串代码要实现的业务很简单。

"有一家按摩店,它提供的一些服务,会根据服务的类型和服务的时长来计算客户的消费情况。除此之外,还会根据用户的消费,给予用户的一定的积分。"

积分,一种营销手段,类似于办会员卡,积分达到一定分数可以打折等。

而下面的代码是一个刘姓开发对于这个需求的实现,目标是打印出某个用户的发票:

// 按摩店当前的收费项目
const plays = {
  Thai: { name: 'Thai', type: 'massage back' },
  Chinese: { name: 'Chinese', type: 'pinch the foot' },
  American: { name: 'American', type: 'massage back' },
}


// 某个用户单词消费类目 
const invoices = {
  customer: '刘冠荣',
  performances: [
    {
      playID: 'Thai',
      time: 55,
    },
    {
      playID: 'Chinese',
      time: 35,
    },
    {
      playID: 'American',
      time: 40,
    },
  ],
}


function statement(invoice, plays) {
  let totalAmount = 0
  let volumeCredits = 0
  let result = `${invoice.customer}的账单:\n`
  // 数字转化成RMB
  const format = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'RMB',
    minimumFractionDigits: 0,
  }).format
  
  for (let perf of invoice.performances) {
    const play = plays[perf.playID]
    let thisAmount = 0
    switch (play.type) {
      case 'message back':
        thisAmount = 40000
        if (perf.time > 30) {
          thisAmount += 1000 * (perf.time - 30)
        }
        break
      case 'pinch the foot':
        thisAmount = 30000
        if (perf.time > 20) {
          thisAmount += 10000 + 500 * (perf.time - 20)
        }
        thisAmount += 300 * perf.time
        break
      default:
        throw new Error(`没有这个项目,去别的店吧: ${play.type}`)
    }
    volumeCredits += Math.max(perf.time - 30, 0)
    if ('comedy' === play.type) volumeCredits += Math.floor(perf.time / 5)
    result += ` ${play.name}: ${format(thisAmount / 100)} (${
      perf.time
    } 分钟)\n`
    totalAmount += thisAmount
  }
  
  result += `总共花费 ${format(totalAmount / 100)}\n`
  result += `您获得 ${volumeCredits} 积分\n`
  return result
}


const statementLog = statement(invoices, plays)
console.log(statementLog)

打印的结果如下是:

让重构提升自己的生活质量

咋看一下,这段代码没啥问题,又不长,逻辑也不多。也没有什么嵌套的if else。

但是,此时 产品 她不单单你打印出字符串直接渲染到界面了。她想要你拼接成html。那我们是不是要复制一份下来,然后在每一个result相关逻辑上修改,还需要修改标签拼接的逻辑。

当我们好不容易写完,产品这时候又要求修改积分计算的算法。我们是不是需要在两个地方修改两遍?

而且,我们每添加修改一个功能,都需要从头到尾再看一次这一片代码。自己写的代码尚且过了几个星期都不认识,更不用说在团队的合作当中了。

当然,如果只是一次性开发的东西,以后不用再维护了,怎么样都无所谓。但是如果是长期的项目,这样必然是有隐患的。所以我们就需要重构它!

为了更好的修改代码,我们遵守诸如函数单一原则,封闭原则等,这是解决问题的道,而重构就是术。重构详细的解释了每种情况应该怎么应对。

二、提炼函数 & switch

看到最显眼的就是switch那一串代码。那么我们就把它给先提出来单独构成一个函数。

拆分一个函数最大的问题,不是拆分这些逻辑,而是拆分那些变量。更准确的说,是要改变作用域

它有这些变量 thisAmout、play、perf

thisAmout是一个临时变量,先不用搭理它。

play和perf都是可以外边传进来的,所以有了如下的拆分结果:

function statement (....) {
  
    // 顶层作用域 的code
    
   function amountFor((perf, play) {
    let thisAmount = 0
    switch (play.type) {
      case 'message back':
        thisAmount = 40000
        if (perf.time > 30) {
          thisAmount += 1000 * (perf.time - 30)
        }
        break
      case 'pinch the foot':
        thisAmount = 30000
        if (perf.time > 20) {
          thisAmount += 10000 + 500 * (perf.time - 20)
        }
        thisAmount += 300 * perf.time
        break
      default:
        throw new Error(`没有这个项目,去别的店吧: ${play.type}`)
    }


    return thisAmount 
  } 
}

三、修改变量名 & 内联变量 & 消减变量

  • 好代码应能清楚地表明它在做什么,而变量命名是更是代码清晰的关键 

thisAmout: 是需要return回去,而且函数名也很直白的表述清楚这个函数要输出的东西是什么。所以它可以改为 result

perf: 由于是在循环中返回的,所以为了语义化更加的明确,可以加一个冠词在前面,变成aPerf

play: 我们回到原本的语境当中,可以知道.play 由顶级作用域的变量以及perf构成临时变量. 我们要消灭它。

我们项目中经常出现这样的一个场景:

让重构提升自己的生活质量

图并没有截完。

作为一个经常使用vue2.0的开发者,截图应该是我们经常看到这样的。我不知道你们看到这样的场景是什么感受,反正我是头皮发麻的。更不要说要在这个上面进行修改了。

我们在修改命名之后, 还需要结合内敛变量手法以及消减变量手法,尽可能的减少不必要的变量。

整理如下:

function statement(invoice, plays) {
  // .....
  function amountFor(aPerf) {
    let result = 0
    switch (playFor(aPerf).type) {
      case 'message back':
        result = 40000
        if (aPerf.time > 30) {
          result += 1000 * (aPerf.time - 30)
        }
        break
      case 'pinch the foot':
        result = 30000
        if (aPerf.time > 20) {
          result += 10000 + 500 * (aPerf.time - 20)
        }
        result += 300 * aPerf.time
        break
      default:
        throw new Error(`没有这个项目,去别的店吧: ${playFor(aPerf).type}`)
    }


    return result
  }


  function playFor(perf) {
    return plays[perf.playID]
  }
}

回到循环当中,在顶级作用域中 thisAmout既然也是临时变量,那么也可以使用内联变量手法去消除它

for (let perf of invoice.performances) {
  volumeCredits += Math.max(perf.time - 30, 0)


  if ('comedy' === playFor(perf).type) volumeCredits += Math.floor(perf.time / 5)


  result += ` ${playFor(perf).name}: ${format(thisAmount / 100)} (${
    perf.time
  } 分钟)\n`
  totalAmount += amountFor(perf)
}

此时再回到循环当中。可以明显看到,它既然做了两件事情,一个是计算 刘冠荣 消费的金额,另外一个是计算他所得的积分。所以我们自然要对循环进行进一步的处理。

四、拆分循环 & 提取积分计算

for (let perf of invoice.performances) {
  volumeCredits = volumeCreditsFor(perf)
  
  result += ` ${playFor(perf).name}: ${format(thisAmount / 100)} (${
    perf.time
  } 分钟)\n`


  totalAmount += amountFor(perf)
}
  
function volumeCreditsFor(aPerf) {
  let result = 0
  result += Math.max(aPerf.time - 30, 0)
  if ('comedy' === playFor(aPerf).type) result += Math.floor(aPerf.time / 5)
  
  return result
}

即便如此,这个循环当中,依旧干了两件事情!!!!

for (let perf of invoice.performances) {
   result += ` ${playFor(perf).name}: ${format(thisAmount / 100)} (${
    perf.time
  } 分钟)\n`
  totalAmount += amountFor(perf)
}


for (let perf of invoice.performances) {
  volumeCredits = volumeCreditsFor(perf)
}

循环了两遍。或许会引起对于性能的担忧。但是起码在这样的场景下面,对于性能的影响其实可以忽略不计的。而且很多我们以为的性能问题,其实对于现代的浏览器而言都不是问题,我们小看开发浏览器的那群人了。

不过,情况自然也是有例外。

就算真的有了性能问题,我们也应该在重构之后在考虑。重构之后,我们的代码结构更加的干净,整洁,对于性能的调优自然是更加的直观明确的,也更加的有助于你分析性能的瓶颈出在哪里.

在《重构》这本书就明确的表述了:

大多数情况下可以忽略它。如果重构引起了性能损耗,先完成重构,再做性能优化

很多我们以为的性能问题,其实对于现代的浏览器而言都不是问题

五、唯一性命名 & 提取format

function rmb(aNumber) {
  return new Intl.NumberFormat('en-US', {
    style: 'currensy',
    currency: 'RMB',
    minimumFractionDigits: 0,
  }).format(aNumber/100)
}
  • 进一步消减了临时变量

六、移动语句

  • 把相关联的变量和语句结合在一起
  • react的代码规范
function statement(invoice, plays) {
  let result = `${invoice.customer}的账单:\n`


  let totalAmount = 0
  for (let perf of invoice.performances) {
    result += ` ${playFor(perf).name}: ${rmb(amountFor(perf))} (${
      perf.time
    } 分钟)\n`
    totalAmount += amountFor(perf)
  }




  let volumeCredits = 0
  for (let perf of invoice.performances) {
    volumeCredits = volumeCreditsFor(perf)
  }


  result += `总共花费 ${rmb(totalAmount)}\n`
  result += `您获得 ${volumeCredits} 积分\n`
  return result
  
  // ......
  // ......
}

组合式API

这张图就很好的解释了vue3退出 COmpostionAPI的一个动机,能够让相关代码集中起来,方便之后更好的复用和拆分。

我们重构的目的不是为了让函数可以复用,而是让函数更加的容易读

让重构提升自己的生活质量

  • 为下一步 更进一步 提炼函数 提供直观的帮助

七、提炼函数 & 消除变量

上一步的移动语句,就是为了更好的提炼函数做准备的。我们把积分和费用的循环逻辑进一步封装。

function totalVolumeCredits() {
  let volumeCredits = 0
  for (let perf of invoice.performances) {
    volumeCredits = volumeCreditsFor(perf)
  }
  return volumeCredits 
}


function totalAmount() {
  let result = 0
  for (let perf of invoice.performances) {
    totalAmount += amountFor(perf)
  }
  return result
}

在结合我们的内联变量,消除临时变量。

此时看看现在代码的全貌,将函数都给折叠起来:

让重构提升自己的生活质量

有上图可以明显看出来。核心代码位于第一个红框之中。是不是比一开始的干净得太多?通过合理的命名和简洁的代码,可以更加的看出来这串代码是做什么用的。如果需要知道具体的变量是怎么来,只需要展开对应的函数即可。

当需要进行修改什么逻辑的时候,可以直接定位到对应的函数中。焦点可以聚集在对应函数当中,不会受到其他代码的干扰。

下面是完整的代码:

function statement(invoice, plays, type) {
  let result = `${invoice.customer}的账单:\n`
  for (let perf of invoice.performances) {
  result += ` ${playFor(perf).name}: ${rmb(amountFor(perf))} (${
    	perf.time
    } 分钟)\n`
  }
  
  result += `总共花费 ${rmb(totalAmount())}\n`
  result += `您获得 ${totalVolumeCredits()} 积分\n`
  return result
  
  function amountFor(aPerf) {
    let thisAmount = 0
    switch (playFor(aPerf).type) {
    	case 'message back':
    		thisAmount = 40000
    		if (aPerf.time > 30) {
    			thisAmount += 1000 * (aPerf.time - 30)
    		}
    		break
    	case 'pinch the foot':
    		thisAmount =  30000
    		if (aPerf.time > 20) {
    			thisAmount += 10000 + 500 * (aPerf.time - 20)
    		}
    		thisAmount += 300 * aPerf.time
    		break
    	default:
    		throw new Error(`没有这个项目,去别的店吧: ${playFor(aPerf).type}`)
    }
    return thisAmount 
  }


  function playFor(aPerf) {
    return plays[aPerf.playID]
  }


  function volumeCreditsFor(aPerf) {
    let result = 0
    result += Math.max(aPerf.time - 30, 0)
    if ('comedy' === playFor(aPerf).type) result += Math.floor(aPerf.time / 5)
    
    return result
  }


  function rmb(aNumber) {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'RMB',
      minimumFractionDigits: 0,
    }).format(aNumber / 100)
  }


  function totalVolumeCredits() {
    let result = 0
    for (let perf of invoice.performances) {
      volumeCredits = volumeCreditsFor(perf)
    }
    return result  
  }


  function totalAmount() {
    let result = 0
    for (let perf of invoice.performances) {
      totalAmount += amountFor(perf)
    }
    return result
  }
}

八、测试

整本书都在强调测试的重要性,上面重构的每一步都需要进行测试才进行下一步操作。

书中有这么一句话:

在没有很好的测试用例的前提下,宁愿不要重构在没有很好的测试用例的前提下,宁愿不要重构

只要上线的代码就是有价值的代码,不管你现在的代码多么简洁, 只有经过充分的测试并上线才有价值,不然就是一文不值的。

重构一定是有风险的。重构不是重写,所以我们需要有计划,有计划的进行,这就是《重构》《代码整洁之道》这些书的伟大之处。

重构是需要测试资源的介入的,这就意味着,你的重构需要得到领导的同意。甚至涉及到了迭代开发流程。这就是另外一个话题了。

“开启AdminJS成长之旅!这是我参与「AdminJS日新计划 · 2 月更文挑战」的第 12 天,点击查看活动详情”