简化设计
为什么设计应当是简单的?
传统的软件方法偏向于进行一次性的Upfront设计,我们知道这很难。
软件方法学的设计者通常喜欢用建筑打比方。他们说,如果你要建造一座大厦,那么在你画完所有的施工图和建筑规范之前,你从来不应该开始真正施工。
我喜欢这样的比喻,因为建筑学的发展经历了几千年甚至可能是上万年,而软件行业,最多也只有5,60年的时间,我们需要从建筑中学习很多东西。就如模式语言的提出者,著名的建筑学家,Christopher Alexander ,给了设计模式很大的启发。
但是,如果我们只是从建筑学到东西,恐怕还是远远不够。我还不知道哪一栋大厦的外形能够发生变化,或者哪一个大商场在发现自动扶梯位置不太合适时,能够自由移动到一个更加合适的地方(我附近一家大超市的自动扶梯真的让我想到了这一点)。也许要部分达到这样的目的还是有可能的,但肯定要花费极高的代价。但是如果我们去构造一个企业的应用系统,那么我们必须准备好企业发展的同时需要对我们的软件做出改变。
软件的核心就是可变。这种变化不仅仅在于需求的变化,还在于人们理解的变化,而软件这种人活动的产物,必须要随之发生改变。
增量迭代的开发方法学是对软件变化的一种响应。但是正如我们前面所讲,增量迭代模型有可能对系统的设计提出比传统的Waterfall更复杂的要求。你需要预期所有可能的情形,然后才能保证你是可以增量的,也是能够迭代的。
这种两难的境地来源于一个基本的假设:
削减软件成本的最主要方法是减少返工的成本以及它的可能性。
传统的软件发法学认为,如果我能够尽量早地冻结需求,那么我修改设计的可能性和成本就越小,如果我的设计能够尽早地冻结,那么我修改实现的可能性和成本就越小。要想早早冻结设计,它必须是非常详尽而复杂的。
我把这种思路称为"改变恐惧症",不仅仅因为变化是最难研究的东西(制订出条条框框则简单的多),也因为我们没有足够的经验来适应变化,更重要的是,从来没有哪个时代比今天变化更快。
现在我们从另一个角度来想问题:
我们是否可能通过增加返工的方法来削减软件的成本?
这个方法初看起来自相矛盾。但是Kent Beck说:
The key is that risk is money just as much as time is money。
既然系统在变,那么你不能肯定今天设计的东西一定能够被日后用到。而你需要等待设计完成的成本会很高,因为在你的设计完成之前,没有人可以开始工作。
设计不是独立的。别人要使用你的设计,他必须能够理解你的设计。如果你今天的设计十分复杂,那么你就增加了从今天开始的所有开销,更多的东西需要检查和测试,更多的东西需要理解,更多的东西需要解释。更重要的是,你不能估算出明日的成本。你必须估计日后将要发生什么事情,通常你不可能准确无误地做到这一点。
所以,让设计简单,我们可以通过不断地增量修改设计使得系统更加接近需求,就象我们开车,每次都做一些小小的调整,最后达到目的地。
什么才是简单的设计?
要让设计简单,是否意味着我们可以随意fix and build,写一段,然后任意修改它。但是简单并不意味着随意,当然更不意味着愚蠢。
就象我在Duplicate Code里指出的一样,Once and Only Once通常是代码最简单的形式,因为它没有重复,每一个类都有自己简单的责任,每一个方法都有自己简单的意图。
简单的设计最重要的特性就是容易适应变化.为了达到这样的目的,简单设计应当:
能够简单地被理解.这依赖于你代码的可理解性.只有可理解,下面的简单性才能达到. 能够简单地被修改和扩展.OO系统往往通过增量的方法改变或增加系统行为,所以也就是要被简单地重用,这要求我们没有重复代码. 有最少数目的类.要易于理解系统,那么系统中每一个类应当和需要解决的问题的每一个重要概念相对应,如果人工加入许多毫无意义的类或者太多与问题概念无法对应的类,系统将无法理解 有最少数目的方法.每一个方法应该有他独立的意义, 如果没有可以言述意图的名字,那么系统将难以理解.如果你看过Kent Beck和其他Agile联盟作者的文章,或者你看过我的Duplicate Code和Long Method,你可能会觉得2和3,4存在着矛盾.因为要达到2的目的,你必须有很多小类和精干的方法,而3,4则要求你具有最少的类和方法.要理解它们之间其实一致的关键在于,你如何决定需要一个新类或者需要一个新的方法.在进行Refactoring时,如果你觉得系统的某一部分具有独立的可对应问题领域的概念时,那么你应当毫不犹豫地使用Extract Class形成一个新类.反之,某一个类无法具有其独立的应用意义,或者该类数据太多而没有行为,这个时候你应当考虑这个类是不是应该被删除.当然,这里面有些例外(如method object)同样道理,如果一个代码片断能够有独立意图的行为,那么不管它的大小,可能是一个简单的表达式,都应该有独立的方法,但如果没有这样清晰的意图,再多的代码都可以在一个方法里面.
显然,简单的设计并非我们想象的那么简单.除了我们可以不太考虑以后解决的问题外,简单,作为一种重要审美标准.不是轻易能够达到的.我们一开始的设计往往不能够达到这样的要求,可能是类的划分不合理,可能存在着重复的代码,可能行为的分配需要调整,要达到简单,你就必须Refactoring你的代码,使设计更加合理.
Refactoring如何支持简单的upfront设计?
开始,你可以作简单的设计,也许是一个类,2、3个方法,你针对这个类写出test case,实现代码,让test case通过。在实现这个类的过程中,你发现2、3个方法太长,它们之间会有很多重复代码,于是你进行refactoring,你使用extract method让方法更能揭示意图。然后,你解决另外一个问题,你写出另外一个Test case,可能和上面一个类没有多大的关系,也可能你发现这个类在以前的类上附加了一些功能。这时,你会发现直接对前面一个类进行修改比较困难,你对前面一个类进行refactoring,让它更加容易加入。如果随着新功能的加入,发现前面一个类其实包含了两个应用概念,你可能需要对这个类进行重构,把它拆分为两个类。你进行refactoring,让所有的Test cast都能通过测试。你接着又选取一个需求,增加新的test case,然后实现它。这时候你发现这个类和原来的某个类具有很多类似的地方,你可能需要把这两个类进行进一步的抽象,通过refactoring形成一个新的超类。。。
随着时间的推移,越来越多的需求被实现,系统变得越来越大,某一天,你发觉系统有些变样,你觉得有必要修改系统的某一部分结构。这时候,你和你的同僚可能需要暂时摘下增加功能的帽子。你们可能需要更多的讨论,交流,使用任何有意义的方法为你们的讨论增强交流的效果,白板、CRC,然后进行Refactoring。
Kent Beck指出,某些大的Refactoring可能并不是一天就能完成的。它可能要花费几天甚至一个月的时间。但是,你还需要继续完成用户的需求。这时采取的方法就是小步的增量改变。在实现新需求,写下新的test case时,你可能看到一个机会能够让你的代码朝着大目标前进一步。使用这样的方法,每一次你的工作可能是移动一个变量或一个方法。但是随着新功能进一步的加入,这样的机会不断出现。最终,一个大的目标会变成很小的工作。这时候,你已经水到渠成,花上几分钟就完成了。
Refactoring的这种工作方式可以大大减少UpFront的设计量,它同时使你的设计变为一种必要和需求的产物,这种为了更好地加入新需求所做的设计,更准确地反映了问题的本身。同时,它使得设计随着你对问题的进一步深入而逐渐变得更合理,随着你对新技术的掌握而变得聪明,这是一种进化的设计方法。
设计模式
简单设计的要求
Refactoring对简单设计的支持好像使设计模式变得有些过时。如果我一开始只需要写下一个简单的test cast、实现、refactoring,增加test cast、实现、refactoring。这是否意味着设计模式就毫无意义了呢?
对设计模式和Refactoring,以及与之相关的Agile联盟方法学的研究给我一个很深刻的印象。一个方面,就像Martin Fowler指出,Agile方法学的提倡者往往也是模式社团的领导者。另一方面,如果你仔细体会一下Agile 宣言,你会发觉它们非常重视人的能力。在我自己组织小组试用XP的过程中也发现,XP对它的参与者有很高的要求。
我们已经看到,简单的设计并不是愚蠢的设计,相反,它们是极端聪明的设计。要实现简单的设计,你必须有丰富的知识。这里在于,简单设计提倡你不要太多考虑以后的具体需求本身,但它确实需要你考虑代码和设计的结构。在用Refactoring武装你的思想中,我已经讲到,如果你的设计结构完全不考虑以后的扩展,那么Refactoring也无能为力。
设计模式是面向对象社团对设计的一个重要总结。其实,它们也是对简单设计的最好诠释。因为,好的设计模式,就是对于解决问题所能发现的最简单、最易重用的设计结构。另一方面,使用广为人知的设计模式也使得你的代码更容易为人理解、接受,大大提高了交流的效率、宽度和深度。
从这种意义上将,Refactoring不但没有排除设计模式,它更促进了设计模式的学习、发现和验证,以及更广泛的应用。Agile对程序员素质的要求,在这一点上也得到了充分的体现。一旦小组成员具备熟练运用设计模式的能力,你的设计可能就是一句话:用XXX模式。(在我领导的EOSP2P项目中,这个模式就是半State/半策略模式。当我说出这个模式时,我们发觉整个小组不再需要upfront设计,我们构建,然后Refactoring。当然,象Factory method模式几乎使每个系统都应当使用的。还有后续的proxy模式,都让设计变得如此轻松)。
但是,在应用设计模式的过程中,你必须时时记住简单性这个原则。设计模式最好的应用方法就是从简单开始,只要能够解决问题,抓住这种模式的核心,那么模式良好的结构保证你的Refactoring建立在一个坚实的基础之上。
所以,为了使Refactoring能够更好地进行,你需要更多的学习模式,因为设计模式不但是良好设计的开始,同时也是Refactoring的目标。
Refactoring的目标
Refactoring有时会失去目标,你可能会产生循环的Refactoring,或者你无法判断Refactoring的结果到底有没有让程序的结构更好。
作为OO社团最重要贡献之一的设计模式能够为Refactoring提供一个明确的目标。在Ralph Johnson多篇关于Refactoring的论文中都提到了这个问题。Martin Fowler的《Refactoring》中明确指出的就有state、strategy、visitor等模式。更深入一步,设计模式不但是Refactoring的目标,同时也为看起来有些零散的行为提供了一个全局性的指引。
Christopher Alexander在《The Timeless way of Building》一书中对模式的定义如下:
Each pattern is a three-part rule, which expression a relation between a certain context, a problem, and a solution.
很多人在软件领域对他的定义做了修改和扩展,但这三部分内容是不变的。把这个定义放到我们软件上下文来看,设计模式有两个重要特性:
设计模式高于代码层,他不是描述一个好的编码风格,或者某种编程习惯用语; 设计模式不是纯粹理论上的体系结构或者分析方法,它是一种可实际操作的东西。模式界提出的Three Rules就说,你必须提出三个实际的应用例子才能够得上设计模式的条件。正因如此,很多人都把设计模式称之为micro-architecture。显然,由于它的可操作性,它可以通过Refactoring的方法达到。同时由于它一定层次的理论和总结性,他可以用于引导Refactoring并作为Refactoring的目标。
关于作者
石一楹,现任浙江大学灵峰科技开发公司技术总监。多年从事OO系统分析、设计。现主要研究方向为Refactoring和分析模式。
本文地址:http://com.8s8s.com/it/it36717.htm