《代码大全2》阅读笔记11-- Chapter 24 Refactoring

Chapter 24 Refactoring
 重构

24.2 Introduction to Refactoring
 重构简介

“在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改”(Fowler

1999)。
·Reasons to Refactor
1. 代码重复
 重复的代码几乎总是代表着对最初设计里彻底分解方面的一个失误。重复的代码同样违背了“

DRY原则”:不要重复自己“Don't Repeat Yourself.”。 还有一句更精辟的:“复制粘贴即设计之谬”。
2. 冗长的子程序
3. 循环过长或嵌套过深。
4. 内聚性太差的类。
 如果看到有某个类大包大揽了许多彼此无关的任务,那么这个类就该被拆分成多个类,每个

类负责一组具有内在的相互关联的任务。
5. 类的接口未能提供层次一致的抽象。
6. 拥有太多的参数的参数列表。
7. 类的内部修改往往被局限于某个部分。
8. 变化导致对多个类的相同修改
9. 对继承体系的同样修改
10. case语句需要做相同的修改
11. 同时使用的相关数据并未以类的方式进行组织。
 如果看到自己常常对同样一组数据进行操作,你也应当问问自己是否该将这些数据及其操作

组织到一个类里面。
12. 成员函数使用其他类的特征比使用自身类的特征还要多。
 这暗示着这一子程序应该被放到另一个类中,然后在原来的类里调用。
13. 过多使用基本数据类型
 示例:如果程序中使用了整形这种基本数据类型表示某种常见的实体,如货币,请考虑创建

一个简单的Money类,这样编译器就可以对Money变量执行类型检查,你也可以对赋给Money的值添加

安全检查功能。如果Money和Temperature都是整形,那么编译器就不会在你错误地使用

bankBalance = recordLowTemperature这样的赋值语句时提出警告。
14. 某个类无所事事
 有时重构会导致某个类无事可做。如果一个类看起来名不副实,那么问问自己能否将该类的

功能转交给其他的类,然后将这个类彻底去掉。
15. 一系列传递流浪数据的子程序。
 看看自己的代码,把数据传递给某个子程序,是否仅仅就为了让该子程序把数据转交给另一

个子程序。这样传来传去的数据被称为“流浪数据/tramp data”。 这样做也不是不行,如此传递特定数

据,是否与每个子程序接口所表示的抽象概念一致。如果这些子程序接口的抽象概念相同,那么他们之

间传递数据并无不妥。如果不是这样,那么就想些其他的办法让各个子程序的接口更加一致。
16. 中间人对象无事可做
17. 某个类同其他类关系过于亲密
18. 子程序命名不恰当
19. 数据成员被设置为公用。
 把数据成员设置为公用(public)绝对是一个糟糕的主意。这样会模糊接口和实现之间的界

限,其本身违背了封装的原则,限制了类在未来可以发挥的灵活性。
20. 某个派生类仅使用了基类的很少一部分成员函数
 这时,把派生类相对于基类的关系从“is-a”转变为“has-a”。即把基类转换成原来的派生类的

数据成员,然后仅仅为原来的派生类提供所需要的成员函数。
21.注释被用于解释难懂的代码
 注释在程序中扮演了重要的角色,但它不应当被用来为拙劣代码的存在而辩护。有箴言为证

:“不要为拙劣的代码编写文档——应当重写代码”(Kernighan and Plauger1978)
22. 使用了全局变量
23. 在子程序调用前使用了设置代码(setup code),或在调用后使用了收尾代码(takedown code

)。
这样的代码应当看作是一种警告:
C++示例:子程序调用前后的设置代码和收尾代码——糟糕的做法
WithdrawTransaction withdrawal;
withdrawal.SetCustomerId(customerId);
withdrawal.SetBalance(balance);
withdrawal.SetWithdrawalAmount(withdrawalAmount);
withdrawal.SetWithdrawalDate(withdrawalDate);

ProcessWithdrawal(withdrawal);

customerId = withdrawal.GetCustomerId();
balance = withdrawal.GetBalance();
withdrawalAmount = withdrawal.GetWithdrawalAmount();
withdrawalDate = withdrawal.GetWithdrawalDate();

另外一个类似的警告是你发现自己为WithdrawalTransaction创建了一个特殊的构造函数,完成一系列

简单的数据初始化。你的代码可能像下面这样:
C++示例:子程序调用前后的设置代码和收尾代码——糟糕的做法
withdrawal = new WithdrawalTransaction(customerId, balance, withdrawalAmount,

withdrawalDate);
withdrawal.ProcessWithdrawal();
delete withdrawal;

只要你发现在一个子程序调用之前使用了设置代码,或者在调用之后使用了收尾代码,就应该问问自己

,这个子程序的接口是否体现了正确的抽象。在本例中,ProcessWithdrawal的参数列表或许应当作如

下修改:
C++示例:子程序调用前后的设置代码和收尾代码——良好的做法
ProcessWithdrawal(customerId, balance, withdrawalAmount, withdrawalDate);
 
请注意,如果情况与上例相反,类似的问题同样存在。如果你发现自己拥有了一个

WithdrawalTransaction对象之后,还常常需要传递该对象的多个值去调用一个子程序,就像这个例子

一样,你就应该考虑对ProcessWithdrawal接口进行重构,是该接口传递WithdrawalTransaction对

象,而非单独的对象成员。
C++示例:需要若干成员函数调用的子程序
ProcessWithdrawal(withdrawal.GetCustomerId(),
  withdrawal.GetBalance(),
  withdrawal.GetWithdrawalAmount(),
  withdrawal.GetWithdrawalDate());
这些方法有可能是对的,也有可能是错的——这要看ProcessWithdrawal()的接口抽象:子程序调用是

需要四个单独的数据,还是需要一个WithdrawalTransaction对象。

24. 程序中的一些代码似乎是在将来的某个时候才会用到的。
 在猜测程序将来有哪些功能可能被用到这方面,程序员已经声名狼藉了。 “超前设计

(Designingahead)”常常会遭到很多可预见的问题。

24.3 Specific Refactorings
 特定的重构

·Data-Level Refactorings 数据级的重构
1. 用具名常量代替神秘数值
2. 使变量的名字更为清晰且传递更多信息
3. 将表达式内联化
 把一个中间变量换成给他赋值的那个表达式本身。
4. 用函数来代替表达式 (这样一来,表达式就不会在代码中重复出现了。)
5. 引入中间变量
6. 用多个单一用途变量代替某个多用途变量
7. 在局部用途中使用局部变量而不是参数
 如果一个被用来做输入的子程序参数在其内部又被用作局部变量,那么请直接创建一个局部

变量来替代它。
8. 将基础数据类型转化为类。
9. 将一组类型码(type codes)转化为类或枚举类型。
10. 将一组类型码转换为一个基类及其相应的派生类。
 例如对OutputType基类,就可以创建Screen,Printer和File这样的派生类。
11. 将数组转换为对象
 如果你正在使用一个数组,其中不同的元素具有不同的类型,那么就应该用一个对象来替代

它。将数组中的各个元素转化为该类的各个成员。

12. 把群集(collection)封装起来
 如果一个类返回一个群集,到处散布的多个群集实例将会带来同步问题。请让你的类型返回

一个只读群集,并且提供相应的为群集添加和删除元素的子程序。
13. 用数据类来代替传统记录

·Statements-Level Refactorings 语句级的重构
1. 分解布尔表达式
2. 将复杂布尔表达式转换成命名准确的布尔函数
3. 合并条件语句不同部分中的重复代码片段
 如果有完全相同的代码同时出现在一个条件语句if语句块和else语句块中,那么就应该将这段

代码移到整个if-then-else语句块的后面。
4. 使用break或return而不是循环控制变量
 如果在循环中用到了一个类似done这样的控制循环的变量,请用break或return来代替它。
5. 在嵌套的if-then-else语句中一旦知道答案就立即返回,而不是去赋一个返回值。
6. 用多态来代替条件语句(尤其是重复的case语句)
 结构化程序里很多的case语句中的逻辑都可以被放到继承关系中,通过多态函数调用实现。
7. 创建和使用null对象而不是去检测空值。

· Routine-Level Refactorings 子程序级重构
1. 提取子程序或者方法
2. 将子程序的代码内联化
 如果子程序的程序题很简单,而含义不言自明,那么就在使用的时候直接使用这些代码。
3. 将冗长的子程序转换为类
4. 用简单的算法替代复杂的算法
5. 增加参数
 如果子程序需要从调用方获得更多的信息,可以增加它的参数从而为其提供信息。
6. 删除参数
 如果子程序已经不再使用某个参数,就删掉它。
7. 将查询操作从修改操作独立出来。
 一旦有类似GetTotals()的操作改变了对象的状态,那就应该将查询功能从状态改变功能中独

立出来。提供两个独立的子程序。
8. 合并类似的子程序,通过参数区分它们的功能
9. 将行为取决于参数的子程序拆分开来
10. 传递整个对象而非特定成员
 如果发现同一个对象的多个值被传递给了一个子程序,考虑是否可以修改器接口使之接收整

个对象。
11. 传递特定成员而非整个对象。
 如果发现创建对象的唯一理由只是你需要将它传入某个子程序,那么就考虑一下是否可以修

改这个子程序,使之接收特定数据成员而非整个对象。
12. 包装向下转型的操作

·Class Implementation Refactorings 类实现的重构
1. 将值对象转化为引用对象
2. 将引用对象转化为值对象
3. 用数据初始化代替虚函数
4. 改变成员函数或成员数据的位置
 请考虑对类的继承体系做出修改。这些修改可以减少派生类的重复工作:
 a. 将子程序上移到基类中。
 b. 将成员上移到基类中。
 c. 将构造函数中的部分代码上移到基类中。
 下面这些改变则可以用来对派生类进行特殊化:
 a. 将子程序下移到派生类中。
 b. 将成员下移到派生类中。
 c. 将构造函数下移到派生类中。
5. 将特殊代码提取为派生类
6. 将类似的代码结合起来放置到基类中

·Class Interface Refactorings 类接口的重构
1. 将成员函数放到另一个类中
 在目标类中创建一个新的成员函数,然后将原类中将函数体移到目标类中。然后在旧的成员

函数中调用新的成员函数。
2. 将一个类变成两个
3. 删除类
4. 去除委托关系
 有时类A调用了类B和类C,而实际上类A只应该调用类B,而B应该调用C。这种情况下就应该

考虑A对B的接口抽象是否合适。如果应该由B调用C,那么就应该只有B调用C。
5. 去掉中间人
 有时存在类A调用类B,类B调用类C的情况,有时让类A直接调用类C会更好。是否应该去掉

类B,取决于怎么做才能够最好地维护类B接口的完整性。
6. 用委托代替继承
7. 用继承代替委托
8. 引入外部的成员函数
9. 引入扩展类
10. 对暴露在外的成员变量进行封装
11. 对于不能修改的类成员,删除相关的Set()成员函数。
12. 隐藏那些不会在类之外被用到的成员函数。
13. 封装不使用的成员函数
14. 合并那些实现非常相似的基类和派生类。

·System-Level Refactorings 系统级重构
1. 为无法控制的数据创建明确的索引源。
2. 将单向的类联系改为双向的类联系。
3. 将双向的类联系改为单项的类联系。
4. 用Factory Method模式而不是简单地构造函数。
5. 用异常取代错误处理代码。

24.4 Refactoring Safely
 安全的重构

重构是一种改善代码质量的强有力的技术。但正如所有功能强大的工具一样,如果使用不当,重构给你

带来的麻烦会比它所带来的好处还要多。 下面一些简短的建议能够让你避免错误的使用重构。
1. 保存初始代码
2. 重构的步伐请小些
3. 同一时间只做一项重构
4. 把要做的事情一条条列出来
5. 设置一个停车场
6. 多使用检查点
7. 利用编译器警告信息
 要让一些小的错误逃过编译器的目光很容易。你最好把编译器的警告级别设置为尽可能苛刻

。一旦输入中有些小错误,编译器就能立即把他们找出来。
8. 重新测试
9. 增加测试用例。
10. 检查对代码的修改
11. 根据重构风险级别来调整重构方法。

· Bad Times to Refactor 不宜重构的情况
1. 不要把重构当做先写后改的代名词
2. 避免用重构代替重写

24.5 Refactoring Strategies
 重构策略
1. 在增加子程序时进行重构
2. 在添加类的时候进行重构
3. 在修补缺陷的时候进行重构
4. 关注易于出错的模块
5. 关注高度杂度的模块
6. 在维护环境下,改善你手中正在处理的代码
7. 定义清楚干净代码和拙劣代码之间的边界,然后尝试把代码移过这条边界
 真实世界混乱不堪并不等于你的代码也得同样的糟糕。将你的系统看做理想代码,混乱的真

实世界,以及从前者到后者的接口的结合。

Key Points 要点
1. 修改是程序一生都要面对的事情,不仅包括最初的开发阶段,还包括首次发布之后。
2. 在修改中软件的质量要么改进,要么恶化。软件演化的首要法则就是代码演化应当提升程序的内在质

量。
3. 重构成功之关键在于程序员应学会关注那些标志着代码需要重构的众多的警告或“代码臭味”。
4. 重构成功的另一要素是程序员应当掌握大量特定的重构方法。
5. 重构成功的最后要点在于要有安全重构的策略。一些重构方法会比其他重构方法要好。
6. 开发阶段的重构时提升程序质量的最佳时机,因为你可以立刻让刚刚产生的改变梦想变成现实。请珍

惜这些开发阶段的天赐良机。

你可能感兴趣的:(refactor)