《Code Complete》ch.24 重构
WHAT?
重构(refactoring),Martin Fowler将其定义为“在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改”。
WHY?
- 神话:一个管理很完善的软件项目,应该首先以系统化的方法进行需求开发,定义一份严谨的列表来描述程序的功能。设计完全遵循需求,并且完成的相当仔细,这样就让程序员的代码编写工作从头到尾直线型地工作。表明绝大多数代码首次编写后就已完美,测试通过即可被抛诸脑后。代码被修改的惟一时机发生在交付使用后在新版本进行功能的添加
- 现实:在初始开发阶段,代码会有实质性的进化。在初始的代码编写过程中,就会有很多剧烈的改变。即使是管理完善的项目,每个月都有大概1/4的需求发生变化,这不可避免地导致相关代码的改变——有时候是实质性的代码改变
重构的理由
- 代码重复:无论何时你需要对一处进行修改,你都不得不对另一处进行相同的修改,“复制粘贴即为设计之谬”
- 冗长的子程序:见到长度超过一屏的子程序就有种莫名的烦躁感
- 循环过长或嵌套过深:循环内的代码往往有成为子程序的潜质
- 内聚性太差的类:如果某个类包揽了太多太多任务,要考虑将它拆分为多个各司其职的类
- 类的接口未能提供一致的抽象层次:多人维护的结果是造就了一只Frankenstein
- 子程序的参数列表过长:不要超过7个葫芦娃
- 类的内部修改往往被局限于某个部分:如果对于某个类的修改要么在这个部分,要么在那个部分,很少有同时修改两个部分的情况,这表明该类至少应该根据功能被拆分为两个类
- 变化导致多个类的相同修改:这些类中的代码应该重新组织,使改动只影响到其中的一个类
- 对继承体系的同样修改:每次对某个类添加派生类时,发现自己都不得不为另一个类进行相同的操作
- 同时使用的相关数据并没有以类的形式进行组织
- 成员函数使用其它类的特征比使用自身类还多:是时候考虑将函数转移到正确的类当中了
- 某个类无所事事:如果一个类看起来名不副实,就将它的功能转交给其他类,然后干掉它
- 一系列传递流浪数据的子程序:看看你的代码,把数据传给某个子程序,是否就只是为了让该子程序把数据转交给另一个子程序?这样被玩来玩去的数据称为“流浪数据/tramp data”,这时需要看看各个子程序接口的抽象概念是否一致
- 中间人对象无事可做:如果某个类中绝大部分代码都只是去调用其它类中的成员函数,就考虑是否把这个middleman去掉,转而直接调用其它类
- 某个类同其他类关系过于亲密:封装是强有力的工具,如果发现一个类对于她的小伙伴了解超过了应有的程度——包括派生类过度了解了基类中的内容,do it
- 子程序命名不当:长痛不如短痛
- 某个派生类只使用了基类的很少一部分成员函数:这样的情况表明这个派生类被创建出来仅仅是因为基类恰好有她所需要的某个函数,而不是逻辑上强烈的派生关系,可以用“has-a”来代替“is-a”
- 为了解释糟糕的代码而存在的大段注释:不要为拙劣的代码编写文档——应当重写代码
- 使用了全局变量:访问器子程序是个好的替代方案
- 在子程序调用前使用了设置代码(setup code),或者在调用后使用了收尾代码(takedown code):下面的代码是个反面例子——为了调用子程序而特意实例化一个对象是不对的,为什么子程序的参数要使用一个对象呢?
Contract contract;
contract.setName("上海餐饮合同");
contract.setPayRate(5);
contract.setApproved(false);
addContract(contract); - 程序中一些代码是为了未来某个时候才用到的:over-design不可取,这种猜测发生变数太多了;与其去准备那些没有发生的需求,不如尽力把当前代码写得清晰直白,使未来的程序员(包括你自己)在理解时不费力
HOW?
数据级的重构/Data-Level Refactorings
- 用具名常量代替Magic Number:无论是数字还是字符串,都不应该以神秘数字的形式出现在代码中
- 使变量的名字更清晰且传递更多信息
- 将表达式内联化:把一个中间变量换成给他赋值的表达式本身(去除过多的中间变量)
- 用函数来替代表达式
- 引入中间变量:将表达式的值赋给中间变量(干!与上上条需要斟酌着使用)
- 用多个单一用途的变量替换一个多用途的变量:如果你有一个多功能的x先后扮演了多个角色,去招聘更多演员吧
- 在局部用途中使用局部变量而不是参数:尤其是当你需要改动参数值的时候,在java中可以用final关键字限定参数不可修改,但如果传入的是一个对象……
- 将基础数据类型转化为类:若一个基础数据类型需要更多的操作或额外的数据,如Money、Temperature等
- 将一组类型码转换为类:见下面的例子
// before refactoring
public static final int OUTPUT_SCREEN = 10;
public static final int OUTPUT_FILE = 20;
public static final int OUTPUT_PRINTER = 30; // after refactoring
public class OutputType {
public static final int SCREEN = 10;
public static final int FILE = 20;
public static final int PRINTER = 30;
} - 将一组类型码转换为一个基类及其派生类:如以上例子,可有Screen、File、Printer三个派生类
- 将数组转化为对象:若正在使用一个数组,且其中不同的成员有不同的类型,不妨建立一个对象代替此数组
- 把群集(collection)封装起来:到处散布的多个群集实例会带来同步问题,请让你的类返回一个只读群集,并提供访问器子程序用于添加/删除成员
- 用数据类来代替传统记录:建立一个包含记录成员的类
语句级的重构/Statement-Level Refactorings
- 分解布尔表达式:通过引入命名准确的中间变量,帮助理解布尔表达式
- 将复杂的布尔表达式转化为明明准确的布尔函数:提高复杂表达式的可读性,便于重用
- 合并条件语句不同部分中的重复代码片段:若在if/else的block中有着相同的代码,那么把它从block中移到外面
- 使用break或return而不是循环控制变量:不要通过stop等变量来判断循环结束
- 用多态来替换条件语句(尤其是重复的case语句):case语句中的逻辑可以放到继承关系里,通过多态调用函数来实现
- 在嵌套的if-then-else语句中,一旦知道答案就立即返回:而不是再啰哩啰嗦的设置一个返回值,经过一系列判断后再返回
- 创建和使用null对象而不是去检测空值:把处理null值的功能从客户代码中抽离开来,放入相应的类中
子程序级的重构/Routine-Level Refactorings
- 提炼子程序或方法:避免重复
- 将子程序的代码内联化:与上一条恰好相反,若子程序本体非常简单且含义不言自明,不妨直接使用这些代码
- 将冗长的子程序转化为类:从而改善代码的可读性
- 用简单的算法替换复杂的算法:写出只有自己读得懂的代码并不是件光彩的事
- 增加参数/删除参数
- 将查询操作从修改操作中独立出来:一个方法只做一件事情
- 合并相似的子程序,通过参数区分它们的功能:如果两个子程序知识用到的常量不同,不妨把常量作为参数传入
- 将行为取决于参数的子程序拆分出来:别试图用参数中的标识位控制子程序行为
- 传递整个对象而非特定成员:如果发现某个对象的多个特定成员被取出作为某个方法的参数,为什么不直接用这个对象呢
- 传递特定成员而非整个对象:如果发现某个对象被创建出来只是为了传入方法作为参数,为什么不让方法直接获取对象中的特定成员作为参数呢
- 包装向下转型的操作:当子程序返回一个对象时,应该返回已知的最精确的对象,尤其适用于Iterator、Collections
类实现的重构/Class Implementation Refactorings
- 将值对象转化为引用对象:如果发现自己正在维护一个超大的复杂的对象,那么不妨在用到的地方都采用引用的方式
- 将引用对象转化为值对象:如果对某个小型的简单对象进行了多次引用操作,也可以直接用值对象
- 用数据初始化代替虚函数:与其在多个派生类中覆盖成员函数,不如让派生类在初始化时设定适当的常量值,然后使用基类中的通用代码处理这些值
- 改变成员函数或成员变量的位置:将子程序/成员/构造函数 上移到基类/下移到派生类
- 将特殊代码提取为派生类:如果某类中的部分代码仅仅对其一部分实例有用,应该把这部分代码放到派生类中
- 将相似的代码结合起来放入基类:与上一条相反
类接口的重构/Class Interface Refactorings
- 将成员函数移动到另一个类中:在目标类中创建一个新的成员函数,然后在原类中把函数体移动到目标类中,最后在原类中调用目标类的函数
- 将一个类变为两个:如果一个类同时具备了两种截然不同的功能
- 删除类:当他无所事事时
- 去除委托关系:防止越级调用
- 去掉中间人:before A->B->C,after A->C
- 用“has-a”代替“is-a”:并不需要公开基类的全部成员函数
- 用“is-a”代替“has-a”:需要公开基类的全部成员函数
- 对暴露在外的成员变量进行封装:将数据成员改为私用,并提供访问器程序
- 对于不能修改的成员,删除set()函数
- 隐藏那些不会在类外面被用到的成员函数
- 合并那些实现非常类似的基类和派生类
系统级重构/System-Level Refactorings
- 为无法控制的数据创建明确的索引源:将数据组织为一体
- 将单向的类联系改为双向的类联系:两个类需要彼此用到对方的功能
- 将双向的类联系改为单向的类联系:实际上只有一个类需要访问另一个类
- 用Fatory模式而并非简单的构造函数:在需要基于类型码创建对象,或者希望使用引用对象而非值对象的时候,需要使用工厂模式
- 用异常机制代替错误码,或者做相反的替换:从实际需求出发,取决于错误处理策略
安全滴进行重构
刀很锋利。
这样的刀,割谁的头,都不会有一丝滞涩。
无论是别人的头,还是自己的头。
- 保存初始代码:借助于成熟的VCS
- 重构的步伐请小一些:小步快跑
- 同一时间只做一项重构:同上一条
- 把要做的事情一件件列出来:维护一份重构列表会让你清楚有哪些事情已经做了,有哪些事情急需去做
- 多使用检查点:当你不小心把事情搞砸时,可以很快地恢复到上个可以工作的版本
- 重新测试:保持一套优秀的测试用例
- 在重构中实时增加/删除/修改测试用例
- 代码审查
- 根据重构的风险级别调整重构方法:像是“重构那些Magic Number”显然不需要投入过多的测试精力,更不需要进行code review;当涉及到类、接口、数据库构架的改变时,就要慎重了
重构策略
- 在增加子程序时重构
- 在增加类的时候进行重构
- 在修补缺陷时进行重构
- 关注易于出错的模块
- 关注复杂的模块
- 定义清楚干净代码和拙劣代码的边界,然后让代码跨越这个边界
《Code Complete》ch.24 重构的更多相关文章
- code complete part1
最近在看code complete,学习了一些东西,作为点滴,记录下来. 关于类: 类的接口抽象应该一致 类的接口要可编程,不要对类的使用者做过多的假设.不要出现类似于:A的输入量一定要大于多少小于多 ...
- 重读 code complete 说说代码质量
重读code complete 说说代码质量 2014年的第一篇文章本来计划写些过去一年的总结和新年展望,但是因为还有一些事情要过一阵才能完成,所以姑且不谈这个,说说最近重读code complete ...
- Code Complete 读后总结和新的扩展阅读计划
Code Complete 读后总结和新的扩展阅读计划 用了一年时间终于将代码大全读完了,在这里做一个简单的总结,并安排下一阶段的扩展阅读计划. 1.选择代码大全作为我程序员职业入门的第一本书,我认为 ...
- 《Code Complete》ch.23 调试
WHAT? 调试——发现错误的一种手段 WHY? 相对于不善于调试的程序员,善于调试的程序员只需要前者1/20的时间就可以找出问题所在 HOW? 科学的调试方法 把错误的发生稳定下来:假设-证实/证伪 ...
- 《Code Complete》ch.21 协同构建
WHAT? 所有的协同构建技术都试图通过这样那样的途径,将展示工作的过程正式化,以便将错误暴露出来 WHY? 提高缺陷检出率,从而缩短开发周期,降低开发成本 发现不明显的错误信息,如不恰当的注释.硬编 ...
- 《Code Complete》ch.20 软件质量概述
WHAT & WHY ? 软件质量的特性 外在特性 正确性(Correctness) 可用性(Usability) 效率(Efficiency) 可靠性(Reliability) 完整性(In ...
- 《Code Complete》ch.16 控制循环
WHAT? 反复执行的代码片段(你是第一天学编程吗) WHY? 知道如何使用及何时使用每一种循环是创建高质量软件的一个决定性因素 HOW? 检测位于循环开始/循环结尾 带退出的循环 进入循环 只从一个 ...
- 《Code Complete》ch.15 使用条件语句
WHAT? 条件语句指if.else.case.switch,循环语句指for.while WHY? 不用条件语句你写得出代码吗? HOW? if-then 正常情况放在异常情况之前 执行频率高的情况 ...
- 《Code Complete》ch.14 组织直线型的代码
WHAT? 最简单的控制流:即按照先后顺序放置语句与语句块 WHY? 尽管组织直线型的代码是一个简单的任务,但代码结构上的一些微妙之处还是会对代码质量.正确性.可读性和可维护性带来影响 HOW? 必须 ...
随机推荐
- SOCKET:SO_LINGER 选项
好多次接触到SO_LINGER选项,但总是忘了这是干什么用的.现在整理一下,我才明白这个参数是用来设定“SOCKET在CLOSE时候是否等待缓冲区发送完成”这个特性的.下面是一些详细的说明. sets ...
- HTML5外包团队——技术分享:HTML5判断设备在线离线及监听网络状态变化例子
<!doctype html> <html> <head> <meta http-equiv="content-type" content ...
- php定时执行脚本
php定时执行脚本 ignore_user_abort(); // run script. in background set_time_limit(0); // run script. foreve ...
- Linux删除包含特殊符号文件名的文件
今天发现机器上有一文件名为 ~~test 的文件名,欲删除之 ,报错查了下, 发现如下解决方法 假设Linux系统中有一个文件名叫“-test”.如果用户想删除它,按照一般的删除方法在命令行中输入“r ...
- "unresolved external symbol __imp__WSACleanup@0"
编译时出现这种问题怎么解决:"unresolved external symbol __imp__WSACleanup@0"出现此类问题一般是ws2_32.lib这个lib没有li ...
- Linux下nl命令的用法详解
Linux中nl命令和cat命令很像,不过nl命令会打上行号,属于比较不常用的命令,下面随小编一起来了解下这个鲜为人知的nl命令吧. nl命令在linux系统中用来计算文件中行号.nl 可以将输出的文 ...
- Perl 模块 Getopt::Std 和 Getopt::Long
示例程序: getopt.pl; 1 2 3 4 5 6 7 8 #!/usr/bin/perl -w #use strict; use Getopt::Std; use vars qw($opt_a ...
- POJ3318--Matrix Multiplication 随机化算法
Description You are given three n × n matrices A, B and C. Does the equation A × B = C hold true? In ...
- spark transformation与action操作函数
一.Transformation map(func) 返回一个新的分布式数据集,由每个原元素经过函数处理后的新元素组成 filter(func) 返回一个新的数据集,经过fun函数处理后返回值为tru ...
- BestCoder Round #84 Bellovin
Bellovin 题意: 给个中文链接:戳戳戳 题解: 这个题其实就是让你求每一位的最长公共子序列,之后输出就好了,求这个有2个算法,一个是n方,另一个nlogn,所以显然是nlogn的算法,其实这就 ...