改善既有代码的设计

861 阅读14分钟

重构:改善既有代码的设计(第2版)

何为重构

   重构就是对软件内部结构的一种调整,目的是不改变软件可观察行为的前提下,提高其可理解性,降低其可修改成本
   重构是优化代码结构,使其阅读性更好,扩展性更强的一种高级技术
   程序是首先写给人看的,其次才是写给机器看

第一章 重构的第一个例子

1.重构函数

  • 重复代码:编程中不要有大量的重复代码,解决办法就是去提炼到一个单独的函数中
  • 内联临时变量:如果对一个变量只引用了一次,那就不妨对他进行一次重构
  • 尽量去掉临时变量:临时变量多了会难以维护,所以尽量去掉所使用的临时变量
  • 引入解释性变量:跟上面那个相反,如果使用函数变得很复杂,可以考虑使用解释型变量了
  • 移除对参数的赋值:用临时变量接收复制结果,另外函数中声明的临时变量最好只被赋值一次,如果超过一次就考虑再声明变量对其进行分解了

2.重构类

  • 搬移方法:每一个方法应该放在她最适合的位置,不能随便乱放,所以很多时候你需要考虑,一个方法在这里是不是最适合的。
  • 搬移字段:每一个字段,变量都应该放到其自己属于的类中,不能随便放,不属于这个类中的字段也需要移走。
  • 提炼一个新类:将不属于这个类中的字段和方法提取到一个新的类中
  • 内容移动:有时候每一个子类都有声明一个字段或方法,但是父类里面却没有这个字段或方法,这时候就考虑把这个字段或方法移动到父类里面,去除子类的这个字段和方法
  • 提炼接口:接口也就是协议,现在比较推崇的是面向接口编程

3.重新组织数据

  • 自封装字段:把字段封装起来的好处就是:如果子类复写这个字段的getter函数,那么可以在里面改变这个字段的获取结果,这样子扩展性可能会更好一点
  • 以对象取代数值:随着开发的进行,有时候一个数据项表示不再简单了,比如刚开始只需要知道一个人的名字就行了,可是后来的需求变成了不但要知道这个人的名字还要知道这个人的电话号码,还有住址等。这个时候就需要考虑将数据变成一个对象了
  • 常量取代数字:有时候使用一个固定的数值并不是太好,最好使用建立一个常量,取一个有意思的名字来替换这个常量

4.简化条件表达式

  • 分解条件表达式:有时候看着一个if else语句很复杂,我们就试着把他分解一下
  • 合并条件表达式:有时我们写的多个if语句是可以合并到一起的
  • 合并重复的条件片段:有时候可能会在if else 语句中写重复的语句,这时候需要将重复的语句抽出来
  • 以卫语句取代嵌套表达式
  • 以多态取代switch语句

第二章 重构的原则

  1. 重构的定义

    • (名词形式)对软件内部结构的一种调整,目的是在不改变软件可察行为的前提下,提高可理解性,降低修改成本。
    • (动词形式)使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。
  2. 软件开发的两顶帽子

    • 添加新功能时,不应该修改既有代码,只管添加新功能并通过测试。(做到这个太难了)
    • 重构时不再添加新功能,只管改进程序结构,并通过已有测试。
  3. 为何重构

    • 重构改进软件设计(Design)消除重复代码,我就可以确定所有事物和行为在代码中只表述一次。
    • 重构使软件更容易理解(Maintain)好让以后接手的人看得懂
    • 重构帮助找到BUG(Debug)顺着计算机的稍微走一遍,大部分bug就能解决
    • 重构提高编程速度(Efficiency)添加新的功能时候顾虑少一点,思路清晰
  4. 何时重构

    • 事不过三,三则重构
    • 预备性重构:添加新的功能时更加容易,让修改多处的代码变成修改一处
    • 帮助理解的重构:使代码更容易懂,让代码做到一目了然
    • 捡垃圾式重构:复审代码时感觉不好 如果有时间就改了
    • 有计划的重构:一般都是有了重大问题
    • 长期重构:先把要重构的地方放着,如果有人遇到要重构的地方就改,因为小修改后系统功能不变
  5. 何时不该重构

    • 既有代码太混乱,且不能正常工作,需要重写而不是重构。
    • 如果不需要修改那些代码就不要重构。
    • 项目接近最后期限时,应该避免重构。
  6. 重构的目标

为什么程序如此难以相与?设计与重构的目标
难以阅读的程序,难以修改容易阅读
逻辑重复的程序,难以修改所有逻辑都只在唯一地点指定
添加新行为时需要修改已有代码的程序,难以修改新的改动不会危及现有行为
带复杂条件逻辑的程序,难以修改尽可能简单表达条件逻辑
  1. 代码应该有一套完整的测试套件,并且运行速度要快。
  2. 先写出可调优的软件,然后调优它以求得足够的速度。

第三章 代码的坏味道

我觉得这一章的内容非常重要,识别出代码的坏味道,是开始正确重构的前提。

  1. 神秘命名
  • 命名这东西刚开始学编程的时候就很是个问题,abcd,xxx1234,拼音什么的妖魔鬼怪都有,看别人的代码看到这些东西真的会头大。我自己一开始也用a1、a2什么的,过了几天就不知道我在写什么了。一个好名字能清晰表明自己的功能和用法。
  1. 重复代码
  • 如果要修改重复代码,必须找出所有相关的副本来修改,想想就很累,需设法提炼成函数。
  1. 过长函数
  • 函数越长,越难理解。每当感觉方法的某个地方需要注释来加以说明,可以把这部分代码放入一个独立的方法中,并以用途(而不是实现手法)来命名方法。
  • 条件表达式和循环常常也是提炼的信号。
  1. 过长参数列表
  • 不用参数就只能选择全局数据,这肯定是不可取的。
  • 改善的几点方法:
    • 如果可以向某个参数发起查询获得另一个参数的值,就用以查询取代参数。
    • 如果正在从现有的数据结构中抽取很多数据项,就保持对象完整。
    • 如果几个参数总是同时出现,就用引入参数对象。
    • 如果某个参数被用作区分函数行为的标记,可以使用移除标记参数。
  1. 全局数据
  • 全局数据的问题在于:从代码库任何一个角落都可以修改它。
  • 把全局数量用一个函数包装起来,并控制对其的访问,最好搬移到一个类或者模块中,控制其作用域。
  1. 可变数据
  • 在一处更新数据,却没有意识到软件中另一处期望着完全不同的数据,于是一个功能失效了。
  • 函数式编程–建立在“数据永不改变”的概念基础上:如果要更改一个数据结构,就返回一份新的数据副本,旧的数据仍保持不变。
  1. 发散式变化
  • 因为不同的原因,在不同的方向上,修改同一个模块。
  1. 霰弹式修改
  • 每遇到某种变化,需要在多个类内做出许多小修改,容易遗漏。应该把需要修改的部分放到一处。
  1. 依恋情结
  • 函数和另一个模块中的函数或者数据交流频繁,远多于自己所处模块内部交流。最好将此函数移动到那个模块中。
  1. 数据泥团
  • 在很多地方看到相同的三四项数据,如果删掉其中一项,其他数据也没有意义,那就应该为它们产生一个新的对象。
  1. 基本类型偏执
  • 创建和自己的问题域有用的基本类型,不要简单用字符串等替代。
  1. 重复switch
  • 每当想要增加一个选择分支,必须找到所有的switch,并逐一更新。可以使用多态来解决。
  1. 循环语句
  • 用管道代替循环可以帮助我们更快看清被处理的元素以及处理它们的动作。
  1. 冗赘的元素
  • 如果一个类不值得存在,那么它就应该消失。
  1. 夸夸其谈通用性
  • 如果函数和类的唯一用户是测试案例,那就先删掉测试,然后移除死代码。
  1. 临时字段
  • 类中某个字段只为某些特殊情况而设置。
  1. 过长的消息链
  • 一个对象请求另一个对象,然后再请求另一个对象。。。代码与查找过程中的导航结构紧密耦合,一旦对象之间的关系发生任何变化,代码就不得不发生改变。
  1. 中间人
  • 某个类的接口有一半的函数都委托给其他类,就应该移除这个中间人。
  1. 内幕交易
  • 模块之间的数据交换很难完全避免,应该都放到明面上来。
  1. 过大的类
  • 类的设计应当遵循单一职责原则。
  1. 异曲同工的类
  • 类的替换要保持接口一致。
  1. 纯数据类
  • 把数据处理搬移到纯数据类中,除非被用作const返回值。
  1. 被拒绝的遗赠
  • 子类继承父类的所有函数和数据,子类只挑选几样来使用。为子类新建一个兄弟类,再运用下移方法和下移字段把用不到的函数下推个兄弟类。
  • 子类只复用了父类的行为,却不想支持父类的接口。运用委托替代继承来达到目的。
  1. 注释
  • 注释不是用来补救劣质代码的,事实上如果我们去除了代码中的所有坏味道,当劣质代码都被移除的时候,注释已经变得多余,因为代码已经讲清楚了一切。

第四章 构筑测试体系

  • 要正确地进行重构,前提是有一套稳固的测试集合,以帮助我发现难以避免的疏漏。
  • 编写优良的测试程序,可以极大提高编程速度。
  • 我们一开始写一些代码喜欢把结果输出到屏幕上 然后逐一检测,这些完全可以让计算机来做,我们要做的就是把期望的输出放到测试代码中,然后做一个对比就行了。
  • 编写测试代码其实就是在自己:为了添加功能我需要实现些什么?还能帮我把注意力剧种到接口而非实现。
  • 测试驱动开发–先编写一个失败的测试,编写代码使测试通过,然后进行重构以保证代码整洁

五、重构列表

Java开发,由于IDE(Intellij Idea)能够很好的支持大多数情况下的重构,有各种的自动提示,所以感觉暂时不需要用到重构列表。

六、重新组织函数

1 . Extract Method 提炼函数
将一段代码放进一个独立函数中,并让函数名称解释该函数的用途。
增加可读性,函数粒度小更容易被复用和覆写。

2 . Inline Method(内联函数)
在函数调用点插入函数本体,然后移除该函数。
函数的本体与名称同样清楚易懂,间接层太多反而不易理解。

3 . Inline Temp(内联临时变量)
将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。

4 . Replace Temp with Query(以查询取代临时变量)
将一个表达式提炼到一个独立函数中,并将临时变量的引用点替换为对函数的调用。
临时变量扩展为查询函数,就可以将使用范围扩展到整个类。
减少临时变量,使函数更短更易维护。

5 . Introduce Explaining Variable(引入解释性变量)
将该复杂表达式的结果放进一个临时变量,以变量名来解释其用途。

6 . Split Temporary Variable(分解临时变量)
针对每次赋值,创造一个独立、对应的临时变量。
临时变量会被多次赋值,容易产生理解歧义。
如果变量被多次赋值(除了“循环变量”和“结果收集变量”),说明承担了多个职责,应该分解。

7 . Remove Assignments to Parameters(移除对参数的赋值)
以一个临时变量取代该参数的位置。
对参数赋值容易降低代码的清晰度;
容易混淆按值传递和按引用传递的方式 ;

8 . Replace Method with Method object 函数对象取代函数
一个大型函数如果包含了很多临时变量,用Extract Method很难拆解,
可以把函数放到一个新创建的类中,把临时变量变成类的实体变量,再用Extract Method拆解。

9 . Substitute Algorithm 替换算法
复杂的算法会增加维护的成本,替换成较简单的算法实现,往往能明显提高代码的可读性和可维护性。

七、在对象之间搬移特性

在面向对象的设计过程中,“决定把责任放在哪儿”是最重要的事之一。
最常见的烦恼是:你不可能一开始就保证把事情做对。
在这种情况下,就可以大胆使用重构,改变自己原先的设计。

1 . Move Method 移动函数
类的行为做到单一职责,不要越俎代庖。
如果一个类有太多行为,或一个类与另一个类有太多合作而形成高度耦合,就需要搬移函数。
观察调用它的那一端、它调用的那一端,已经继承体系中它的任何一个重定义函数。
根据“这个函数不哪个对象的交流比较多”,决定其移动路径。

2 . Move Field(搬移字段)
如果一个类的字段在另一个类中使用更频繁,就考虑搬移它。

3 . Extract Class提炼类
一个类应该是一个清楚地抽象,处理一些明确的责仸。

4 . Inline Class 将类内联化
Inline Class (将类内联化)正好于Extract Class (提炼类)相反。如果一个类丌再承担足够责仸、丌再有单独存在的理由。将这个类的所有特性搬移到另一个类中,然后移除原类。

5 . Hide Delegate 隐藏委托关系
在服务类上建立客户所需的所有函数,用以隐藏委托关系 6 . Remove Middle Man(移除中间人)
封装委托对象也是要付出代价的:每当客户要使用受托类的新特性时,就必须在服务端添加一个委托函数。
随着委托类的特性(功能)越来越多,服务类完全变成了“中间人”,此时就应该让客户直接调用受托类。
很难说什么程度的隐藏才是合适的,随着系统不断变化,使用Hide DelegateRemove Middle Man不断调整。

7 . Introduce Foreign Method 引入外加函数
你需要为提供服务的类增加一个函数,但你无法修改这个类。
在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。

8 . Introduce Local Extension 引入本地扩展
你需要为服务类提供一些额外函数,但你无法修改这个类。
建立一个新类,使它包含这些额外函数。让这个扩展品成为源类的子类戒包装类。