2
、重构
重构的概念不是源自
Martin Fowler
这部名著《重构——改善既有代码的设计》(《
Refactoring
:
Improving the Design of Existing Code
》),而是来自
Smalltalk
社区,感谢
Fowler
此书是用我最熟悉的
Java
来作为所举实例的语言的,这使我阅读此书比较省劲,而不像阅读《设计模式》,毕竟对
C++
没那么多经验。这本书一共
15
章,我看到第
6
章。第
1
章重构,第一个案例,用
Step by Step
的方式完成了一系列重构;第
2
章:重构原则,介绍了什么是重构和一些与之相关的话题,大都是经验之谈,没什么代码;第
3
章让我思考了很多,它主要介绍了代码的坏味道,一共
22
种
Bad Smell
。其中大部分都是我在实际编程过程中遇到过或认真思考过的,因此就有一种遇到老朋友的欣喜,看得格外起劲。而后第
4
章主要讨论了构筑测试体系,这部分内容也是让我深有感触的。第
5
章是本书所讲的主体真正开始的序幕,介绍了一份统领全书的重构名录,起到了提纲挈领的作用;然后第
6
章介绍了第一类重构方法:重新组织你的函数(
Compsosing Methods
),很多方法正是我平常使用的——使用时并不知道这样做就是重构,也不知道有这些名称——不过,这些方法都来自于实践,因此会产生这些共鸣也是理所当然的。但是一些坏味道我也不完全理解,在编程时没有遇到过,有些重构手法在我看来也并没有太大必要,这些可能有待日后编程经验的进一步增加才能更好的理解吧。
2.1
代码的坏味道
用味道来形容代码质量,是一种很屌也很流行的说法,原来的一些叫法都比较土,总之先前也看过很多被称为好的编程风格的文章,比如讨论一个函数到底最长有多长才算合理之类的。这本书里也包含这些内容,不过比我以前看过的要多。下面只挑一些我遇到过的,并且有些话要说的
Bad Smell
来讨论之。
2.1.1
Duplicated Code
(重复代码)
重复代码被
Fowler
作为第一种坏味道提出来,或许因为它是目前最常见的问题,同时也是这个人人宣传要“代码复用”“最大程度的重用”的时代的主旋律所不许可的。重复代码在实际的项目中非常害人,主要有以下几个缺点:
u
同样的代码,一旦修改,需要多处维护,非常麻烦,还容易出错。在做那个人力资源系统时我就饱受此苦,修改别人的代码,不知道那是哪位仁兄编的,同样的代码出现了好几次,搞得我改了这段改那段,相当麻烦。还有前一阵在地磅系统中加入视频监控功能,在那个拥有
6
个
if
语句的函数里,每个分支语句下都在干一件事,向数据库的
4
个表中插入数据,
不同的是插入的数据略有区别,但表是一样的。我需要把我写的函数加入到这六个分支语句里,而且必须要找准插入点,每个分支语句下的代码都有五、六十行,当时就在想,这家伙当初编程时怎么不把插入数据库的代码写成一个函数呢?(当然用
DAO
更好了)这些项目都是用
Delphi
完成的,而且整个代码量奇大无比,领域知识及其复杂,先前的程序员还不按套路出牌,想把重复的代码重构成一个函数(
Extract Method
,提炼函数)都不敢,因为不知道会产生什么可怕的后果。美国有句土话:如果没坏就接着用。不过,这样的隐患随着代码量的持续增加,会越来越难缠——中国的很多所谓架构师都不重视代码质量,殊不知最后我们交付的最有价值的是可以工作的软件,如果陷入坏味道代码的泥潭,最后想脱身都难,很容易导致项目失败(关于这个话题,在《代码大全》里有很精辟的阐述)。
u
编写重复的代码会浪费时间,降低生产力。有人可能觉得没什么,因为重复的代码都是复制粘贴产生的,但是在实际的开发中,我们大都有这样的经历(特别是做没什么太高难的技术含量的企业信息系统的开发),往往编写新的功能只需要几分钟(但是功能不能太大,反正我是这样,当设计做好了之后,基本上都是在生产代码了。特别是熟练之后,不需要查手册帮助之类的,真的很快),而大量的时间都是在调试了,编写重复的代码,意味着每一段重复的代码在添加同样的新代码后都需要调试——很奇怪,这常常会出错,特别是工作环境很恶劣的时候。
2.1.2
Long Method
(过长函数)
刚才我所说的那个包含
6
个
if
语句的函数有
10
来屏(使用
Delphi 7
,
17
寸纯平显示器),真是痛苦的不行,往往看了后面忘了前面,估计当时写这个函数的人可能很爽,想到什么写什么,这可苦了我们后面这些人,理解这个函数花了我
6
个小时——这岂不是在大大降低生产率吗?以前看李开复写过的一篇文章,说微软以前有个大牛写了个
1000
多行的函数,而且这个函数还难度极高,没有一句重复一句废话,并且改动一句整个系统就不能用了,搞得后来没人敢改这个函数。好像后来有人把这个函数重写了,分成了很多小函数。
一个函数多长算长?很多人说最长不要超过一屏,我觉得这个标准是个可视化的标准,可以量化,不过有些死板。我觉得这要视实际情况而定,函数不能太长,同时也不能过于琐碎,分得块太小也不太便于理解,我想还是根据领域模型来考虑函数长度吧,每个函数只干自己的事,不要越俎代庖,反正从我的经验来看,按照这个原则就不会编写出特别长的函数,特别是使用
Java
这种优雅的面向对象语言。一件事情有
30
个步骤怎么也做完了吧,加上那些声明、创建的代码,五六十行应该都能搞定了。五六十行超过一屏了,不过也不会太糟糕,因为核心算法的代码都在一屏里,不会有什么太大的影响。
2.1.3
Large Class
(过大类)
其实与
Long Method
一样,一个类封装了太多的方法,但是我遇到这种情况都是发生在没有正确运用
OO
,甚至根本不知道
OO
的时候,这很有问题,虽然很多人使用
Java
编程,但是并不意味他们是运用
OO
的思想在进行程序设计,我经常看到这样的
Javabean
,里面包含了一个模块所有方法——我通常把它叫做
Method Container
,认真学习
OO
设计,应该不会出现如此庞大的类(包含在
class
中的未必是类,类要有意义,否则就是函数集,不能称为类)。
2.1.4
Long Parameter List
(过长参数列)
让我印象最深刻的一次遇到这个问题是做视频监控系统时,起初只是为构造函数添加参数,结果参数越来越多,竟然有
7
个,后来发现很多参数其实是一个类的属性,当时很后悔起初要是按照
OO
设计就不会出现这样的情况了,最初图省事,结果后来反倒麻烦,不得不重构,采用
Preserve Whole Object
(
288
)搞定了。
2.1.5
Switch Statements
(
switch
惊悚现身)
前面提到的那个视频系统出现
6
个
if
语句,其实这
6
个
if
语句是包含在
switch
分支中的一个
case
里的。这个
switch
比那
6
个
if
语句还要恐怖,它不仅意味着重复代码,因为两个分支在执行类似的代码,只是变量赋给的值不同,还意味着魔法数字的出现。由分支条件控制,整个程序在这两段中来回蹦(谢天谢地只有两个分支)。
2.1.6
Lazy Class
(冗赘类)
有些因为经过不断重构而逐步失去自身价值的类——只剩下一个空壳了——就让它消失吧。这种类在项目中实在是让后续的开发人员浪费大量宝贵时间。其实这一点推而广之就是程序中任何没什么用处的代码立马删掉,然而现实的某些想法常常让人放弃这么做,最可怕的是在最开始这种现象初露端倪时姑息纵容,到后来就变得愈发不可收拾了。这种现实想法往往是旧的代码先不要删,这样如果增加的代码不能更好的工作可以退回去,然而新的代码可以正常工作后,又懒得去管那些废弃的代码了,于是就留下了很多遗留代码。由于大型项目往往都不会是一伙人做完,跨越六七年的项目中途换将也是有情可原,即使不会换,但是维护和开发也有可能是两拨人,这样后继者也不知道哪些代码是有用的哪些是废弃的,很多时间浪费在了读根本没有价值的废代码上了,而且大家还不敢删除,怕引起麻烦,这样废代码越来越多,最后有价值的代码被废代码淹没了,项目也就死了。
2.1.7
Speculative Generality
(夸夸其谈未来性)
这种现象也特别普遍,很多人都在为莫须有的功能努力,记得网上流传的真正的程序员的特点有一条叫真正的程序员认为自己比用户更清楚用户想要什么。在某些情况下确实是这样,因为毕竟我们可能做过许多类的项目,发现了客户都是在软件不断的迭代中学习逐步了解自己需要什么,我们就可能产生某种预见性,估计到客户也可能像过去那些同类客户一样,在未来产生这样的需求。不过这个事情就不好说了,因为我也碰到过我们替用户想需求,结果并不是客户想要的,我们费了好大劲实现的功能客户一点都不看好,只好废弃重写。我们那时候做人力资源系统中的工资模板管理模块时就遇到了这种情况。
2.1.8
Data Class
(纯稚的数据类)
这个就是被称为哑对象的类,
Rod Johnson
也曾在其著作中批评过
TO
这样的哑对象。这种类只有属性和存取器方法,没有任何其他的方法。我对
Data Class
没有那么极端的鲜明的好恶,因为那时候在用
JSP+Servlet+Javabean
模仿
MVC
框架和
O/R Mapping
框架时使用
VO
、
PO
这样的
Data Class
,觉得还是挺有作用的,它可以使得显示层更加的单纯,将逻辑封装在业务门面中,然后将显示结果封装在
VO
里,然后直接与
JSP
页面形成对应关系;数据访问也同理,感觉很好的解决了一些问题。后来看
Struts
、
Hibernate
时,发现它们借助
IDE
或其他工具把这种
Data Class
都自动生成了,觉得挺好的,因为那些类实在没什么必要一个一个编写。
2.1.9
Comments
(过多的注释)
Fowler
指正注释过多是一种坏味道主要源于现实——糟糕的代码试图用长长的注释来弥补,是不对的。然而在我遇到的情况往往是注释乱写,有的没必要注释的地方写了很多注释,反而很难理解的算法、流程反而没有注释了。有很多方法如
getUserByID
(
int id
),本来很明确了,非要写注释;而一套复杂的商业规则,竟然一行注释都没有,仅有的注释就是变量名(如果变量名如果起的能自名义,根本就不需要注释)。这种现象的出现主要是作者本身也没有特别清晰的思路,并没有理顺好算法或规则,所以写不出清晰的注释——但这种注释很重要,要不鬼都看不懂,特别是出现魔法数字时(当然尽量避免出现这种情况)。
2.1.10
其他
一共有
22
种坏味道在这本书里,但是我只举出
9
个,因为这
9
个属于比较浅层次的,还有一些关于父类子类继承关系的坏味道,因为我没怎么用过这种继承,周围人也很几乎没用过,所以就没遇过这种坏味道了。上面写过的坏味道其实只是借助它们的名字说一些问题,这些问题造成了程序质量的低下,难以维护,难以扩展,容易出错,看也看不懂,改也很费事。我想一个有经验的程序员看了这
22
种坏味道(当然在实际项目开发中还有很多坏味道书里没有提及),应该就会有自己的一些解决方案了,很多方法跟
Fowler
提供的方法是一样的,这一点
Fowler
在书的前面也提到了。但是建议大家看看这本书,
Fowler
已经提出了每种坏味道的解决方案,如果已经看完了所有的重构方法,也可以看这部分来复习所学的主要的重构方法。
2.2
什么是重构
按照
Fowler
的说法,重构有两个定义,一个是其作为名词的定义,一个是其作为动词的定义。
重构(名词):对软件内部结构的一种调整,目的是在不改变
[
软件之可察行为
]
前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构准则(手法),在不改变
[
软件之可察行为
]
前提下,调整其结构。
相关的细节我就不解释了,因为很简单。
2.3
构筑测试体系
这一点也是我这次去广州做项目体会到的,起初也没写测试程序,重构时担惊受怕的,后来针对一些方法写了测试程序,确实大大提高了效率,以后再作开发一定要写测试,虽然表面上费时,但实际上可以省去很多反复调试的时间,那次是用
Delphi 7
,也没什么单元测试工具,自己写测试比较麻烦,而在
Java
中有了
JUnit
,在
Delphi 2005
中也有了
DUnit
,应该好好珍惜,测试驱动是绝对有好处的。而且重构不写测试,很容易改错,我就是在修改程序时有一个对象原先需要创建,后来不需要了,我只删掉了它的对象销毁方法,却忘了删掉创建代码,结果花了一天时间来找错,后来实在不行了,编写测试,才搞定了,天大的教训啊。
2.4
重新组织你的函数
在这一部分我觉得最有价值的是
Extract Method
(提炼函数),其他的倒是没太发现太大的用处,可能是因为我没遇到过这种情况吧。后面的有些方法我觉得有点过于纯化代码了,应该适度采取,在复杂的环境下,不排除它们是好的方法。(毕竟
Fowler
比我的经验多得多,见过的项目和程序也多得多,以后可能会在某个时候用到它们)
2.5
其他
像《重构》这样的书,和《设计模式》一样,都不是看了一遍就可以束之高阁的,在实践中遇到困难回过头来看,慢慢才会真正提高。
另外这本书本来熊节翻译就好了嘛,我看他参与翻译的《
J2EE
核心模式》不是很好嘛,干嘛非要拉上侯捷,最烦看他翻译的书,不使用大陆通用的术语翻译方法,还总是用些不明不白含糊其辞的文言,看了真是不爽。我还买了本英文版的,等回去对照那本一起看吧——另外一定得好好学英语了,要不它会成为我发展的瓶颈的。
3
、设计模式
只是将这些模式总结到了一个表里了,设计模式更是没有编码经验就看不懂的东西,以前变成时还用过工厂方法,确实解决了不少问题。而且我越来越发现,很多高层次的书都离开不设计模式。比如那时候看《
Ajax in action
》,又有多少新东西呢,到后面就都是设计模式,工具的介绍,实际案例等等内容了,那时看过一篇文章说高手看书快,因为每本书的新东西都很有限,其实一点都不假,因为我已经看过《
Ajax
基础教程》了,再加上《设计模式》,《
J2EE
核心模式》,一些工具(无非就是测试工具、调试工具、建模工具等),一些第三方类库(框架),再加上点项目介绍,如果对这些部分多很了解,那么给它们套上一层
Ajax
的罩衣,不是很容易吗?
Ajax
,如果熟悉
Javascript
,熟悉
DOM
,熟悉
XML
,熟悉某种服务器端语言,熟悉
ActiveX
等模型,只需要学习一下
XMLHttp
对象不就
OK
了吗?根本不需要太长时间,也没有任何难度。然后还可能更进一步了解一些诸如企业级服务(远程调用,事务处理等),管理方面的知识,不过如此嘛。当然这只是研究的一个方向,这里面一定存在很多最佳实践,但是仔细研究那些最佳实践甚至是模式,也只有一小部分是真正的创新,很少很少。其实学习
SOA
技术也不过如此,学学分析模式,分布式系统架构的知识,
XML
、
J2EE
发展史等等,
SOA
也不是什么新的会让
35
岁以后的人学不会的东西。这个话题激发了我想写一篇怎么才不会
35
岁之后因为跟不上而被淘汰的文章了,好了,就写这么多了。
|
目的
|
|
创建型
|
结构型
|
行为型
|
|
范围
|
类
|
将对象的部分创建工作延迟到子类
|
使用继承机制组合类
|
使用继承描述算法和控制流
|
|
Adapter
(类)
|
|
Factory Method
|
Interpreter
|
|
定义一个用于创建对象的接口,让子类决定将哪一个类实例化,
Factory Method
是一个类的实例化延迟到子类
|
将一个类的接口转换成客户希望的另一个接口。
Adapter
模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
|
给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
|
|
Template Method
|
|
定一个操作中的算法的骨架,而将一些步骤延迟到子类中。
Template Method
使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
|
|
|
对象
|
将对象的部分创建工作延迟到另一个对象中
|
描述对象的组装方式
|
描述一组对象怎样协作完成单个对象所无法完成的任务
|
|
Adapter
(
对象
)
|
|
Chain of Responsibility
|
|
Abstract Factory
|
为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。
|
|
Command
|
|
|
将一个请求封装成为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。
|
|
提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类
|
Iterator
|
|
Bridge
|
|
提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示
|
|
Mediator
|
|
Builder
|
将抽象部分与它的实现部分分离,使他们都可以独立的变化
|
用一个中介对象来封装一系列对象交互。中介者使各对象不需要显示的相互引用,从而使其耦合松散,而且可以独立的改变它们之间的交互。
|
|
Memento
|
|
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
|
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到保存的状态。
|
|
Composite
|
|
将对象组合成树形结构以表示“部分
-
整体”的层次结构。
Composite
使得客户对单个对象和复合对象的使用具有一致性。
|
|
Prototype
|
Observer
|
|
定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖它的对象都得到通知并自动刷新。
|
|
用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象
|
|
Singleton
|
|
State
|
|
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。
|
|
保证一个类仅有一个实例,并提供一个访问它的全局访问点
|
|
Decorator
|
|
动态地给一个对象添加一些额外的职责。就扩展功能而言,
Decorator
模式比生成子类方式更为灵活
|
Strategy
|
|
定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。本模式使得算法的变化可独立于使用它的客户。
|
|
Facade
|
Visitor
|
|
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这
些元素的新操作。
|
|
为子系统中的一组接口提供一个一致的界面,
Façade
模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
|
|
Flyweight
|
|
|
运用共享技术有效的支持大量细颗粒度的对象
|
|
Proxy
|
|
为其他对象提供一个代理以控制对这个对象的访问
|
|