# 重构改善既有代码的设计

重构(refactoring):对软件内部结构的一种调整,目的是在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构, 提高其可理解性,降低其修改成本

所以重构是可大可小的,不是只给项目大修大补(更换语言、更换框架等)才叫重构,日常修复bug、缺陷也是对代码进行了重构

# 为何重构&重构好处

程序难维护的几个特征

  • 难以阅读的程序,难以修改

  • 逻辑重复的程序,难以维护

  • 添加新行为时需要修改已有代码的程序,难以修改

  • 带复杂条件逻辑的程序,难以修改

# 改进软件设计

代码结构的流失是累积性的,程序的设计在不断的迭代,更新,当人们只为短期目的,或是在完全理解整体设计之前,就贸然修改代码,程序将逐渐失去自己的结构,程序员 越难通过阅读源码而理解原来的设计。重构很像是整理代码,你所做的就是让所有东西回到应处的位置上。

完成同样一样事情,设计不良的程序往往需要更多代码,这常常是因为代码在不同的地方使用完全相同的语句做同样的事。因此改进设计的一个重要方向就是消除重复代码。 这个动作的重要性在于方便未来的修改。

重构手法:用绝对安全的手法从焦油坑中整理出可测试的接口,给它添加测试,以此作为继续重构的立足点

# 使软件更容易理解

我们在努力完成程序的功能的过程中,往往注意力全在完成功能上,不会为未来的接盘侠做考虑,换句话说就是不考虑代码的易理解性。 为了改善代码,让代码更好地表达自己的用途,那就需要花一点点时间进行重构

# 帮助找到bug

对代码的理解,可以帮助我们更快定位bug

# 提高编程速度

如果有更简单更易理解的代码,开发者就能将更多精力集中在功能上,不需要花额外的时间去理解系统,调试问题,寻找重复代码

总体而言希望重构达到目的:

  • 容易阅读

  • 所有逻辑都只在唯一地点指定

  • 新的改动不会危及现有行为

  • 尽可能简单表达条件逻辑

# 何时需要重构

什么时候觉得代码是需要重构优化的

  • 添加功能时原有的代码设计无法帮助我们轻松添加我们需要的特性时,重构原有设计,吏新特性的添加更快速,更流畅

  • 修补错误时当发生bug时,回想代码没有清晰到让你马上定位bug位置,那么说明代码需要重构

重构的时候还得能够识别哪些代码是有问题

# 识别代码坏味道

  • 遇到有歧义的变量。变量名包含了我们对当前代码的理解,好的变量或函数名应该能清楚表达自己的功能和目的,变量名称是代码清晰的关键。

    不用太过担心修改变量,会引发其它的错误,因为我们现在使用的编辑器都很强大,可以通过查找或者全局搜索等功能变量被使用的地方

  • 重复代码

    一个以上的地点看到相同的程序结构

    解决策略:设法将它们合而为一

  • 过长函数或过大的类

    毕竟代码越长越难以理解,至于如何确定哪段代码该进行分解。书中列了几个技巧:1. 寻找注释。2. 条件表达示和循环

    解决策略:拆解函数

  • 发散式变化

    一个函数经常因为不同的原因在不同的方向上发生变化,这里我的理解是有可能这个函数做的事情太多,职责不明确(墙头草)

    解决策略:拆解函数,提炼内容

  • 霰弹式修改

    每遇到某种变化,你都必须在许多不同的方法/类内做出许多小修改,

    解决策略:应该把这些有相关的代码尽量放在一起

  • switch惊悚现身

    switch的问题在于重复。你常会发现同样的switch语句散布于不同地点。如果要为它添加一个新的case语句,就必须找到所有switch语句并修改它们

  • 夸夸其谈未来性

    企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这么往往造成系统更难理解和维护

    解决策略:该搬就搬吧

  • 令人迷惑的暂时字段

    某个函数内部需要因为一特殊情况而加一些额外处理的代码

    解决策略:将这些孤儿放进单独的函数中

  • 过多的注释

    不是说不该写注释,但当看到某个函数需要很多注释来说明的时候,往往说明这的功能或流程是复杂的,我们关注的是这么复杂的部分而不是注释本身

  • 平行继承体系

    为某个类添加一个子类,必须也为另一个类相应增加一个子类

    解决策略:让一个继承体系的实例引用另一个继承体系的实例

  • 冗赘类

    如果一个类的所得不值其身价,它就应该消失。

  • 长参数列,依恋情结,数据泥团,基本类型偏执,过度耦合的消息链,不完美的库类,纯稚的数据类,被拒绝的遗赠

# 如何重构

重构的是有风险的,在挖掘代码的过程中,会发现一些值得修改的地方,于是你挖得更深。挖得越深,找到的重构机会机会就越多。。。最后就给自己挖了一个大坑

所以对于稍微大点的重构,动手之前得先做一些准备工作

  • 要知道它原来的功能

  • 确定与它有耦合的地方

  • 为即将修改的代码建立一组可靠的测试环境,好的测试是重构的根本,它能告诉我们是否引入bug

接下来了解一下重构手段,首先是针对函数的

# 提炼函数

过长的函数难让人难以理解和维护

对于过长函数可以通过提炼的手段,将部分代码片段放进一个独立的函数中,并让函数名称解释该函数的用途

# 合并内联函数

这个跟提炼函数相反,是把内部调用的函数代码直接写入主函数中

提炼出来的函数可能带来帮助,但非必要的间接性有时会显得多此一举。当内部代码和函数名称同样清晰易懂且内部函数没有被复用时,则可将代码合并至调用函数中

function getRating() {
    return  (moreThanFiveLateDeliveries()) ? 2 : 1
}
function moreThanFiveLateDeliveries() {
    return _numberOfLateDeleveries > 5
}

=>

function getRating() {
    return  (_numberOfLateDeleveries > 5()) ? 2 : 1
}

日常开发中组织函数需要把握得恰到好处,不可过渡提炼函数,也不可将所有代码集中在一个函数中

# 用变量代替复杂表达示

表达式有可能非常复杂而且难以阅读。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式

if((platform.toUpperCase().indexOf('mac')>-1) &&
(browser.toUpperCase().indexOf("IE") > -1) &&
resize >0) {
  // do somethine
}

=>

const isMacOs = platform.toUpperCase().indexOf('mac')>-1
const isIEBrowser = browser.toUpperCase().indexOf("IE") > -1
const wasResized = resize >0

if(isMacOs && isIEBrowser && wasResized) {
  // do something
}

接下看下前端业务代码方面的优化方式

# 渐进式重构

设计不是一蹴而就的,有时候写着写着才发现某些代码可以抽离出来单独使用

渐进式重构是不断地对既有代码进行抽象、分离和组合

首先我们在同一个源文件中新增功能,发现部分代码无副作用且可分离,因此在同一个文件中进行代码分割,形成许多功能单一的模块。如此往复后发现单文件的体积越来越大,此时就可以将功能相关联的模块抽出来放到单独的文件中统一管理,如 helpers、components、constants 等等。

拆分出来的代码需要满足几个条件:

  • 代码后期可复用

  • 代码无副作用

  • 代码逻辑单一

# 高内聚低耦合

高内聚低耦合是软件设计领域里亘古不变的话题,同时也是前端很容易走远的地方。因为前端业务总在不断的添加,代码又总UI交互连系,平时也很习惯性往一个页面不断添加业务代码,不知不觉导致一个页面里各种业务功能耦合在了一起

举个栗子,假如我们一个邀请页面里面放着一个二维码,别人可以扫码跳到注册页面进行注册。此时需要添加一个功能,扫码后先回到这个邀请页面,此时的邀请页面显示的不是一个二维码,而是一个花里胡俏的背景和一个按钮,通过点击按钮再跳到注册页

正常的添加思路:

  • 进入页面需要根据条件判断当前是分享的页面,还是分享后的页面

  • 将分享的背景图和按钮混入到原有的DOM中,通过条件来显示哪个DOM

  • 添加点击按钮的跳转

这种直接混入新功能代码的方式导致新功能与旧的页面严重耦合,如果之后继续往这页面迭代,越往后越搞不清谁是谁了

接下来看下合理的添加方式

  • 分享后页面无论是界面交互还是点击事件都与旧的页面没有任何关联,所以分享后的页面完全可以独立创建成一个组件

  • 然后在旧页面中引入这个组件,并添加显示组件的条件逻辑

  • 如果要进一步做优化的话,还可以的将旧的页面也抽离出来独立成一个组件

  • 将两个组件引入到一个页面中,通过条件判断显示哪个组件即可

这样的变更让这个页面功能更加明确、可控。即使之后继续迭代也不用担心影响另一个组件。换句话说,这个页面功能是高内聚的,与产品代码是低耦合的。而且还得到了两个可以利用的组件

# 合理冗余

过多的依赖组件,频繁的抽象可能导致设计过度。先看一个例子

有这样一个需求,一开始很简单,需要设计两组列表,格式一样:

我们会抽象一个组件来显示它们

const Item = ({ title, content }) => (
  <div>
    <h4>{title}</h4>
    <p>{content}</p>
  </div>
);

现在需求要求在第一个列表的标题上增加热文标记:

我们扩展一下这个组件

const Item = ({ title, content }, index) => (
  <div>
    <h4>{title}{index === 0 && <span>hot</span>}</h4>
    <p>{content}</p>
  </div>
);

需求又变了,要求:在第一个展位去掉内容,并且在下方加个按钮;第二个展位的标题右边增加一个超链接以及增加一个副标题:

继续扩展我们的组件

const Item = ({ title, content }, index) => (
  <div>
    <h4>
      {title}
      {index === 0 && <span>hot</span>}
      {index === 1 && <a href="xxx">去看看</a>}
    </h4>
    {index === 1 && <h5>副标题</h5>}
    <p>
      {index !== 0 && content}
      {index === 0 && <button>领福利<button>}
    </p>
  </div>
);

可以看到,之前抽象的好好的,随着迭代,代码就面目全非了,现在这个组件包含了很多条件判断,逻辑变得复杂,可读性也变差了

接下来用两个 if 重写一下

// 第一个列表
if (index === 0) {
  return (
    <div>
      <h4>标题一<span>hot</span></h4>
      <p><button>领福利<button></p>
    </div>
  );
}
// 第二个列表
if (index === 1) {
  return (
    <div>
      <h4>标题二<a href="xxx">去看看</a></h4>
      <h5>副标题</h5>
      <p>内容</p>
    </div>
  );
}

这里类似改成使用两个组件去分别显示两种列表的内容,代码稍微冗余了,但是业务逻辑却清晰了

# END

无论采用何种方式,最终目的都是要把业务逻辑表达清楚,让代码始终保持良好的可读性和可维护性

任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员

掘金-我是如何将业务代码写优雅的 (opens new window)