《重构》读书笔记 —— 如何让你的代码变得更好

好不容易读完了重构——2010年下半年唯一读了的一本书。只能说这半年以来很忙,来了北京之后连个喘息的机会都没有,半年都没有管过博客了,写写读后感,就当作是除除草吧。
以下只是我的一些读书笔记,不过虽然是笔记,但是里面并没有关于重构方法的细节记录,而只是粗略的记下了我理解的重构的大致思路。和《重构》这本书不同,这本书本身说的很细,作者把每一步重构的都拆成了很小的步骤,每一步都可以轻松的回滚,所以有兴趣的可以去看看这本书,肯定都会有一定收获的。
我现在只是一名小菜,过段时间之后,我自己回头看这篇文章,可能都会觉得自己很傻,所以如有不对,还请多多指教,如果实在看不顺眼,就请纯当笑话看吧。
好,下面进入正题。

注:因为是读书笔记,所以可能会有抄袭等等奇奇怪怪的问题,如果发现有版权问题,请联系我,我会尽快删除本文。

===================== 我是欢乐的分隔线 =====================

1. 重构的目的

我们总希望自己的代码能写的更加好看,以至于我MM把我写代码比喻成为打扮自己。没有错,代码就是一个程序员的外表。而重构就是为了让你的代码更加好看。
但是仅仅是好看而已么?
不是,好看意味着简洁,好理解,好维护,好扩展。这就是我们真正要达到的目的。
那这些目的最终又是为了完成什么呢?我们最后再说。

2. 设计模式和重构

设计模式想必大家肯定已经不陌生了,平时在开发的时候,大家也肯定用过各种各样的模式来解决遇到的问题。而当你现在手头的代码使用的模式不能很好的支持你继续开发的时候(可能是不够灵活,也可能是过于灵活),你就需要使用重构来修改它,将你的代码变得更加优雅。

3. 重构与测试

重构与测试的关系为什么要写在前面?因为测试实在是太重要了。
我在最近的项目中备受重构的挫折,为什么?因为没有测试。和很多程序员一样,经常迫于项目的压力,没有时间去实现一些测试用例,或者有些测试用例不方便实现,等到需要重构的时候,问题就出现了。

“我不敢改这段代码,改了要是出错了怎么办?”
“但是这个版本一定要有这个功能啊。”
“那我尝试着改吧,出了错再调。”

如果连这段代码的作者都这么说,谁还敢改这段代码?而偏偏修改中又引入了其他的Bug,结果就是和这个功能相关的所有人都为此买单。大家的时间就这么浪费在了无意义的事情上。如何避免这个问题呢?测试!这是我自己受到的血淋淋的教训。

写测试代码的时候,经常会遇见两个问题:

Q:“刚开发完一个功能,紧接着就要开发另外一个,没有时间来写这些测试代码啊?”
A:在开发这个功能之前,请先些测试用例,这样不仅仅节省了写测试用例的时间,而且能帮助你更快的设计接口,避免走弯路,因为你已经在考虑如何使用这个接口了。

Q:“我还有开发任务,没有办法写全面的测试用例啊!”
A:一个全面的测试程序基本是不可能写出来的,功能一直在加,开发一直在进行,没有那么多时间给我们补全测试用例。但是我们不能因噎废食,测试对我们有好处,我们只要实现可能导致出错,或者实现原来出过错的测试用例即可。能找出大部分问题,总比什么问题都找不出,之后去补救的好。

4. 重构的手段

不要把重构看成一件很大或者需要花费很多时间的工程,也许它只是添加或者删除一个函数的参数,也许是从一种设计模式切换到另外一种,当然也有需要大规模的修改代码的结构的时候,具体看你遇见的问题来进行操作。

但是请记住:不管过程如何,重构的目的和本质总是不变的。不要为了重构而重构。

如何让代码变得更加好看,答案其实很废话:封装。把不变的部分放在一块,把变化的部分提取出来封装在另外一块。
而何时封装和如何封装,正是整本书在讲的内容。我归纳了一下,大概如下:

4.1. 让人迷惑的临时变量

临时变量容易让人产生迷惑,一个变量被来回的赋值,在某一段代码中他表达的是一个意思,在另外一段代码中,表达的又是另外一个意思,到最后就会让人产生疑惑,这个变量到底是什么?所以使用临时变量一定要小心。如果遇见代码中有很多临时变量,我们可以通过以下一些步骤来消除他们:

  1. 如果一个临时变量在代码中表示了多个意思,那么将代码分段,给每段的临时变量取上不同的名字
  2. 将能用函数查询代替的临时变量全变成函数查询,期间请先不要担心效率
  3. 将能提取的代码提取成让人更好理解的函数
  4. 如果还有顽固的让人不爽的临时变量,请修改你的算法

当然并不是所有临时变量都是不好的,我们的目的是让代码更加易懂,所以只要这个临时变量能帮助我们理解这段代码,那这个变量就是有意义的。比如用一个临时变量来代替一个复杂的表达式。

4.2. 简化条件判断

条件判断是程序开发中不可避免的一部分,而当某些语句执行的条件比较复杂的时候,如果处理不好,各种各样的条件判断就会阻碍我们理解这段代码真正的目的。而以下就几个方法,能将复杂的条件判断变得清晰。

  1. 用易懂的临时变量或者查询来代替复杂的条件。比如isValid()函数,就比xxx & FLAG_XXX != 0 && xxxx != “” …,要清晰许多。
  2. 确定这段代码的真实目的,对于处理前的条件判断,尽量使用ASSERT和卫语句,发现错误就尽快退出处理,减少条件判断嵌套的层次。
  3. 如果是类型的判断,请引入子类+多态来解决这个问题。
  4. 对于NULL的处理,可以引入NULL对象。通过多态,定义在对象为NULL时的行为。

在写代码时,请不要在意如“一个函数,一个出口”,“使用break,continue是破坏代码的行为”之类的,一直被人灌输的思想。只要能让代码易懂,请尽情的使用这些关键字。

4.3. 减少重复代码

重复代码是万恶之源(当然还有各种万恶之源,如调试器,我们都忽略吧),这点想必只要做过开发的人都经历过,改了这里忘了改那里。既然是相同的代码,那么就请放在一起来吧,尽量不要让重复的代码在你的代码中出现。避免重复代码的方法,有下面几种:

  1. 如果代码完全一样,可以直接提取公共函数
  2. 如果你使用的是一个库,无法修改他本身的代码,那么我们可以建立代理函数或者代理类来减少重复代码
  3. 对于相似的代码,可以使用模版方法,Trait技术来减少重复的代码

减少重复代码还有一个很重要的方法,那就是避免纯数据类。

纯数据类是一个很神奇的东西,OO开发带来的一个很大的优点就是可以把数据和数据的操作封装在一起,让代码看起来更加自然。所以在代码中应该尽量避免出现纯数据类,如果经常出现对一个纯数据类的操作,请将他直接封装在这个数据类中。
比如:文件路径,本身它是一个字符串,习惯Windows开发的童鞋喜欢直接调用Window Api来对路径进行操作,但是有些操作需要多个Api组合完成。这时候,我们无意中就会写出很多重复的代码,实际上,我们把他封装成一个文件路径的类,就可以很好的避免这些重复代码的产生。

4.4. 封装变化

如何让代码看起来简单?有一个标准,看起来变化越少的代码,越易懂。如果一段只有几行的简单代码,那肯定是很容易懂的。既然这样,那么如果我们把变化的部分都封装起来,那么代码肯定看起来就简单了。
如何封装变化,大家可以猛烈的参考设计模式。如果没有看过,Gang of Four和Head First的都不错。
对于经常用的方法,我这里总结了一下(以下不变化的意思是,不变或者变化较少):

4.4.1. 数据变化不会导致行为发生变化

在这种情况下,你需要做的就仅仅是封装数据了。

  • 尽量避免纯数据类,因为行为是固定的,所以请把行为放入数据类中,让它变得更加有用。
  • 有一些基本类型实际上也是纯数据类,如上文提到的文件路径。
  • 如果这个变化的数据是几个固定的型别码,那么请用类将它封装起来,再提供几个静态变量表示各种类型,避免出现超出范围的型别码。

4.4.2. 数据变化会导致行为发生变化

一般这种情况都是因为数据本身表示类型,这种情况下,请引入子类+多态。解决这种问题设计模式有很多:状态模式,职责链和命令模式等等。

4.4.3. 即使数据不变化,行为本身也可能发生变化

如果行为本身会有变化,此时,我们就需要使用反封装的方法,将数据操作提取出来。此类有一个典型的设计模式:Strategy。

在封装变化的时候,还有一个问题要注意:
如果出现了“一个新的变化,你要修改很多个类的方法”的问题,请将这些方法移入一个类中,或者提取出一个公有的类。避免这种问题。

4.5. 整理你的代码

有一个很有趣的关于bug数量的规律:一个模块的逻辑代码行数在200-400行之间的时候,bug数量是最少的。根据这个规律,如果你某个模块的代码过多,那么你就应该开始拆分他了。
而对于实现来说,除了一些变态的Trick,一般越是短小的代码越容易理解,所以如果你写了一段惊世骇俗的冗长代码,请尽快缩短他,不然后来的人要理解他就比较痛苦了。

4.5.1. 过长的函数

一段非常长的函数,常会让希望理解他的人望而却步。因为他没有办法很快的理解这段函数的目的,深陷在各种逻辑的泥沼中不能自拔。

  • 对于过长的函数,可以先检查里面是否有重复代码,或者可以分段,如果有,请提取成新的函数
  • 检查里面是否有临时变量,看能不能用上述方法简化
  • 如果还是过长,对于缩短代码,这里有一个杀手锏,把这个函数变成一个方法类。
    如:有一个很复杂的函数:ComplexBehavior(xxx),其中的逻辑无法简化。那么可以把它转化成一个叫做ComplexBehavior的类,在其中提取方法,达到缩短过长函数的目的。之后把调用的形式变成ComplexBehavior(xxx).run()即可。

4.5.2. 过长的参数列表

如果一个函数如果参数较多,可能会导致这个函数难以理解,不方便使用,而且这种函数经常还会遇见需要添加新的参数的情况,这种修改有时是很痛苦的。而在OO的程序中,参数列表一般要短很多。为什么呢?答案就是封装。如何封装?

  • 首先,看有没有能省去的参数,是否存在在对象成员变量中的参数,如果有,请删除它
  • 然后,自然而然想到的,就是整合参数,因为很可能这些参数之间是互相关联的,那么把这些互相关联的参数,提取成一个对象吧
  • 不要想着提高效率或者这个函数不需要某个对象的某些参数,而不把这个对象传入(除非是其他部分的接口)。这种效率不是我们应该考虑的事情,这种提高一般都是零头,而对于不需要的情况,你也不知道什么时候他就需要这些东西了,都传进去总是不会错的。
  • 如果还是不行,请继续用那个杀手锏,建立一个方法类,把所有的参数都转化成他的成员变量。

4.5.3. 过长的类

在代码中,大家是不是经常遇见吓人的万能的工具类呢?如果一个类,他本身想做很多事情,那么这个类必然会变得过长。

  • 在设计一个类的时候,请首先确定好这个类的目的是什么
  • 对于已经过长的类,请先检查有没有重复代码,有请先消除
  • 确定好他到底想做那些事情,然后将各个部分分别提取成单独的类
  • 如果功能已经很单一了,只是实现本身复杂,请将他的实现分解成多个类
  • 如果这个类多个方法很类似,只是使用方式不同,请尝试提取出共有的接口,然后找出确定使用方式的型别码,然后利用多态来简化这个类

4.6. 胶合层

在代码中,由于自顶向下设计和自下向上实现产生的冲突导致的,用于连接两部分的代码,就是胶合层。他一般出现在接口和代码真正实现的连接处。胶合层有时候是不可避免的,但是如果过厚,就会导致代码难以理解,找很久也发现不了代码的真实在做的事情,所以一般胶合层是越薄越好。如果发现你的代码中间出现了不必要的胶合层,请删除他们。
什么样是不必要的胶合层呢?如果他的存在并不能使你的代码看起来更加简单,而他又没有什么特殊的用途,那么他就是不必要的胶合层。比如一些可有可无的Adapter和Proxy。

4.7. 过多的注释

写注释是件好事,但是如果发现有一大段代码在解释一些写的非常烂的代码的时候,那么尝试重构这段代码吧。因为不管注释写的再多,我们总归是要去理解这段奇特的代码的,所以让代码本身变得可读性更高才是正确的选择。

需要注意的是,在实施重构的时候,请使用“建立新函数之后替换原函数”的方式来实现重构,而不是直接修改,这样才能保证每一步足够小,而且能回滚,举一个例子,删除函数参数。

5. 重构与性能

程序员写代码的时候,最喜欢做的一件事情,那就是下意识的检查自己写过的代码。下面这些问题,不知道你是否问过自己。

“这里用for循环查找,万一数据量大一点,效率会有问题吧?”
“这里用map是不是比vector更好一些?”
“这里遍历询问好么?要不要让每个模块先注册一下他关注的内容?”
“这种实现方式会不会绕太多弯路了,速度会很慢吧”

除非你对程序这部分会承担的数据量或者他所使用组件的性能有非常清楚的了解,否则,请不要幻想着程序会慢在哪些地方,然后擅自的优化代码。因为事实可能并非你的想象,往往对于vector变map的优化,在时间上表现的都只是一个零头。而如果因为这个修改,使你这部分代码变得晦涩难懂,那就太得不偿失了。在优化性能之前,请先实现好整个原型,有了这个,你才能好好的雕琢它。写代码的时候,请谨记KISS原则,优化是之后的事情。

那到底是哪一部分代码导致我的程序性能不好呢?问性能分析工具吧。

Linux开发的童鞋请移步gprof:http://www.cs.utah.edu/dept/old/texinfo/as/gprof_toc.html
Windows开发的童鞋请自行搜索:WPT, AQTime

6. 重构的本质

开头说了很多重构的目的,提高可理解性,降低其修改成本等等。我们再深究一些,完成这些都是为了什么?所有这些重构的手法,为的是什么?为了让代码简单?如果只是为了让代码简单,书中就不会提及如产生变化会导致多个模块调整等等的问题。那重构到底为了什么?

正交性!

看过Unix编程艺术的人应该都知道,这就是重构的本质。如何让每一个动作,只改变一件事情,而不会影响其他。这就是重构想完成的事情。

7. 结束

好了,乱七八糟的写了这么多,至此,就是我看《重构》这本书的一些心得。

===================== 我是欢乐的分隔线 =====================

如果你看到了这个地方,真的多谢你的耐心,都是海大空的文字,连个像样的图或者代码实例都没有,所以如果你没有开始看一篇博客,就必须看完的强迫症,真的多谢。