以项目重构为契机,我读了《Refactoring Improving the Design of Existing Code》(Martin Fowler 著)的中文译本《重构 改善既有代码的设计》(熊节 译)。先附上中、英文电子版下载地址: link.
如果能力允许,读原著是首选。我主要读译本,看不懂的地方再看原著,帮助理解。
书中重构技术的范例介绍主要依托面向对象编程技术Java。英文版因为创作较早,用Java 1.1、1.2举例。而Java 1.1、1.2在2000年之前出版,如今Java已经升级到1.9了,可见书中有些技术已经过时。另外,如今的IDE具有强大的自动化重构功能,比如eclipse、Intellij Idea,书中对于重构的“危险”不用过于担心。本书更重要的是作者推崇的重构理念。
说明:个人认为书写的太啰嗦,以下引用部分做了提炼,并不是原句。
what
重构(refactoring):在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。
名词定义:对软件内部结构的调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
此书适用范围:单进程程序。
单进程重构与并发和分布式程序设计的重构是完全不同的。
单进程的方法调用成本很低,在分布式软件中,方法的往返必须减至最低限度。
why
短期来看,对错误的修改更容易,不需要维护冗余代码;好的代码结构对追踪问题有帮助。
长期来看,重构可以使你更好理解代码的作用和运作方式,使得添加新功能更容易;有利于复用、扩展。
when
何时重构
不用专门拨出时间重构,不是为了重构而重构。重构必定是为了达到某个目的——因为你想做某件事,重构帮你把这件事做好。
如果你发现要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构,使特性添加比较容易,再添加特性。
事不过三,三则重构。(类似的事不要重复做)
想理解代码所做的事
修补错误
复审代码:某个团队进行设计复审,和单个复审者进行代码复审。
何时不该重构
现有代码不能正常运行。(应该“重写”而不是重构)
项目已近最后期限。
如果最后没有时间重构了,意味着你早该重构的。
how
以下排序尽为本文组织序列,与原著相差很大。
尽量除去临时变量(传来传去,容易跟丢,用方法代替,方便修改)
让每个方法返回一个值,安排多个方法返回多个值。
但是对于计算复杂度较高的方法,还是建议用临时变量吧。
最好不要在另一个对象的属性基础上运用switch语句。
在对象自己的数据上使用switch语句,或使用多态。
两顶帽子:添加新功能、重构
帽子代指行为:添加新功能,不可修改既有代码;重构,不能添加新功能。
在开发时切换两个角色。
改进设计的一个重要方向是消除重复代码
4.1 如果所有catch区段之内,都重复执行了同一段代码,就将重复代码移到finally区段。
注:书中为final,此处原著译文都有问题。
4.2 当子类中有重复的行为,应将该方法移至超类。
4.3 两个类有相同的方法,将方法子集提炼到一个独立的接口。
找出缺乏“间接层利益之处”,在不修改现有行为的前提下,为它加入一个间接层。
找出不值得的间接层,将它拿掉。
中间层可以提高复用率,也要去掉没有复用价值的中间层。
- 技巧
修改某个方法名称时,留下旧方法,让它调用新方法。
不要过早发布接口。修改你的代码所有权政策,使重构更顺畅。
“所有权政策”指的是“我负责的代码不允许其他人修改”。如果没有这么强烈的代码主人意识,合作伙伴重构了他的代码,他就可以修改调用代码。
为整个包定义一个异常基类,再定义异常子类,不影响调用者。
调用者只关心异常基类就好了
创建固定不变的对象。不再每次都创建新对象。
系统把大半时间都耗费再一小半代码身上。
如果创建对象消耗很大的资源,那就创建一个固定不变的对象,可以提升系统速度。
- 提炼代码的信号
寻找注释,条件表达式和循环。
当你感觉需要撰写注释时,先尝试重构,试着让注释变得多余。
Replace Inheritance with Delegation(352) 委托模式:用聚合来替代继承
- 将只赋值一次,不希望改变的变量声明为final
将临时变量声明为final,确保该临时变量只被赋值一次。
在较长的方法中使用final,帮助检查参数是否做了修改。
在对象创建之后不希望改变的字段,不要提供设值方法,声明为final。
间接访问变量的好处是,子类可以通过复写一个方法而改变获取数据的途径,支持更灵活的数据管理方式。
决定在接到请求时创建新的对象,还是预先将它们创建好。
用注册表对象(Dictionary)保存创建好的对象,比如运用Hashtable, HashMap, ConcurentHashMap...
equals()与hashCode()重写必须同时进行。
- 为什么最好不要把数据声明为public?
数据和使用数据的行为集中在一起,修改比较简单。
想想要修改散落在整个程序中的数据...Ouch~
若子类中只有常量方法,就没有存在价值。
在超类中设计与常量方法返回值相应的字段,去除子类,避免因继承而带来的额外复杂性。
保持代码清晰才是最关键的,“单一出口”规则其实不是那么有用。
用卫语句处理罕见情况,及时退出。
if-else结构每个分支是同等重要的。
- 引入Null Object的时机
只有当大多数客户端代码都要求对空对象做出相同响应时,声明Null Object才有意义。
- 令方法携带参数,以提高灵活性
一些做类似工作的方法,因少数几个数值致使行为不同,此时将这些方法统一起来,通过参数处理变化情况,使问题简化。
- 想到可能的情况,避免抛异常。
"异常"只应该被用于异常的、罕见的行为,也就是那些产生意料之外的错误行为,而不应该成为条件检查的替代品。
模板方法:将执行操作的序列移至超类,借助多态保证各操作仍得以保持差异性。
超类中创建这个方法,调用子类重写的方法。
关于测试
- 重构之前,首先要有可靠的测试机制,测试必须有自我检查能力。
- 撰写测试代码的最有用时机是在开始编程之前。
明确功能需要做什么,接口设计,考虑意外情况的处理。
- 测试的目的是希望找出现在或未来可能出现的错误。
- 测试最担心出错的部分。
- 考虑可能出错的边界条件,把测试火力集中在那儿。
- 当事情被认为应该会出错时,别忘了检查是否抛出了预期的异常。
- “花合理时间抓出大多数bug”,好过“穷尽一生抓出所有bug”。
- 每当收到bug报告,先写一个单元测试来暴露bug。
- 随着测试类愈来愈多,可以生成另一类,专门用来包含由其他测试类所组成的测试套件。(这个类即“主控”测试类)
结语
书中的良言
没有任何量度规矩比得上一个见识广博者的直觉。
培养自己的判断力,一个类内有多少实例变量算是太大、一个方法内有多少行代码才算太长。
"得道"的标志是:你可以自信地停止重构。
真正去做才能有更多体会。
重构不是一劳永逸的,此时你需要这样重构,彼时你需要再用相反的方法改回去。就像书中列出了很多相反的重构技术,它们不是矛盾的,而是要看情况具体情况采用合适的方法。
附: 书中列出的相反重构技术
6.1 Extract Method | 6.2 Inline Method |
---|---|
7.3 Extract Class | 7.4 Inline Class |
8.3 Change Value to Reference | 8.4 Change Reference to Value |
8.7 Change Unidirectional Association to Bidirectional | 8.8 Change Bidirectional Association to Unidirectional |
9.1 Decompose Conditional | 9.2 Consolidate Conditional Expression |
10.2 Add Parameter | 10.3 Remove Parameter |
11.1 Pull Up Field | 11.5 Push Down Field |
11.2 Pull Up Method | 11.4 Push Down Method |
11.11 Replace Inheritance with Delegation | 11.12 Replace Delegation with Inheritance |