代码改变世界

重构,小步进行曲

2010-11-13 16:00  横刀天笑  阅读(2441)  评论(22编辑  收藏  举报

对于重构的重要性相信不需要再强调。在开发的过程中,随着代码的演进,需求的改变我们必须持续不断的对既有代码进行重构:重命名(以更精确地反映元素的职责),提取方法(用更具描述性的名字来归纳一段代码),提取基类(消除重复,提高抽象层次)等。如果我们只是一味的去开发新的代码,而对老代码不闻不问,以为只要它能工作就够了,总有一天我们会在这个上面栽跟头的,设计会慢慢的走向腐化。

好了,本文并不是想强调重构有多重要,因为那不用强调。我想说的是:重构,是小步进行曲。

在阅读Martin Fowler的《重构》时,几次都没有将全书读完,前面几章倒是读的大呼过瘾,不过后面的重构目录让我心生困惑:重构需要这么小的步骤么?连一个重命名都需要这么繁琐的进行。曾几时我还小人的想:老马是不是才思枯竭,没有什么写的了,但又不想出版这么一本薄薄的册子,如是故意弄的这么小步伐,凑篇幅。

不过现在我越来越觉得小步进行的重要性,如果我不能小步的重构一块代码,我甚至都不会去重构它。

在最近的结对编程中,经常碰到这样的场景:

我:这段代码太长了,我们重构一下吧

Partner:好

我:(鼠标键盘一起上了)

Partner:你这样不对(然后抢过键盘)

(实际上我们仅仅是为了想建立一个新类,然后将这个类里的一些方法移动到新的类里,这样原有类的代码就变短了,职责也清晰了,而新类也有很清晰的职责。)

我:(我的做法是,新建一个类,然后将方法copy过去)

Partner:(而搭档的做法是,新建一个类NewClass,然后在原有类里声明一个这个类的字段newClass,然后将要抽离的方法method的方法体再抽取一个方法internalMethod,然后将新方法internalMethod移动到NewClass中,这样method方法就变成newClass.internalMethod,而method的调用者没有任何变化,而在这中间的每一步都运行一下单元测试)

原有类:

   1: public class OldClass{
   2:     //many other code
   3:     public void method(){
   4:         //many code
   5:     }
   6:     //many other code
   7: }

第一步:

   1: public class NewClass{
   2: }
   3:  
   4: public class OldClass{
   5:     private NewClass newClass = new NewClass();
   6:     //many other code
   7:     public void method(){
   8:         //many code
   9:     }
  10:     //many other code
  11: }

第二步:

   1: public class NewClass{
   2: }
   3:  
   4: public class OldClass{
   5:     private NewClass newClass = new NewClass();
   6:     //many other code
   7:     
   8:     //原方法继续在这里,保证测试仍然可以通过
   9:     public void method(){
  10:         internalMethod();
  11:     }
  12:     //many other code
  13:  
  14:     //使用IDE重构工具提取方法
  15:     public void internalMethod(){
  16:         //many code
  17:     }
  18: }

第三步:

   1: public class NewClass{
   2:     //使用IDE 移动方法重构
   3:     public void internalMethod(){
   4:         //many code
   5:     }
   6: }
   7:  
   8: public class OldClass{
   9:     private NewClass newClass = new NewClass();
  10:     //many other code
  11:     
  12:     //原方法继续在这里,保证测试仍然可以通过
  13:     public void method(){
  14:         newClass.internalMethod();
  15:     }
  16:     //many other code
  17: }

在经过上面的三步后,移动方法重构就基本完成了,然后再进行一些重命名等工作,将代码重构的更好。

 

虽然我嘴上没说,但对这种小步进行的方式却很不以为然,我觉得这完全是在浪费时间,我们有能力控制重构过程的小混乱,然后让它恢复秩序。

日子就这么一天天的过去了,终于有一天,因为大部分人去参加了公司的会议,没有新的故事可以做了,如是我决定做一下有个模块的重构工作,为了确保万无一失,我还花了很长时间来绘制重构完成后应该有的类阶层次结构图,然后贴在电脑屏幕旁,然后还做了一个简单的计划,我应该从哪里入手,怎么做怎么做之类的。唯独我没有做到的是小步进行,当时只是为了能尽快的达到结构图的目的,大步进行,我甚至没有借助IDE提供的工具,靠人肉重构(如果你也在使用这种方式,请千万不要继续下去了,这是重构大忌)。我不仅忘记了使用IDE提供的重构工具,甚至还忘记了分步频繁提交的准则,实际上我一开始,这次重构之旅注定要失败。到了下午的时候,我看到测试类里满眼的红色标记(编译不通过),我都焦头烂额了,但是我甚至还一错再错将单元测试暂时屏蔽,以为我可以在重构完成后再回过头来修理测试。最后,我在没有测试的情况下,继续摸黑前行。最后,好不容易将功能代码能编译通过了,我舒了一口气,然后敲了几个命令编译-部署,去倒了一杯咖啡,我回来的时候刷新了一下页面,给了我当头一击,一个刺眼的错误页面展现在眼前。因为我已经走的太远,我都不知道如何去调查错误,这个错误是从几何时引入的。没办法,在下班前的一刻我将整天的工作全部rollback。

在日后的结对编程过程中,我仔细的观察了同事的重构过程,每一步都那么小,每一次都保证测试通过然后再进行下一步重构,每一次有意义的重构都赶紧提交代码,我明白了老马的用心良苦:

1、如果我们每一步都很小,我们就能在下次重构之前记住我们上一次干了啥,重构完成之后,运行测试发现测试未通过,我们就能很容易的定位错误,修正错误。

2、如果每一步都很小,我们在修改功能代码后,我们也能很容易的演进测试,让测试反应当前的变化,这样测试就会与功能代码共同演进。

3、如果每一步都很小,我们在跑完测试后可以马上提交代码,当我们发现重构发生问题时,我们可以回退到上一步,而不至于像我那样将整天的工作全部回滚。

4、如果每一步都很小,我们就可以借助IDE对重构提供的支持来安全的进行,因为IDE对步骤太大的重构支持并不好。

 

如果你也在重构,请谨记小步进行,让你的每一步都是安全的。请不要厌烦《重构》中重构目录的罗嗦,因为那才是重构的精髓所在。