基于 Refactoring: Improving the Design of Existing Code, Second Edition 编写

重构的原则

注意,重构应该是一小步一小步进行的,每一步都不应该破坏代码的功能。因此,最为理想的重构流程应当是修改-测试-提交并一步步迭代。

重构的目标是让代码更加模块化,或者探究其本质,是让代码能够被人读懂。正如作者所说:

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

任何傻瓜都会写出能够让机器理解的代码,只有好的程序员才能写出人类可以理解的代码。

添加新功能和重构应该是两个不同的过程,通常是在添加新功能之前,发现现有的的代码结构无法较好地添加新功能,所以先重构。两个过程应当是交替进行的,而不是同时进行

为什么要重构?

  • 提高软件的设计
  • 使软件更容易被理解
  • 帮助寻找 bug
  • 从长期来看,能提高编程的速度

重构类型:

  • 准备重构:使添加新特性更容易
  • 理解重构:使代码更容易理解
  • 清理代码中的“垃圾”

重构的问题:

  • 拖慢新特性
  • 代码所有权
  • 分支
  • 测试
  • 遗留代码
  • 数据库重构

重构的确通常会降低程序运行速度,但通常不会有太多,这是值得的。如果重构导致了程序运行效率明显下降,就需要好好分析设计了。

代码中的坏味道

神秘的名字

重复的代码

较长的函数

较长的参数列表

全局的数据

可变的数据

分散的更改

特征税

数据团

原始着迷

重复的 switch

循环

懒惰的元素

临时域

消息链

中间人

内部交易

大类

用不同的接口替代类

数据类

拒绝遗产

注释:当你觉得必须要写注释,或当看到一段代码,必须要去看注释时,很可能需要重构

构建测试

由于重构过程需要反复测试,所以能够自我测试的代码是很重要的

尽管测试是无法完全详尽所有问题的,但是不完全的测试远远好于没有测试

重构的第一个集合

提取函数:将部分代码提取成一个函数

内联函数:与前者相反,将函数中的内容插入到使用该函数的代码段中

提取变量:对于一个复杂的表达式,将其分解为多段,赋予其含义

内联变量:与前者相反,展开变量,用于解决变量命名污染的问题

改变函数声明:包括修改函数名、参数列表

封装变量:使用 get-set 方法封装整个数据变量

重命名变量:“计算机科学中只有两个难题:缓存失效和命名”

引入参数对象:多个函数的参数列表相同,且都不止一个参数,可以考虑使用一个类封装

将函数组合到类中:如果多个函数包含了一个相同的参数,则可以考虑成为那个参数所在类的成员函数

将函数组合到转换中:多个函数总是会连在一起使用,可以尝试将其打包,其返回结果也同样打包返回

分裂阶段:将代码划分为几个阶段

封装

封装记录:将记录用数据类替换

封装集合:向表示有复数元素的类添加 addremove 单个元素的方法,使其呈现其复数的含义,且尽可能保持内部实现的透明

用对象替换原始:将数据值用对象代替,将类型码用类代替

用询问代替临时:在每次需要时都调用函数,而不是第一次调用函数时用变量保存

提取类:如果类变得越来越大,就需要考虑将其一部分职能抽取出来,形成单独的类

内联类:与前面的相反,当一个类感觉多余了

隐藏指派:编写一个函数,来隐藏指派

移除中间人:和前面的相反,由于每个指派都需要额外编写函数,可能会导致代码臃肿,可读性反而下降

替代算法:使用更整洁的算法替代原有的代码

移动特征

移动函数:本质上是在移动函数的上下文,寻找更合适该函数的位置

移动域:如果经常需要将某个域传递给另一个函数时,就要考虑那个域的位置是否正确了

将语句移动到函数:如果在调用特定函数之间经常会出现同样的语句,就要考虑那条语句是否也应该在该函数中了

将语句移动到调用者:和前面的相反,使函数更具有灵活性

用函数调用替换内联代码:通常使用的是库函数

滑动语句:使得变量的定义和使用紧挨着

分裂循环:使得一个循环中做一件事

用流水线替换循环mapreducefilter 代替循环可提高可读性

移除已死的代码:由于存在版本控制,即使后面再要用了,也可以找回,而不是直接注释掉

组织数据

分裂变量:即移除对参数的重新赋值,使每个变量都只做一件事

重命名域:命名是一个大问题

用查询替换派生变量:即用不可变的计算来代替可变的变量

将引用改为值:目标同样是提高数据结构的不可变性

将值改为引用:目的是实现共享数据

简化条件逻辑

解耦合条件:一种提取函数的特例,目的是把复杂的条件语句简化为 if (A()) B(); else C(); 的形式

合并条件表达式:即将多个 if 语句的条件合并在一起,同样通常会配合着提取函数

用守卫子句替换嵌套条件:即直接 return,而不是大量无用的 if-else 或层叠的 if

用多态替换条件:即利用面向对象编程来替换面向过程编程

引入特例:通常是引入一个空对象,使得能够对是否是 null 一视同仁

引入断言:断言除了可以用来找出错误外,更重要的是显式表达了程序执行前所需的状态

重构 API

从修改中分离查询:实质上就是将一个函数中有副作用的部分和无副作用的部分分离

参数化函数:向一个函数增加一个参数以实现更多功能,而不是定义多个函数

移除标志参数:特别的,一个用于标志的参数用于选择应该执行哪一部分的函数,因此最好是直接拆分为多个函数,更加灵活

保留整个对象:不应该先将一条记录解构,然后将这些值传递给某个函数。而是应该直接将那条记录整个传递给该函数。

用查询替换参数:使用方法来替代传递的参数,可以减轻调用者的负担

用参数替代查询:和上面相反,主要是为了减少函数对多余上下文的引用

移除设置方法:为了实现某个域的不可变

用工厂方法替换构造函数:构造函数不是一层较高的抽象,受限严重,可扩展性差

用命令替换函数:创建一个对象,然后调用该命令的 execute() 方法,这种做法能够将函数的参数视为域使用,分离出构造和多步执行的过程,但由于绝大部分函数都没有明显的这种性质(事实上优秀的函数也不应该有这种性质),故不太常用

用函数替换命令:和上面相反

处理继承

上拉方法:将多个子类共有的方法提升到父类中

上拉域:同理

上拉构造体:同理

下推方法:相反

下推域:同理

用子类代替类型代码:类似于使用状态/策略

移除子类:与前面的相反

提取父类:提取两个类的共同部分作为一个两个类共同的父类

合并继承:有时不再需要父类和子类两个类了,一个类即可

用指派替换子类:都知道使用集成须谨慎,组合优于继承,这种重构就是为了解决继承带来的问题

用指派替换父类:同理