Refactoring to Patterns
简介
撰文/Joshua Kerievsky 编译/透明
模式是面向对象设计的基石,而测试优先的编程方法和大刀阔斧的重构则是进化式设计的基石。为了避免过分设计或者设计不足,我们有必要学习如何让模式适应新的、渐进式的软件开发节奏。
——Joshua Kerievsky
软件模式的伟大之处就在于:它们传递了许多有用的设计思想。那么,如果学了一大堆模式,你就能成为一个相当优秀的软件设计师,对吗?当我学了、用了几十个模式之后,我也这样想过。这些模式帮助我开发了更灵活的框架,建造了更坚固、更具扩展性的软件系统。但是,两年以后,我发现:对模式的了解和使用常常让我陷入过分设计的境地。
设计技能再提升一个档次之后,我发现自己开始以另一种方式使用模式:重构以得到模式,而不是在前期设计中使用模式,也不过早地把它们引入代码之中。这种新的方式来自于极限编程(XP)的设计实践,它使我的设计不多不少,恰到好处。
什么是过分设计?
当你把代码搞得比需要的还要复杂、还要灵活时,你就是在过分设计。有些人这样做,是因为他们相信自己知道系统未来的需求。他们认为:最好是今天就把设计做得灵活些、复杂些,这样它就能适应明天的需要。听起来很美,如果你能未卜先知的话。
但是,如果你的预言错了,你就是在浪费宝贵的时间和金钱。花上几天乃至几周的时间去给设计增加不必要的灵活性,这种情况很常见——结果,给系统加新功能、排除错误的时间就少了。
如果你按照预先的设想写出了代码,但是预想的需要却没有出现,怎么办?你不会删掉它,因为删掉它会比较麻烦,或者你还希望以后能用上这些东西。不管出于什么原因吧,反正这些过分灵活、过分复杂的代码越积越多,你和团队中其他的程序员就不得不在越来越多、越来越复杂的代码基础上工作。
为了补偿这一切,人们决定把系统分成独立的小块,各自负责一块。这看起来能让他们的工作轻松些,但是却有副作用:每个人都按照自己的想法去写代码,很少看看别人是不是已经做过了自己需要的工作,所以就产生很多的重复代码。
过分设计会影响生产力,因为当别人接过一个这样的设计时,他不得不花些时间来了解设计的细微之处,然后才能舒服地扩展或维护它。
过分设计总是悄悄地发生,许多设计师和程序员甚至都没有意识到自己正在过分设计。当他们的公司发现团队生产力下降时,很少有人知道过分设计在其中扮演的角色。
程序员们会过分设计,也许主要的原因是他们不想被糟糕的设计困住。糟糕的设计常常跟代码结合得过于紧密,所以很难改进。我也曾经遇到过这种事,所以我才会这么喜欢使用模式预先做好设计。
模式,万灵药
当我第一次开始学习模式的时候,它们向我展示了一种灵活、精妙而优雅的面向对象设计方法,这正是我非常想掌握的。在系统地学习了模式之后,我就开始使用它们来改进以前的系统、构造以后的系统。由于我知道模式能带来怎样的效果,所以我觉得自己走的肯定是一条正确的路。
但是,随着时间的推移,模式的威力让我对更简单的编写代码的方式视而不见。只要发现有两种或者三种不同的计算方法,我马上就跑去实现Strategy模式,但是实际上一个简单的条件表达式会更简单、更容易实现,也更加实用。
有一件事情让我对模式的专注显露无遗。当时,我和另一个人一起写程序,我们俩已经写了这样一个类:它实现了Java的TreeModel接口,用来在一个树型窗口控件中显示特定对象的图案。我们的代码能够工作,但是树型控件在显示时会调用每个被显示对象的toString()方法,但这个方法并不能返回我们想要的信息。我们也不能修改toString()方法,因为系统的其他部分对这个方法还有依赖。于是,我们开始想办法。习惯所致,我马上就想到了模式。首先浮现在我脑海中的是Decorator模式,于是我就提议用Decorator模式来封装要显示的对象,然后重载封装对象的toString()方法。可惜,我的搭档的回答让我大吃一惊:“杀鸡焉用牛刀?”他创建了一个名叫NodeDisplay的类,它的构造子接收一个待显示类的实例。NodeDisplay类很容易写,因为它的全部代码量还不到10行。而我的Decorator模式起码需要50行代码,要通过很多次委托才能调用到需要显示的对象。
象这样的经验让我知道:我不能再这样过多地考虑模式,必须重新回到短小精干的代码。于是,一次抉择摆在了我的面前:为了成为更好的软件设计者,我费了很大的力气来学习模式;但是现在,为了再提升一个层次,我不能再依赖它们。
欲速则不达
要提高一个档次,还必须学会避免设计不足。相比过分设计,设计不足的情况要常见得多。如果我们单单想着给系统增加越来越多的功能,而不随时留心改善设计,那就很可能会犯设计不足的错误。许多程序员都是这样干的——起码我曾经这样干过。你只管让代码能运行,然后就去完成下一个任务,再也不会回来改进现在写的代码。当然,你希望常常回来维护自己的代码,但是要么因为你自己太忙,要么因为经理和客户催得太紧,最后你还是没有时间。
这是对软件的不尊重,它会使你的软件开发节奏变成“快、慢、更慢”,就象这样:
1. 你很快地发布了系统的1.0版本,但是代码质量很龌龊。
2. 你想发布2.0版本,但是龌龊的代码减慢了你的开发速度。
3. 当你想发布未来的版本时,由于龌龊代码越来越多,你的速度也越来越慢,直到客户和老板对这个系统失去信心。
在我们这个行业里,这种事情是司空见惯的,它会大大降低企业的竞争力。还好,还有另一条比较好的路。
苏格拉底式开发
测试优先的编程、大刀阔斧的重构,这两个来自极限编程的实践彻底改变了我开发软件的方式。我发现,这两个实践既帮助了我,也帮助了整个公司。我们既不会过分设计,也不会设计不足,节约下大量的时间来满足我们的需要:构造良好的系统,按时交付。
测试优先的编程方法把程序设计变成了苏格拉底式问答:写测试就是向你的系统提一个问题,然后你就应该编写代码来回答这个问题。再重复这个问答过程,直到你得到需要的系统。这样的方法大大提高了编码的有效程度。这种编程节奏让我感觉耳目一新:无须再把系统的每个细枝末节都考虑清楚。测试优先的编程方法让我可以先实现主要的功能,然后再根据需要对其进行改进,使其获得所需的灵活性。
大刀阔斧的重构则是这个渐进式设计过程中的一部分。重构是“保持行为的代码转换”,或者按照Martin Fowler的定义,重构是“在不改变代码外在行为的前提下对其做出修改,以改进代码的内部结构,使其更容易理解或修改”。
当苏格拉底要讲一个道理的时候,他总是会采用问答的形式。当受教者答出一个答案时,苏格拉底会再问更多的问题。通过这些问题,他帮助受教者抛开无关紧要的东西,理清混乱的思路,巩固学到的知识。而重构扮演的正是这样一个角色。当你进行重构的时候,你不断地解剖自己的代码,去除其中的重复,使它变得越来越清晰、越来越简洁。
重构也是需要技巧的。你不能制定出一个改进系统设计的时间表,到了规定的时间再重构;你必须在任何需要的时候重构。重构可以提高你的代码质量,从而使你可以保持健康的开发节奏。Martin Fowler等人撰写的《系统重构:改善现有设计》[1]一书记录了很多重构方法,其中每一条都指出了一种常见的改进需求和完成改进的步骤。
为什么要通过重构得到模式?
在许多不同的项目中,我观察了自己和同事重构的方式。在使用Fowler的书中记载的许多重构方法的同时,我们还是发现模式也能帮助我们改善设计。在这些时候,我们就要通过重构来得到模式,从而避免制造出过分的灵活性或者不必要的复杂度。
“重构得到模式”的动机和“不依赖模式进行重构”的动机是一样的:减少或去除重复,简化设计,使代码能更好地表达自己的意图。
但是,重构得到模式不是照搬书上的模式。举个例子吧,让我们看看书上记载的Decorator模式的“意图”和“适用性”,然后再看看Erich Gamma和Kent Beck在JUnit中通过重构得到Decorator模式的动机。
Decorator模式的意图:[2]
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。
Decorator模式的适用性:
以下情况使用Decorator模式
l 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
l 处理那些可以撤消的职责。
l 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
在JUnit中通过重构得到Decorator模式的动机
为什么要重构得到Decorator模式?Erich记得有下列几个原因:
“有人添加了TestSetup类,并将其作为TestSuite的子类。在增加了RepeatedTestCase和ActiveTestCase两个类以后,我们发现可以通过Decorator模式来减少代码的重复。”[3]
看出来了吗?重构得到Decorator模式的动机(减少重复代码)和Decorator模式的“意图”和“适用性”(作为子类的动态替代品)几乎没有任何关系。对于其他的模式,我也发现了类似的情况。请看下面的例子:
模式
意图(GoF)[4]
重构动机
Builder
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
简化代码
去除重复
减少创建错误
Factory Method
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。
去除重复
表达意图
Template Method
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
去除重复
基于这些观察,我开始记录一些“重构得到模式”的方法,用它们来讲述模式对改进设计的意义。在此过程中,我使用了来自真实项目的重构作为例子,只有这样才能准确地描述系统的约束和模式的用途。
我对“重构得到模式”的研究是对Martin Fowler的研究的延续。在他收录的重构中,有一些重构得到模式的例子:
· Form Template Method (345)
· Introduce Null Object (260)
· Replace Constructor with Factory Method (304)
· Replace Type Code with State/Strategy (227)
· Duplicate Observed Data (189)
另外,Fowler还提到:
在模式和重构之间有着天然的联系。模式是你想要抵达的目的地;重构则是从另一个地方过去的路。[5]
这也正好与《设计模式》四位作者的观点不谋而合:
我们的设计模式记录了许多重构产生的设计结构。……设计模式为你的重构提供了目标。[6]
渐进式设计
时至今日,在对模式——“重构产生的设计结构”——非常熟悉了之后,我终于明白:理解重构得到模式的原因,比理解最终的结果或者实现这个结果的细节更有价值。
如果你想成为一个更好的软件设计师,那么,学习好的软件设计的发展过程会比学习这些设计本身更有价值,因为设计发展的过程才是智慧藏身之所。发展最后得到的设计结构可以帮助你,但是如果不知道它发展的历程,你就很可能会误用它,导致你的项目过分设计。
现在,我们的软件设计文献过分集中于好的解决方案,而不太重视这些解决方案的发展演化过程。我们需要改变这种情况。重构的资料能够帮助我们更好地理解一个好的设计方案,因为它揭示了这些方案的演化过程。
对于大多数的模式,我们都必须这样对待:在重构的场景中观察模式,而不仅仅把模式视为可复用的软件单元。这也许是我收集、编写这一系列文章的最初用意吧。
通过学习渐进式的设计方法,你就能够成为一个更好的软件设计师,并且减少过分设计和设计不足的几率。测试优先的编程方法和大刀阔斧的重构则是渐进式设计的关键。学会通过重构得到模式,你会发现自己的能力更强,完全能够做出优秀的设计。
写作目标
迄今为止,我已经写了十多篇关于重构的文章,此外还有其他主题的文章。写这一系列文章的目的是要帮助你学会如何
l 在适当的时候通过重构得到模式,当有更简单的方案出现时则抛弃模式
l 使用模式来表达自己的意图
l 了解并继续学习大量的模式
l 了解如何以简单的方式和复杂的方式实现模式
l 使用模式使代码更简练、更清晰
l 渐进式设计
我在这里使用的格式和Martin在《系统重构》中使用的格式几乎完全一样。另外,我还加上了下列元素:
l 交流、重复和简化
l 在“过程”中加入编了号的步骤,与“范例”中的步骤一一对应。
我的系列文章将会延续相当长的时间,所以欢迎给我反馈——请把你的想法、注释或者问题发送到joshua@industriallogic.com[7]。本系列文章将一直在Internet上公开,地址是http://industriallogic.com/xp/refactoring/[8]。
我还开了一个邮件列表(refactoring@yahoogroup.com[9])用于讨论重构、设计模式和支持自动化重构的IDE等等问题。
[1] 译注:此书名是在侯捷先生译本基础上、辅以大陆用词习惯得到的。原书名是Refactoring: Improving the Design of Existing Code,Addison-Wesley 1999年出版。
[2] 译注:Eric Gamma等著,李英军等译,《设计模式》,机械工业出版社2000年出版,第115页。
[3] 译注:这段话出自Eric Gamma与作者的私人信件。
[4] 译注:这里的“意图”全部引用李英军等人翻译的《设计模式》。
[5] 译注:这句话出自《系统重构》,是我自作主张翻译的。
[6] 译注:引自《设计模式》,第234页。
[7] 译注:这是作者的信箱。如果你对中文译本有什么想法,或者如果你不想用英文跟作者交流,请告诉我:gigix@umlchina.com
[8] 译注:这是原文的地址。中文译本在我的主页上公开。
[9] 译注:目前没有对应的中文邮件列表,可以在现有的邮件列表(DPE_cn@yahoogroups.com)上讨论相关问题。