一提到重构(Refactoring),先不给出重构的定义,我们根据字面意思猜想,重构大概就是要重写代码。
的确,单从字面上理解,重构与重写代码非常相似,但是,细纠起来他们却有着本质的区别。下面我们就以重写代码为切入点,重点探究一下敏捷开发中重构的秘密。
为什么要重写代码呢?我们可能是遇到了类似最极端的“文不对题”的低级错误,也可能就是代码结构繁杂、不完整、代码有错误或者效率不高。
例如,我们以敏捷实践为例,在测试驱动开发中,我们采用尽可能简单的代码让测试用例通过,我们收获了测试框架,也就是说搭建好了建筑楼房的钢筋骨架,但是这些简单代码也可能产生了以下问题,逻辑或者含义表达不甚清楚;代码不完整或者不严谨,不能为迭代新加的测试用例所通过;代码在测试框架中的测试用例执行时根本就有错误而不通过;代码结构繁杂;代码性能不佳需要优化。
重写代码就是重构吗?重写代码与重构有着本质的区别。美国知名作家、软件顾问、演讲大师Martin Flower曾经说过,重构就是“用于重组固有编码,在外部功能不变的状态下改变内部结构”。通俗地说,重构就是在不改变当前外部行为条件下对现有代码结构进行修改的过程。重写代码因为增加新的功能代码,所以不是重构;另外,如果只是为了纯粹的代码优化而不改变结构也不是重构。
对于代码来说,它要做什么是明确的,重构要解决的是如何做的问题。重构的目的就是通过调整程序代码改善软件的质量和性能,使得程序的设计模式和架构更加合理,进一步提高软件的扩展性和维护性。
为什么要进行重构呢?软件产品最初制造出来是经过精心设计的,具有良好的架构。但是,随着时间的增长,用户需求的不断变化,必须不断地修改原有的功能、追加新的功能。而且,还避免不了对一些缺陷进行修改。为了满足用户的新需求,不可避免的要违反最初的设计架构。当用户的新需求越来越难实现时,软件的架构对新需求也渐渐会失去支撑能力,最终变成一种束缚。
重构能帮助我们解决上面所说的重写代码的几个问题。在测试驱动开发(Test-Driven Development,简称TDD)中,除了测试用例编写和实现外,最主要的工作就是进行重构了。同时,也正是通过重构,使得我们在测试驱动开发的每一个步骤中,为测试用例增量而进行开发的代码得以实现。
重构时机的选择
什么时候进行重构呢?我们会说,上面说到为什么要重写代码时遇到的那几个问题都要需要重构的信号。重构可以说在TDD中时时刻刻都要发生,无处不在。“种下一种行为,收获一种习惯”,大量的重构工作,让我们明白其实重构应当被看作是测试驱动开发中的一种开发习惯了。因此,我们要在任何必要的时候进行重构。
一些软件代码经常会发出需要进行重构的信号,这时,要求我们的“嗅觉”一定要灵敏。因为,这些代码已经散发出“异味”,来提醒我们必须要进行重构了。例如:新增的测试用例不能通过;出现了重复代码;两个类耦合太多,太亲密;出现了代码行尺寸极其庞大的类;没有实际作用的懒惰类;体积庞大的方法函数;方法中的长参数列;子类种出现相同的方法定义等。当然除了客观的几个信号外,自己主观上感觉代码逻辑或者意图表达的不清楚明晰,也是要重构的触发器了。
常见的一些需要重构的信号见表1,是重构的触发标记。
表1 重构的信号
ID 需要重构的信号
1 新增的测试用例不能通过
2 出现了重复代码
3 两个类耦合太多,太亲密
4 出现了代码行尺寸极其庞大的类
5 没有实际作用的懒惰类
6 体积庞大的方法函数
7 方法中出现长参数列
8 子类中出现相同的方法定义
9 很难看懂,代码逻辑或者意图表达的不清楚明晰
10 一些不加任何约束的switch语句,或者一大串if/else嵌套
11 太多“非常有必要的”注释
12 代码中硬性嵌入太多数值
13 类中定义了区分不同类型的类别代码
14 太难给类、方法、变量命名时
15 出现了类似xxxUtil/xxxManager/xxxController的类定义时
16 类中出现了某些变量有时又用,有时没用
17 函数参数列中出现太多的string参数
18 ……
新增加了测试用例,运行不通过,我们当然要修改代码,使其通过,所以要重构。
为什么出现重复代码时也要重构呢?TDD的目的就是让代码能正常的、简洁的工作。当出现重复代码时,我们就需要考虑如何让代码简洁的工作了。重复出现这种代码的地方,我们是否可以考虑通过把这些重复代码抽象成为一个单独的方法而多次调用呢?在一些继承的体系中出现重复,我们是否可以将重复代码封装到更高一层级别中去呢?某些代码是重复的结构,我们是否考虑将这些重复结构用模版方法呢......这些都需要通过重构来完成。
当两个类太亲密,耦合太多,即一个类对另一个类的内部细节知道和了解太多时,可能也是要产生不和谐的隐患因素了。“距离产生美”的说法,在软件设计行业也是适用的,若即若离的松耦合模式才是和谐之美。
我们可能需要对部分方法进行迁移归并,让彼此纠缠不清的代码处于同一位置。对于平级的两个个体适用,同样,对于有继承关系的父子类等也是适用的。当子类过多的了解父类的实现细节时,父类可能要隐藏自己的内容了(对待子孙,父辈也要留一手)。这时,我们要么将继承变成委派关系,要么通过将父类的某些细节行为私有而实现松耦合化,降低这种彼此纠缠的耦合。所以,这个时候我们也不得不进行重构。
在TDD中也有追求简洁苗条的习惯,这样看来TDD中也符合当今以瘦为美的时代潮流。现在,我们开分析一下TDD的审美观。暂且设置一个选美大赛的场景,TDD作为我们的裁判。在这次选美大赛中,在这位崇尚以瘦为美的大师眼中,如果出现了一位吨位过重的选手,那他肯定是容忍不下去的。我们的美学大师一定会拷问这位吨位过重的选手:为什么你这么胖?(TDD潜台词,这么胖肯定不单纯,太可疑了)你是不是到处瞎吃(TDD潜台词,肯定胃口大,实现了过多的功能)。
在他咄咄逼人的追问下,这位选手肯定急得汗流夹背了。接着他要担任医学美容大师了,用重构来对该选手痛下杀手:是否可以将功能内聚的代码拆分为独立的方法?是否可以将多个行为的复合体实现功能单一化?是否可以用子类来实现部分具体功能?是否可以用switch多态来实现多个条件判断......
手术完成后,效果显著,这位选手变得更加婀娜多姿了。
什么时候不应该进行重构呢?我们一直在讨论什么时候需要进行重构,但是,不是所有问题都必须通过重构才能更好的解决。
例如,当程序代码错误太多,根本无法正常运行的时候,我们更应该重写代码而不是重构。重构之前,代码必须能够在大多数测试情况下正常运行。另外,当项目已经接近最后期限的时候,我们应该避免重构。因为,即使我们重构成功了,那么重构所赢得的生产力也只能在项目最后期限之后才能体现出来。
重构的方法
通过上面的讲述,我们了解到在什么情况下应该立刻进行重构,以及如何重构这些有“异味”的代码等情况。那么,进行重构的方法有哪些呢?在这一节,我们重点来解决重构的方法问题。重构的方法见表2。
表2重构方法
ID 重构方法
1 提取类
2 提取函数、方法
3 提取接口
4 用子类或者对象来代替类型代码
5 用多态来代替条件判断
6 形成模版方法
7 引入临时变量
8 用工厂方法代替构造方法
9 用符号常量定义来代替硬性编码数字嵌入代码
10用委托代替继承
11将注释转化为代码
12单一方法化的内聚
13使用围绕if体的多条件返回语句代替嵌套的负杂if/else条件判断
14类型定义的优化,例如使用const等改变变量数据类型,效率优化
15……
在重构代码时,需要尽量将自动测试框架准备好,除了是新增的测试用例不能通过时要求的重构之外,上一节重构信号特征识别中所述的重构时机都要求自动测试框架准备停当。这时,测试框架就能为我们的重构保驾护航了。
因为,重构不会改变代码的外部行为,每次重构后,用测试框架对重构后的代码检验,将是对重构正确与否最直接的判断。而且,重构的过程犹如三寸金莲般的小步趋近,每一小步完成后的运行测试,将会在最早的时间让我们知道重构的有效性。
重构的规律之美
在前几节中,我们重点讲了触发重构的时机判断以及重构的方法。而且从以上的例子,我们也有了比较形象的认识,在这里我们再深入挖掘和总结一下重构的信号和重构方法之间的对应关系,梳理出重构的规律之美。
表3中所展示的是我们对于重构的信号和重构方法之间对应关系的认识,这些仅仅起到了抛砖引玉的作用。我们期望随着大家经验的不断积累,能够对于重构的规律之美有更深刻的认识。
重构的经验之谈
重构虽然几乎完全是在源代码中进行的,但是在进行重构时,如果有一些极限编程(Extreme Programming,简称XP)中随手编写的代码逻辑草图或者有比较完整的UML图,将会对我们进行重构提供较有效率的帮助,这些图形化的描述让我们更容易发现和解决问题。
那么,如何做到自由重构呢,这还需要我们在面向对象编程(OOP),面向方面编程(AOP)以及设计模式上多花些时间和精力来解决了。
重构仅仅指工作代码重构吗?
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/14639675/viewspace-567469/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/14639675/viewspace-567469/