重构是在不改变软件可观察性的前提下,改善软件的内部结构。
第一章 重构,第一个案例
如果对一段代码添加一个新特性很麻烦,那就先重构这段代码,让添加新特性变得很容易,然后再添加新特性。
重构之前必须有一套可靠的测试机制,这些测试要么OK,要么即能列出失败清单,即具有自我检验能力。
重构技术就是以微小的步伐修改程序,并测试通过。
任何一个傻瓜都可以写出计算机理解的代码,但只有写出人类容易理解的代码,才是优秀的程序员。
第二章 重构原则
1、重构作名词,是一种对软件的调整;作动词,是一种改善软件结果的方法。
2、两顶帽子:添加新功能,重构,同一时间只能戴其中一个。添加新功能时,只要保证能通过测试,不重构;重构时不再添加新功能,只改变程序结构,并让其通过测试。
3、重构可以并且应该用于以下目的:
改进软件设计:如果没有重构,随着功能的添加,程序会逐渐腐败,失去自己的结构,也愈来愈理解程序的设计。重构就像整理代码,使设计变得清晰。
使软件更容易理解:重构的过程相当于理解了软件的设计结构,这种理解是针对任何维护这个程序的人。另一方面,可以用重构来帮助理解不熟悉的代码。
帮助找到bug:重构时可以深入理解程序的行为,提高了发现bug的机会。
提高编程速度:良好的设计是快速开发的根本,重构正是一种产生良好设计的代码的手段。
4、何时重构:
三次原则 —— 事不过三,过则重构。
添加新功能时重构,这是最好的时机。
修补错误的时候重构
复审代码时重构,这在开发团队中有利于知识的传播和积累。结对编程、极限编程。
5、怎么对领导说为什么重构:如果领导懂,那说不说无所谓;如果不懂,那就不说。
6、重构的难题:
关于接口的重构:保留原有接口,增加新接口。
难以通过重构的手法完成的设计改动:重构方案进行对比,如果有优势,那就使用它。
何时不该重构:重构的代价大于重写。项目接近于交付日期。
重构与设计:重构不能取代设计,设计也不能取代重构。设计的目的在于找到一个合适的初始解决方案,没有完美的设计,可以通过重构改善设计。
重构可以带来简单的设计,同时又不降低灵活性,这也降低了设计的难度。
重构与性能:重构可能会降低系统性能,但可以改善对软件的理解。如果系统性能的降低可以接收,那么就重构吧。
关于性能,可能大部分的时间消耗在很少的一些代码上,重点关注这些代码。在性能优化阶段,找出瓶颈点,通过不停地重构、测试、性能度量来进行优化。
第三章 代码的坏味道
味道的好坏要靠直觉来判断,没有理论上的定义,说哪个代码是好的,哪个是坏的。
下面是常见的一些坏味道的代码:
1、Duplicated Code 重复代码:提取成公共函数,在需要使用的地方调用。
2、Long Method 过长函数:分解该函数。如果说需要一段注释来解释一些代码的作用,那就将这些代码提炼到一个函数,并以其用途命令。这样比写注释好多了。
还有条件表达式和循环语句。
3、Large Class 过大的类
4、Long Parameter List 过长参数列,包括函数入参和函数内的临时变量
5、Divergent Change 发散式变化:对于模块来说,针对外部不同的变化类型,内部针对每种变化类型的修改应该越少越好。即一个类受多种变化的影响。
6、Shotgun Surgery 散弹式修改:对来模块来说,针对一种外部的变化,内部涉及的修改的地方应该越少越好。即一个变化影响多个类。
7、Featura Envy 依恋情结
8、Data Clumps 数据泥团:比如类中的相同字段,函数的相同参数
9、Primitive Obsession 基本类型偏执
10、Switch Satatments switch惊悚现身:可以用多态
11、Parallel Inheritance Hierarchies 平行继承体系
12、Lazy Class 冗赘类
13、Speculative Generality 夸夸其谈未来性
14、Temporary Field 令人迷惑的暂时字段
15、Message Chains 过多耦合的消息链
16、Middle Man 中间人
17、Inappropriate Intimacy 狎昵关系
18、Alternative Classes with Different Interfaces 异曲同工的类
19、Incomplete Library Class 不完美的库类
20、Data Class 纯稚的数据类
21、Refused Bequest 被拒绝的遗赠
22、Comments 过多的注释:当你需要写注释时,请尝试重构,让注释变得多余。
第四章 构筑测试体系
测试要是自动化的,让测试自己检查测试结果。这里所说的测试都是单元测试。
功能测试每次发现一个新的bug时,写一个单元测试来暴露这个bug。
测试你把握不大代码,和各种边界条件。
测试错误的时候,检查是否是预期的错误。
不要认为测试无法发现所有bug就不写测试,但它能发现大多数bug
第五章 重构列表
重构的基本技巧:小步前进,频繁测试。
重构与设计模式之间的关系与生俱来,模式是目标,重构是手段。
第六章 重新组织函数
1、Extract Method 提炼函数:将一段代码单独提取出来,并以用途命名。重点在于对局部变量的优化,包括传进源函数的参数和源函数内的临时变量。
2、Inline Method 内联函数:如果函数本体和函数名一样清晰,且本体很简短,那么就用本体替代函数。
3、Inline Temp 内联临时变量:如果一个变量只被使用一次,那么尝试内联它。
4、Replace Temp with Query 以查询代替临时变量:如果一个临时变量只保存某一表达式的结果,将表达式提取成函数,将对临时变量的引导转换成对函数的调用。
5、Introduce Explaining Variable 引入解释性变量:对于一个复杂的表达式,将表达式或其中的一部分的结果放进一个临时变量,并以变量名来解释表达式的用途。
6、Split Temporary Variable 分解临时变量:某个临时变量被赋值多次,它既不是循环变量,也不用于收集计算结果,那么针对每次赋值,创建一个新的临时变量。
即让临时变量只承担一次责任。
7、Remove Assignment to Parameters 移除对参数的赋值:代码对一个参数进行赋值,可以用一个临时变量取代参数。
8、Replace Method with Method Object 以函数对象取代函数:有一个大型函数,无法对局部对象使用Extract Method,那么将这个函数放进一个对象内,如此依赖局部变量
就变成对象内的字段。然后在对象内将函数分解成小函数。
9、Substitute Algorithm 替代算法:想要把某个算法替换成更清晰的算法,那么将函数本体替换成算法。
第七章 在对象之间搬移特性
1、Move Method 搬移函数:一个函数与其所驻类之外的另一个类进行更多的交流(调用后者,或者被后者调用),那么在该函数最常引用的类中
建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或者完全移除。
2、Move Field 搬移字段:某个字段被其所驻类之外的另一个更多地用到。在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。
3、Extract Class 提炼类:某个类做了应该由两个类做的事。建立一个新类,将相关的字段和函数从旧类搬移到新类。
4、Inline Class 内联类:某个类没有做太多事情。将这个类的所有特性搬移到另一个类中,然后移除原类。
5、Hide Delegate 隐藏委托关系:客户通过一个委托类来调用另一个对象。在服务类上建立客户所需的所有函数,用以隐藏委托关系。
6、Remove Middle Man 移除中间人:某个类做了过多的简单委托动作。让客户直接调用受托类。
7、Introduce Foreign Method 引入外加函数:需要为提供服务的类增加一个函数,但无法修改这个类。在客户类中建立一个函数,并以第一参数
形式传入一个服务类实例。
8、Introduce Local Extension 引入本地扩展:需要为服务类提供一些额外函数,但无法修改这个类。建立一个新类,使它包含这些额外函数。
让这个扩展品成为源类的子类或包装类。
第八章 重新组织数据
1、Self Encapsulate Field 自封装字段:你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。为这个字段建立取值/设置函数,
并以这些函数来访问这个字段。
2、Replace Data Value with Object 以对象取代数据值:你有一个数据项,需要与其他数据和行为一起使用才有意义。将数据项变成对象。
3、Change Value to Reference 将值对象改为引用对象:你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象。
将这个值对象变成引用对象。
4、Change Reference to Value 将引用对象改成值对象:你有一个引用对象,很小且不可变,而且不易管理。将它变成一个值对象。
值对象有一个非常重要的特性:它们应该是不可变的。
5、Replace Array with Object 以对象取代数组:你有一个数组,其中的元素各自代表不同的东西。以对象替代数据。
对于数组中的每个元素,以一个字段来表示。
6、Duplicate Observed Data 复制被监视数据:你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。
将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
7、Change Unidirectional Association to Bidirectional 将单向关联改成双向关联:两个类都需要使用对方特性,但其间只有一条单向连接。
添加一个反向指针,并使修改函数能够同时更新两条连接。
8、Change Bidirectional Association Unidirectional 将双向关联改成单向关联:两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。
去除不必要的关联。
9、Replace Magic Number with Symbolic Constant 以字面常量取代魔法数:你有一个字面数值,带有特别含义。
创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。
10、Encapsulate Field 封装字段:你的类中存在一个public字段。将它声明为private,并提供相应的访问函数。
11、Encapsulate Collection 封装集合:有一个函数返回一个集合。让这个函数返回该集合的一个副本,并在这个类中提供添加/移除集合元素的函数。
12、Replace Record with Data Class 以数据类取代记录:你需要面对传统编程环境中的记录结构。为该记录创建一个哑数据对象。
13、Replace Type Code with Class 以类取代类型码:类之中有一个数值类型码,但它并不影响类的行为。以一个新类替换该数值类型码。
14、Replace Type Code with Subclasses 以子类取代类型码:你有一个不可变的类型码,它会影响类的行为。以子类取代这个类型码。
15、Replace Type Code with State/Strategy 以State/Strategy取代类型码:你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它。
以状态对象取代类型码。
16、Replace Subclasswith Fields 以字段取代子类:你的各个子类的唯一差别只在”返回常量数据“的函数身上。
修改这些函数,使它们返回超类中的某个(新增)字段,然后销毁子类。
第九章 简化条件表达式
1、Decompose Conditional 分解条件表达式:你有一个复杂的条件语句。从if、then、else三个段落中分别提炼出独立函数。
2、Consolidate Conditional Expression 合并条件表达式:你有一系列条件测试,都得到相同结果。
将这些测试合并为一个条件表达式,并将这个条件表达式提炼成一个独立函数。如果这些条件表达式之间彼此独立,那么慎用本项重构。
3、Consolidate Duplicate Conditional Fragments 合并重复的条件片段:在条件表达式的每个分支上有着相同的一段代码。
将这段代码搬移到条件表达式之外。
4、Remove Control Flag 移除控制标记:在一系列布尔表达式中,某个变量带有控制标记的作用。以break或return取代控制标记。
5、Replace Nested Conditional with Guard Clauses 以卫语句取代嵌套条件表达式:函数中的条件逻辑使人难以看清正常的执行路径。
使用卫语句表现所有特殊情况。精髓就是给某条分支以特别的重视,来表达该条件十分罕见,应该单独检查。
每个函数一个入口一个出口:入口最好是一个,出口则不一定要是一个,保持代码清晰才是关键。
6、Replace Conditional with Polymorphism 以多态取代条件表达式:你手上有个条件表达式,它根据对象类型的不同而选择不同的行为。
将这个条件表达式的每个分支放进一个子类内的复写函数中,然后将原始函数声明为抽象函数。
7、Introduce Null Object 引入Null对象:你需要再三检查某对象是否为Null。将Null值替换为Null对象。
8、Introduce Assertion 引入断言:某一段代码需要对程序状态做出某种假设。以断言明确表现这种假设。
断言的价值在于:帮助程序员理解代码正确运行的必要条件。不能滥用断言,不要使用它来检查“你认为应该为真”的条件,请只使用
它来检查“一定必须为真”的条件。