重构:改善既有代码的设计
基于 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 方法封装整个数据变量
重命名变量:“计算机科学中只有两个难题:缓存失效和命名”
引入参数对象:多个函数的参数列表相同,且都不止一个参数,可以考虑使用一个类封装
将函数组合到类中:如果多个函数包含了一个相同的参数,则可以考虑成为那个参数所在类的成员函数
将函数组合到转换中:多个函数总是会连在一起使用,可以尝试将其打包,其返回结果也同样打包返回
分裂阶段:将代码划分为几个阶段
封装
封装记录:将记录用数据类替换
封装集合:向表示有复数元素的类添加 add
、remove
单个元素的方法,使其呈现其复数的含义,且尽可能保持内部实现的透明
用对象替换原始:将数据值用对象代替,将类型码用类代替
用询问代替临时:在每次需要时都调用函数,而不是第一次调用函数时用变量保存
提取类:如果类变得越来越大,就需要考虑将其一部分职能抽取出来,形成单独的类
内联类:与前面的相反,当一个类感觉多余了
隐藏指派:编写一个函数,来隐藏指派
移除中间人:和前面的相反,由于每个指派都需要额外编写函数,可能会导致代码臃肿,可读性反而下降
替代算法:使用更整洁的算法替代原有的代码
移动特征
移动函数:本质上是在移动函数的上下文,寻找更合适该函数的位置
移动域:如果经常需要将某个域传递给另一个函数时,就要考虑那个域的位置是否正确了
将语句移动到函数:如果在调用特定函数之间经常会出现同样的语句,就要考虑那条语句是否也应该在该函数中了
将语句移动到调用者:和前面的相反,使函数更具有灵活性
用函数调用替换内联代码:通常使用的是库函数
滑动语句:使得变量的定义和使用紧挨着
分裂循环:使得一个循环中做一件事
用流水线替换循环:map
、reduce
、filter
代替循环可提高可读性
移除已死的代码:由于存在版本控制,即使后面再要用了,也可以找回,而不是直接注释掉
组织数据
分裂变量:即移除对参数的重新赋值,使每个变量都只做一件事
重命名域:命名是一个大问题
用查询替换派生变量:即用不可变的计算来代替可变的变量
将引用改为值:目标同样是提高数据结构的不可变性
将值改为引用:目的是实现共享数据
简化条件逻辑
解耦合条件:一种提取函数的特例,目的是把复杂的条件语句简化为 if (A()) B(); else C();
的形式
合并条件表达式:即将多个 if 语句的条件合并在一起,同样通常会配合着提取函数
用守卫子句替换嵌套条件:即直接 return
,而不是大量无用的 if-else 或层叠的 if
用多态替换条件:即利用面向对象编程来替换面向过程编程
引入特例:通常是引入一个空对象,使得能够对是否是 null 一视同仁
引入断言:断言除了可以用来找出错误外,更重要的是显式表达了程序执行前所需的状态
重构 API
从修改中分离查询:实质上就是将一个函数中有副作用的部分和无副作用的部分分离
参数化函数:向一个函数增加一个参数以实现更多功能,而不是定义多个函数
移除标志参数:特别的,一个用于标志的参数用于选择应该执行哪一部分的函数,因此最好是直接拆分为多个函数,更加灵活
保留整个对象:不应该先将一条记录解构,然后将这些值传递给某个函数。而是应该直接将那条记录整个传递给该函数。
用查询替换参数:使用方法来替代传递的参数,可以减轻调用者的负担
用参数替代查询:和上面相反,主要是为了减少函数对多余上下文的引用
移除设置方法:为了实现某个域的不可变
用工厂方法替换构造函数:构造函数不是一层较高的抽象,受限严重,可扩展性差
用命令替换函数:创建一个对象,然后调用该命令的 execute()
方法,这种做法能够将函数的参数视为域使用,分离出构造和多步执行的过程,但由于绝大部分函数都没有明显的这种性质(事实上优秀的函数也不应该有这种性质),故不太常用
用函数替换命令:和上面相反
处理继承
上拉方法:将多个子类共有的方法提升到父类中
上拉域:同理
上拉构造体:同理
下推方法:相反
下推域:同理
用子类代替类型代码:类似于使用状态/策略
移除子类:与前面的相反
提取父类:提取两个类的共同部分作为一个两个类共同的父类
合并继承:有时不再需要父类和子类两个类了,一个类即可
用指派替换子类:都知道使用集成须谨慎,组合优于继承,这种重构就是为了解决继承带来的问题
用指派替换父类:同理