来源:《
软件设计精要与模式》
设计没有标准,只有目标。如果硬要制定一个标准,那么标准就是快捷、适用与优雅。如何从没有标准的设计中体验设计的乐趣,寻求问题的解决之道,成了我们软件设计者生命不可承受之重。与众多创造了灿烂文化的艺术家一样,作为软件产品的设计者,设计常常成了一种苦闷的象征。就如困守在地球之上的古代人类一般,因为渴望飞翔的自由,于是搔首问天,俯仰天地,体察宇宙与璀璨的星辰,从而发现了天体的运行轨迹与宇宙的奥秘。软件设计师在经历了众多挫折与失败后,似乎也得到了一种指引,逐渐地开始了探索设计之道的奥秘,企图叩开那道紧紧关闭的镂有神秘咒语的设计之门。
终于,设计大师们获准进入了那穹顶高悬的殿堂,此时才发现我们又进入了一座迷宫。正是这样,当今的软件设计,不患无标准,而患标准何其之多。在众多设计标准之前,我们都希望找到最好的设计方案,然而什么是最好,每个人都有自己心中的“哈姆雷特”。王子复仇记可以在不同的舞台上被演绎为各种不同的版本,不过对于软件设计而言,我想我可以说:满足客户需求的设计就是最好的标准!然而,前提是怎样通过设计来满足客户需求?
计划的设计与演进的设计
通常来说,软件设计不外乎两种方式:计划的设计和演进的设计。很多人认为,计划的设计更符合工程学的理念。设计的复杂度视项目的规模而定。就好似建造房屋,如果我们仅仅为了容身的一席之地,需要搭建一间茅屋,那么我们只需要准备好土石与茅草,以及夯土用的工具,再请上几个工人,吆喝着就可以开始筑屋的工作了。没有设计图纸,没有详细的规划,也没有建筑大师的匠心独运,茅屋在开工之初就失去了建筑学的普遍意义,它仅仅是随意设计的简陋产物。我们召集工人夯好土墙,然后在屋顶堆放好茅草,梳理几番,就算完工。
以茅屋这般简陋的产品而言,对于计划的设计难免会产生牛刀之叹。然而一旦我们面对诸如建造苏州拙政园这般堪称世界园林史奇迹的建筑,就非得胸中有丘壑的设计大师不能为。我们必须在建筑之前对整个园林的设计有一个通盘的考虑,并参考居者意见,方可以确定设计方案,并最终完成翔实细致的设计图纸。楼台亭榭,曲水通幽,庭院深深,宁静致远,拙政园的建筑可以说无一处不是独具匠心。何处应该堆叠假山,何处应该开辟池塘,亭子或飞或踞的姿态,庭院错落别致的分布,乃至于园内的一花一木,都是精心设计的结果。没有详尽的计划,又何来这疏朗典雅的拙政园?
建筑如此,大型项目的软件设计也有类似之处。在项目的准备期间,计划的设计要求我们首要考虑的不是编码,而是整个系统的架构。通过与客户的充分交流,我们整理出项目需求,并根据需求考虑系统中的功能模块以及系统的核心功能。我们需要把握项目开发的进度,并对于项目开发可能需要的工具与技术做出抉择。我们需要遵循统一的设计规范,并对那些可能存在的技术难点进行预研。我们需要确定每一个模块的功能,以及模块间的关系和系统分布的层次。就好比亭台、楼阁在园林中的位置与造型,对于整个系统架构的组成,我们需要匠心独运,高屋建瓴,从一个抽象与宏观的层面来考虑。
世界上最精致优美的建筑风景,并不都是计划设计的产物。例如“集中国水乡之美”的周庄,那些河埠廊坊,过街骑楼以及临河的水阁瓦墙,并不是某位建筑大师呕心沥血的杰作,而是经历了数百年的历史沧桑,渐进地增添与更替各种建筑,最后形成现在这般灵秀的水乡风貌。
演进的设计正是如此,它是一个渐进的过程。演进的设计并不要求前期的设计有多么的完美,实现的需求有多么的完整,我们仅需要把现阶段考虑的问题通过编码实现就可以了。随着演进的深入,对需求的准确理解,编码也会随之而修正,整个设计会逐渐丰满起来,经过一系列的方法,最后渐趋完美。
演进的设计并非毫无章法的设计,它仍然要遵循特定的设计标准与过程控制办法。正如整个周庄的建筑,不论修建的年代,它们都具有浓郁的江南水乡风格。如果我们在周庄的临河边,修建一栋充满了欧式风格的洋房洋楼,即使造型典雅,设计卓越,却会因为破坏整体之美,而成为设计中的败笔。演进的设计,同样需要计划,同样需要架构的设计,它与计划的设计唯一的区别是设计的准则。演进设计的标准是符合客户现有的需求,而计划的设计还需要考虑未来的功能扩展。演进的设计推崇尽快地实现,而计划的设计则考虑架构的完整。二者之间似乎充满了不可调和的矛盾,各执两端。有人会质疑演进设计的简陋与平庸,他们认为没有计划,只会令设计一团糟。传统的隐喻习惯将软件开发比作建筑学,但我需要特别申明的是,二者虽然都是工程学,但软件设计与建筑设计并非完全相同,除了工艺学与开发过程的区别之外,最大的关键是需求变更的频率。软件设计最大的敌人就是需求变更。所以,我们很难在设计之初,考虑到客户的全部需求,甚至于实现未来的扩展。至少我们在设计一开始,就应该不停地拷问自己:
架构设计的标准
我不厌其烦地用建筑来比喻计划的设计与演进的设计,那是因为软件的架构(Architecture)正是从建筑学的概念引申而来。架构作为软件设计的高层部分,是一个软件系统从整体到部分的最高层次的划分,是用于支撑更细节设计的框架。无论是采用计划的设计还是演进的设计,我们都不能忽略软件架构在软件开发过程中举足轻重的地位。正如世界上许多卓越超群堪称艺术品的建筑物一样,许多设计优秀的软件系统,它们的共同特征是具有良好的体系架构。架构设计需要重视的关注点包括如下内容。
(1)程序组织(Program Organization)
系统架构必须划分出整个系统的功能模块(或者是子系统),以及正确描述模块间的关系。在架构设计文档中,这些内容应该属于逻辑视图(Logical View)的范畴。功能模块(子系统)应基于客户需求与功能特征进行合理地划分。对于面向对象技术而言,我们可以对这些功能模块(子系统)以组件或包的形式进行封装。我们需要考虑模块间的解耦,这意味着设计组件或包时需要判断哪些接口可以发布,并采取面向接口的设计方式。我们还可以引入分层的概念,并根据层次定义模块(子系统)的边界。在构建程序组织时,设计师应该与领域专家合作,理解客户需求,从而明确定义各个功能模块(子系统)的责任。从面向对象思想的角度出发,我们希望定义的所有组件或包与其他组件或包之间的依赖尽可能少,如果没有好的程序组织,系统架构会像一张蜘蛛网一般,看似四通八达,但很容易让开发人员迷失在开发进程中。
(2)数据设计(Data Design)
软件系统很难脱离数据而单独存在,因而数据设计会成为制约项目成功的关键。此外,对于软件设计而言,一个好的数据设计还能够提高软件系统的整体性能。数据设计与程序组织也是相关的,很多时候,功能模块的划分应与数据库(数据表)的划分保持一致。除了基于功能需求进行数据设计,我们还必须考虑对象与数据的映射(ORM),数据的查询性能,数据库未来的扩展及数据库的迁移。
(3)安全性(Security)
没有考虑安全性的软件系统,就好似一幢摇摇欲坠的高楼大厦,设计再好,也没人有胆量去居住。安全性并非千篇一律的特征,针对不同的领域,架构师对系统安全的关注程度也不一样,但数据安全是最起码的要求。此外,我们还需要考虑用户的授权、加密与非法攻击的防御。架构设计不仅仅考虑软件层面上的安全,还需要从硬件系统的物理分布、软件的部署环境等多方面衡量系统的安全性。
(4)性能(Performance)
我们不能奢望通过改善硬件系统使得系统性能符合客户的要求,软件设计必须从架构的层面上考虑性能的优化。例如,我们可以考虑优化数据库访问、合理地分层以及引入缓存机制。如果是Web系统,那么在设计时应该通过减少HTTP会话实现降低服务器负载的目的。在考察架构的性能时,需要与客户充分地沟通。我们需要获取与性能相关的数据,从而预先评估架构的性能指标。
(5)可扩展性(Scalability)
可扩展性往往会成为系统架构设计的致命毒药。一方面,如果不考虑系统的可扩展性,会导致未来的维护与更新步履蹒跚。另一方面,如果我们过分地重视系统的可扩展,则会给架构设计制造障碍,甚至导致过度设计。实现系统的可扩展性付出的牺牲,需要设计思想与设计方法来弥补,例如面向对象思想。我们还可以引入面向方面编程(AOP,Aspect- Oriented Programming)技术,或者利用Web Service实现面向服务架构(Service-Oriented Architecture)。
(6)可靠性(Reliability)
可靠性包括容错性和错误处理。必须考虑系统可能出现的错误和异常,以及出现错误或抛出异常后,应该如何处理?如果可能的话,我们应该考虑从错误中恢复,即使不能恢复,也要保证系统的运行不受影响。此外,统一的异常处理机制是必需的,特别是对于框架而言,有利于框架用户对框架API的调用。
(7)可用性(Usability)
例如用户界面(User Interface)的设计,系统要求界面的风格以及用户的操作必须统一,同时还应该考虑用户的体验,以及保证操作流程符合业务规则。有时候界面设计能够得到用户的认可,就意味着项目已经成功了一半。虽然,我们反对“金玉其外,败絮其中”,然而“酒好不怕巷子深”的传统营销理念,已经不符合产品交付的标准。归根结底,我们开发的产品是交付给客户使用的。
优秀的架构设计并不易得,正如优秀的架构师千金难求。但如果我们能够按照以上的要点来衡量架构的设计,或许能够尽可能早地发现架构的缺陷。不要等到考试结束交卷之后,方才因为发现错误无法修改而追悔莫及。
过度设计,还是简单设计
Kent Beck在《解析极限编程——拥抱变化》中为简单系统制定了4个评价标准,依次为(最重要的排在最前面):
说易行难。知道这些标准并不等于说我们已经遵循了这些标准。开发人员并不是喜欢故弄玄虚的空谈家,实干家的精神使得他们视复杂为猛虎,却同时明白追求简单往往意味着设计力度上的百倍付出。在设计时,我们不仅要考虑软件的功能,还要考虑软件的性能、可扩展性,模块间的耦合关系,系统的稳定、部署和更新,版本的管理,系统的安全,界面的友好程度。要想实现简单系统,可谓是蜀道之难,难于上青天!
Do the simplest thing that could possibly work! 这是XP(Extreme Programming,极限编程)人士大声疾呼的口号。还有一个响亮的口号是“You aren't going to need it”。这其中包含的核心意义就是不要为了考虑程序的可扩展性,把目前不需要的功能加入到软件中来。极限编程提倡开发人员不要“杞人忧天”,既然未来的天是否会塌陷还有待考证,不如脚踏实地关注目前需要做的事情,至少在态度上更为务实。虽然,这种做法颇有几分“目光短浅”的嫌疑,然而软件开发人员毕竟不是预言家,未来的功能扩展无法估计,与其花费精力与时间去考虑整个架构的可扩展性,倒不如集中精神对付眼前的需求。我们也许会承担未来因为扩展而承担的巨大代价,但还有一种可能是,在付出了巨大代价解决了系统可扩展性后,又因为系统的变化脱离了预期的轨道,从而导致此前的心血付诸东流。
所以,演进的设计更符合极限编程的核心准则。然而简单并非意味着简易,所谓“大道至简”,自简入繁并不难,化繁为简才真正需要大智慧。我们必须理解的一点就是,所谓的“简单系统”同时还应该保证它的有效性。举例来说,如果我们要开发一个数据库管理系统,按照当前的需求,由于数据库访问并不频繁,我们可以采用按需创建数据库连接的方式,一旦结束访问就关闭连接。但随着该系统使用人数的增多,数据库的频繁访问会使得原有的数据库连接策略成为系统的性能瓶颈,此时,我们就需要修改系统,将原有的数据库连接策略改为连接池方式,专门负责连接资源的分配与释放。
如果我们从一开始就考虑引入连接池,对于之前的简单需求而言,是否意味着过度设计呢?按需创建数据库连接的方式确实简单,但它对于客户的需求而言,是否又是最有效的解决方案?在此刻,或许被指责为过度设计的方案,在彼刻或许又被推许为最完美的方案,那么我们应该按何种标准来评判解决方案是过度设计,还是简单设计?
事实上,问题并不在于设计是否过度,而在于设计的理念。是只做目前需要的事,还是未雨绸缪,想好今后的功能扩展?这个问题的答案还需要实际的项目开发来检验,根据不同的需求,答案会因此而异。最流行的未必就是最好的,极限编程自有它的适用场景,迭代开发模式在特定的场景下也会成为最佳的软件过程方法。
需要设计模式吗
如果我们仅考虑实现当前的功能需求,我们还需要设计模式吗?坦白说,我并不认为设计模式与过度设计有关。过度设计的导火索是设计模式的滥用。很多时候,合理地利用设计模式反而能使我们的程序结构简单化,特别是,它能够让我们的开发过程更简单。
Christopher Alexander在描述城市和建筑模式时说,“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。”设计模式正是为解决软件开发领域中的类似问题,从而提出的解决方案,它作为面向对象编程经验的总结,是软件设计技巧中最重要的方法与原则。放弃设计模式,意味着希望成为优秀设计师的你,在将历史的车轮向前倒滚了十年之后,有幸成为了和GOF(Gang of Four,即设计模式的提出者或者集大成者Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides)同行的伟大先驱者。你放弃了站在前人肩膀上眺望远方的机会,而选择自己艰难吃力地踮起脚尖,最后失去的却是更远处美丽的风景。
需要设计模式吗?答案看来是不言而喻。关键一点是我们需要确定模式的应用是否过度?世界上很多天才横溢的程序员,可以在一段代码中包含6种设计模式,也可以不利用模式就能把设计做得很好。然而,软件设计的目标并不是炫耀茴香豆的“茴”字有几种写法,我们需要的是有效的设计。学习设计模式,不是为了炫耀、吹嘘,不是为了故作艰深,而是为了改善我们的设计。它可以为某种功能实现提供参考模型、设计方法以及应用范例。我们不需要奉GOF的设计模式为圭臬,盲目地膜拜它。合理地运用设计模式,才是明智的抉择。
很多开发人员都读过GOF所著的《设计模式》,对这23种模式也背得滚瓜烂熟。但重要的不是你熟记了多少个模式的名称,关键还在于付诸实践的运用。设计模式并不是西方魔法的神奇咒语,背熟了就能成为大魔法师。一定要理解各种设计模式内含的设计思想与设计准则。为了有效地设计,去熟悉某种模式所花费的代价是值得的;但如果能将它应用在实际的项目开发中,才能够体现设计模式的真正价值。
其实在软件设计人员中,唾弃设计模式的可能很少,盲目夸大设计模式功用的或许更多。言必谈“模式”,并不能使你成为优秀的架构师。真正出色的设计师,懂得判断运用模式的时机。应用设计模式应该使得程序的结构更简单,而不是更复杂。评判复杂度的标准不是类的个数与代码量,而是看功能的实现方式是否能够消除重复编码,合理应对变化。
还有一个问题是,很多初涉软件设计领域的人员,往往对设计模式很困惑。对于他们来说,由于没有项目的实际经验,面向对象的思想也还未曾建立,设计模式未免过于高深了。其实,即使是非常有经验的程序员,也不敢夸口对各种模式都能合理应用。水滴石穿,非一朝一夕之功,不要寄希望于一蹴而就。
虽然,设计模式并非衡量软件开发者水平的一杆标尺,不过,若能掌握设计模式,至少对你的设计能力有所助益。如虎添翼,又何乐而不为呢?
重构是必然的
如前所述,由于客户的需求总是变化的,我们无法给出一个完美的设计方案。摆在面前的问题是如何及时应对这样的改变?明智的做法是,改善现有的程序结构,使得系统能够更好地支持需求的扩展。重构技术,正是“改善既有代码设计”的利器。问题是,这样没有添加任何功能的重构,你是否愿意为此付出精力与时间去完成。当客户要求的Deadline将要到来的时候,你愿意为你的重构工作“买单”吗?
有时候,软件设计常常身不由己。我们常常抱怨项目经理们像老鹰一般盯着自己的一举一动,殊不知经理们其实已经被客户们压迫得鸡飞狗跳了。此时,哪里还谈得上软件系统的重构呢。有时候,软件开发者的命运真如那被蒙了双眼,整日里转圈拉磨的驴子一般悲惨,在项目Deadline的驱策下,不死不休。然而,“文武之道,一张一弛”,一个技术团队如果没有一段时间用以技术的积累与沉淀,迟早会分崩离析。项目在交付之后,并不代表项目已经结束,除了必要的技术支持之外,项目的重构其实更为重要。所谓,“退一步海阔天空”,适时地重构不仅能改善系统的整个架构,也能为今后的设计提供指引。甚至在数度重构之后,因为设计的合理性,会成为今后项目开发的框架平台或者公共类库。
如果从纯技术的角度来看,重构非但必然,而且重要。软件产品一个显著特征是具有锈蚀性,就好比铁制品一般,如果不勤加擦拭,难免会因为需求的修改而变得锈迹斑斑,慢慢腐烂。重构虽然没有改变产品的功能,却能够抛光这些锈迹,使得软件系统避免氧气的腐蚀,降低了引入Bug的可能性;也使得系统架构更加完善,不至于因为新功能的引入而变得臃肿不堪。引入重构技术,完全符合简单设计的原则。此外,对于系统架构师而言,重构技术能够降低糟糕的设计给软件开发带来的风险。架构设计难以一蹴而就,演进的设计证明了这一点。即使采用计划的设计,同样需要在设计过程中对架构完成重构。
Martin Fowler在《重构——改善既有代码的设计》一书中为重构总结了许多条款。这些条款并不是政治课本的教条,也不是“日月神教”的神奇咒语,念着它们就可以刀枪不入、内力大增。条款确实很重要,但你需要的是学会它后,然后忘记它,就像张三丰传授张无忌太极拳那样,需得彻底忘记所学的招数,方能去掉拳招中的棱角,圆转不断如长江之水滔滔不绝。我并不是故弄玄虚,事实上唯有如此,重构的精神才能完全融入到软件的设计中。
UML重要吗
UML(Unified Modeling Language,统一建模语言)主要创始人是Jim Rumbaugh、Ivar Jacobson和Grady Booch,他们在整合了各自的建模方法后,联合创造了一种开放的标准。之后,UML部分地统治了软件世界,在成为软件设计的世界语后,建立了梦想中通天的巴比塔。
在软件设计过程中,几乎很难绕过UML,就好像修建一幢建筑,离不开建筑图纸一样。如果需要详细了解一个软件系统的设计方案时,我更希望先看看UML图,然后再看文档的文字描述。如果让我阅读一段代码,我也希望能先看看类图,或许更容易理解代码的含义。UML是面向对象世界的世界语,它便于程序员间无差别的交流,让其他人更容易理解你的意图。同时,在设计UML图的过程中,也是一种对思路的清理,对客户需求的把握,对设计思想的跟踪。
UML作为一种基于对象的统一建模语言,它能够为系统架构提供清晰直观的设计。在面向对象世界里,UML的地位弥足轻重,甚至被称为是软件设计的一场革命。对于有计划的设计,UML的价值体现得淋漓尽致。如果我们要清晰地表现模块的功能,模块间的关系和系统分布的层次,使用UML可以使设计师减少很多麻烦,同时降低了语义描述的二义性。
演进的设计降低了UML的重要性。按照极限编程的开发原则,我们仅需要对眼前的需求进行编码、测试,然后重构。极限编程往往对规范文档深恶痛绝。事实上,没有及时更新的文档有时候比没有文档给项目带来的负面影响更大,它容易引导开发人员走向错误的道路。UML图作为文档的一部分,同样承担了这样的责任。在敏捷开发过程中,有时候我们可以抛开UML烦琐而死板的设计,毕竟最能忠实体现设计思想的,不是文档,不是用例图或者类图,而是代码。
也许我有些贬低UML的价值,但我始终认为UML其中一个价值就是用于设计思想的交流与理解,即使是在极限编程中,我们也可以在Pair Programming(结队编程)过程中利用UML探讨设计方案。不同的是,这样的UML图可以写入到文档中,也可以画在会议室的白板上,然后被清洁工无意间擦去。
在软件开发方法中,用例驱动开发更能凸现UML的价值。UML的创始人之一Ivar Jacobson提出通过用例(User Case)来说明功能需求,从而驱动用户接口设计、软件设计与测试等开发过程。在软件领域里,很多人持有的一个观点是领域专家也应该明白UML用例,这样在进行需求讨论时,技术专家才能与领域专家有效地沟通。
测试驱动开发
传统的软件开发过程,强调首先进行需求分析,再从需求分析中抽象出概要设计,进而做出详细设计,然后编码,最后才是测试以验证代码的正确性。然而,测试驱动开发(Test Driven Development,TDD)颠覆了整个传统。在测试驱动开发提倡的软件开发过程中,仅仅包括三方面的活动:编写测试用例,编码,最后进行测试。整个过程,我们需要不停地运用重构技术去改善代码的结构,消除重复代码使其更简单、更灵活、更容易理解。
通过测试来驱动开发,听起来是那么的离经叛道,然而实施起来,又是那么合理、正确和简单。测试驱动开发如屹立不倒的基石,基于一个前提:我们不能在一开始就获得正确的设计!确实如此,即使有最资深的领域专家的支持与合作,我们在理解客户的需求时,仍然会存在偏差。利用测试驱动开发,可以避免对不完整需求造成的不成熟的设计。
有人说,测试驱动开发的过程是否过于谨小慎微了?正应了那句老话:“大胆设想,小心求证。”测试驱动开发的精髓,固然令人产生设计者是悲观主义者的遐想,但却是解决设计忧虑的药石良方。如同沙漠里的壁虎一般,既可以根据四周所居的环境变换肤色,也可以在遭遇危险时,挣断尾巴从而求得生存。归根结底,设计之道,乃是应付变化之道。小型的爬行兽虽然没有庞大的身躯与力拔山兮的巨力,但却因为它的灵活与轻便,反而更容易调转方向,不至于走向错误的不归途。
测试驱动开发的主要精神可以概括为“测试先行,快速反馈”。要求在没有任何实现代码的前提下,首先编写测试代码。过程看似荒谬,但却蕴涵一个颠扑不破的真理,即所谓 “顾客就是上帝”。客户无疑就是我们的上帝,所有的设计都围绕着客户的需求而来。需求实则是客户假想的操作体验,如果把客户看作是测试用户的话,这些假想的操作体验,其实就是测试用例。从测试的角度,而非开发的角度出发,就能最大程度体贴客户的需求。由于你一直处于试验的阶段,就会迫使你不断与客户交流,然后再通过反馈得来的知识更新你的设计。又由于你不停地完成测试,从而保障了你的代码经得起客户的考验。
我不认为测试驱动开发能够包治百病,我甚至已经能够想象得到,如果在一个大型的软件系统中,自始至终都运用测试驱动开发,会给整个开发过程带来怎样令人胆战心惊的灾难性后果。于是,事情又回到了我在本章中描述的起点。我将软件设计方式分为计划的设计与演进的设计两种方式,会让人误以为这两种方式会是壁垒分明的两大对立阵营。然而现实情况却是,它们非但不会在战场上成为敌我双方,反而是相辅相成、并行不悖的协作部队。当我们面对一个大型系统的时候,“分而治之”是我们唯一能够选择的解决之道。如何“分之”,正是计划的设计所需要关注的,解构出系统的层次、构成及其之间的关系,将整个系统分为能与外界通信,同时又独立为一体的不同模块。如何“治之”,则可以应用演进的设计,以简单、适用作为设计前提,从不断的演进中创造客户所需。
老子云,道可道,非常道。所谓的道,是一件虚幻至难以言说的东西。不过软件设计之道,却不是故弄玄虚的产物,归根结底,还是要用大量的实践去锤炼。如此,璞玉方能琢而成器,而所谓的设计方法,就好比是炼制过程中加入的适量合金元素,在化学反应之后,锻造为品质优良的精钢。
![](http://www.agiledon.com/image.axd?picture=mybook.jpg)
终于,设计大师们获准进入了那穹顶高悬的殿堂,此时才发现我们又进入了一座迷宫。正是这样,当今的软件设计,不患无标准,而患标准何其之多。在众多设计标准之前,我们都希望找到最好的设计方案,然而什么是最好,每个人都有自己心中的“哈姆雷特”。王子复仇记可以在不同的舞台上被演绎为各种不同的版本,不过对于软件设计而言,我想我可以说:满足客户需求的设计就是最好的标准!然而,前提是怎样通过设计来满足客户需求?
计划的设计与演进的设计
通常来说,软件设计不外乎两种方式:计划的设计和演进的设计。很多人认为,计划的设计更符合工程学的理念。设计的复杂度视项目的规模而定。就好似建造房屋,如果我们仅仅为了容身的一席之地,需要搭建一间茅屋,那么我们只需要准备好土石与茅草,以及夯土用的工具,再请上几个工人,吆喝着就可以开始筑屋的工作了。没有设计图纸,没有详细的规划,也没有建筑大师的匠心独运,茅屋在开工之初就失去了建筑学的普遍意义,它仅仅是随意设计的简陋产物。我们召集工人夯好土墙,然后在屋顶堆放好茅草,梳理几番,就算完工。
![](http://www.agiledon.com/image.axd?picture=house.jpg)
建筑如此,大型项目的软件设计也有类似之处。在项目的准备期间,计划的设计要求我们首要考虑的不是编码,而是整个系统的架构。通过与客户的充分交流,我们整理出项目需求,并根据需求考虑系统中的功能模块以及系统的核心功能。我们需要把握项目开发的进度,并对于项目开发可能需要的工具与技术做出抉择。我们需要遵循统一的设计规范,并对那些可能存在的技术难点进行预研。我们需要确定每一个模块的功能,以及模块间的关系和系统分布的层次。就好比亭台、楼阁在园林中的位置与造型,对于整个系统架构的组成,我们需要匠心独运,高屋建瓴,从一个抽象与宏观的层面来考虑。
世界上最精致优美的建筑风景,并不都是计划设计的产物。例如“集中国水乡之美”的周庄,那些河埠廊坊,过街骑楼以及临河的水阁瓦墙,并不是某位建筑大师呕心沥血的杰作,而是经历了数百年的历史沧桑,渐进地增添与更替各种建筑,最后形成现在这般灵秀的水乡风貌。
演进的设计正是如此,它是一个渐进的过程。演进的设计并不要求前期的设计有多么的完美,实现的需求有多么的完整,我们仅需要把现阶段考虑的问题通过编码实现就可以了。随着演进的深入,对需求的准确理解,编码也会随之而修正,整个设计会逐渐丰满起来,经过一系列的方法,最后渐趋完美。
演进的设计并非毫无章法的设计,它仍然要遵循特定的设计标准与过程控制办法。正如整个周庄的建筑,不论修建的年代,它们都具有浓郁的江南水乡风格。如果我们在周庄的临河边,修建一栋充满了欧式风格的洋房洋楼,即使造型典雅,设计卓越,却会因为破坏整体之美,而成为设计中的败笔。演进的设计,同样需要计划,同样需要架构的设计,它与计划的设计唯一的区别是设计的准则。演进设计的标准是符合客户现有的需求,而计划的设计还需要考虑未来的功能扩展。演进的设计推崇尽快地实现,而计划的设计则考虑架构的完整。二者之间似乎充满了不可调和的矛盾,各执两端。有人会质疑演进设计的简陋与平庸,他们认为没有计划,只会令设计一团糟。传统的隐喻习惯将软件开发比作建筑学,但我需要特别申明的是,二者虽然都是工程学,但软件设计与建筑设计并非完全相同,除了工艺学与开发过程的区别之外,最大的关键是需求变更的频率。软件设计最大的敌人就是需求变更。所以,我们很难在设计之初,考虑到客户的全部需求,甚至于实现未来的扩展。至少我们在设计一开始,就应该不停地拷问自己:
- 我对客户的需求都理解了吗?
- 我能确定客户的需求不再变化吗?
- 我设计的软件架构真的能满足需求吗?
架构设计的标准
![](http://www.agiledon.com/image.axd?picture=bridge.jpg)
(1)程序组织(Program Organization)
系统架构必须划分出整个系统的功能模块(或者是子系统),以及正确描述模块间的关系。在架构设计文档中,这些内容应该属于逻辑视图(Logical View)的范畴。功能模块(子系统)应基于客户需求与功能特征进行合理地划分。对于面向对象技术而言,我们可以对这些功能模块(子系统)以组件或包的形式进行封装。我们需要考虑模块间的解耦,这意味着设计组件或包时需要判断哪些接口可以发布,并采取面向接口的设计方式。我们还可以引入分层的概念,并根据层次定义模块(子系统)的边界。在构建程序组织时,设计师应该与领域专家合作,理解客户需求,从而明确定义各个功能模块(子系统)的责任。从面向对象思想的角度出发,我们希望定义的所有组件或包与其他组件或包之间的依赖尽可能少,如果没有好的程序组织,系统架构会像一张蜘蛛网一般,看似四通八达,但很容易让开发人员迷失在开发进程中。
(2)数据设计(Data Design)
软件系统很难脱离数据而单独存在,因而数据设计会成为制约项目成功的关键。此外,对于软件设计而言,一个好的数据设计还能够提高软件系统的整体性能。数据设计与程序组织也是相关的,很多时候,功能模块的划分应与数据库(数据表)的划分保持一致。除了基于功能需求进行数据设计,我们还必须考虑对象与数据的映射(ORM),数据的查询性能,数据库未来的扩展及数据库的迁移。
(3)安全性(Security)
没有考虑安全性的软件系统,就好似一幢摇摇欲坠的高楼大厦,设计再好,也没人有胆量去居住。安全性并非千篇一律的特征,针对不同的领域,架构师对系统安全的关注程度也不一样,但数据安全是最起码的要求。此外,我们还需要考虑用户的授权、加密与非法攻击的防御。架构设计不仅仅考虑软件层面上的安全,还需要从硬件系统的物理分布、软件的部署环境等多方面衡量系统的安全性。
(4)性能(Performance)
我们不能奢望通过改善硬件系统使得系统性能符合客户的要求,软件设计必须从架构的层面上考虑性能的优化。例如,我们可以考虑优化数据库访问、合理地分层以及引入缓存机制。如果是Web系统,那么在设计时应该通过减少HTTP会话实现降低服务器负载的目的。在考察架构的性能时,需要与客户充分地沟通。我们需要获取与性能相关的数据,从而预先评估架构的性能指标。
(5)可扩展性(Scalability)
可扩展性往往会成为系统架构设计的致命毒药。一方面,如果不考虑系统的可扩展性,会导致未来的维护与更新步履蹒跚。另一方面,如果我们过分地重视系统的可扩展,则会给架构设计制造障碍,甚至导致过度设计。实现系统的可扩展性付出的牺牲,需要设计思想与设计方法来弥补,例如面向对象思想。我们还可以引入面向方面编程(AOP,Aspect- Oriented Programming)技术,或者利用Web Service实现面向服务架构(Service-Oriented Architecture)。
(6)可靠性(Reliability)
可靠性包括容错性和错误处理。必须考虑系统可能出现的错误和异常,以及出现错误或抛出异常后,应该如何处理?如果可能的话,我们应该考虑从错误中恢复,即使不能恢复,也要保证系统的运行不受影响。此外,统一的异常处理机制是必需的,特别是对于框架而言,有利于框架用户对框架API的调用。
(7)可用性(Usability)
例如用户界面(User Interface)的设计,系统要求界面的风格以及用户的操作必须统一,同时还应该考虑用户的体验,以及保证操作流程符合业务规则。有时候界面设计能够得到用户的认可,就意味着项目已经成功了一半。虽然,我们反对“金玉其外,败絮其中”,然而“酒好不怕巷子深”的传统营销理念,已经不符合产品交付的标准。归根结底,我们开发的产品是交付给客户使用的。
优秀的架构设计并不易得,正如优秀的架构师千金难求。但如果我们能够按照以上的要点来衡量架构的设计,或许能够尽可能早地发现架构的缺陷。不要等到考试结束交卷之后,方才因为发现错误无法修改而追悔莫及。
过度设计,还是简单设计
Kent Beck在《解析极限编程——拥抱变化》中为简单系统制定了4个评价标准,依次为(最重要的排在最前面):
- 通过所有测试;
- 体现所有意图;
- 避免重复;
- 类或者方法数量最少。
说易行难。知道这些标准并不等于说我们已经遵循了这些标准。开发人员并不是喜欢故弄玄虚的空谈家,实干家的精神使得他们视复杂为猛虎,却同时明白追求简单往往意味着设计力度上的百倍付出。在设计时,我们不仅要考虑软件的功能,还要考虑软件的性能、可扩展性,模块间的耦合关系,系统的稳定、部署和更新,版本的管理,系统的安全,界面的友好程度。要想实现简单系统,可谓是蜀道之难,难于上青天!
Do the simplest thing that could possibly work! 这是XP(Extreme Programming,极限编程)人士大声疾呼的口号。还有一个响亮的口号是“You aren't going to need it”。这其中包含的核心意义就是不要为了考虑程序的可扩展性,把目前不需要的功能加入到软件中来。极限编程提倡开发人员不要“杞人忧天”,既然未来的天是否会塌陷还有待考证,不如脚踏实地关注目前需要做的事情,至少在态度上更为务实。虽然,这种做法颇有几分“目光短浅”的嫌疑,然而软件开发人员毕竟不是预言家,未来的功能扩展无法估计,与其花费精力与时间去考虑整个架构的可扩展性,倒不如集中精神对付眼前的需求。我们也许会承担未来因为扩展而承担的巨大代价,但还有一种可能是,在付出了巨大代价解决了系统可扩展性后,又因为系统的变化脱离了预期的轨道,从而导致此前的心血付诸东流。
所以,演进的设计更符合极限编程的核心准则。然而简单并非意味着简易,所谓“大道至简”,自简入繁并不难,化繁为简才真正需要大智慧。我们必须理解的一点就是,所谓的“简单系统”同时还应该保证它的有效性。举例来说,如果我们要开发一个数据库管理系统,按照当前的需求,由于数据库访问并不频繁,我们可以采用按需创建数据库连接的方式,一旦结束访问就关闭连接。但随着该系统使用人数的增多,数据库的频繁访问会使得原有的数据库连接策略成为系统的性能瓶颈,此时,我们就需要修改系统,将原有的数据库连接策略改为连接池方式,专门负责连接资源的分配与释放。
如果我们从一开始就考虑引入连接池,对于之前的简单需求而言,是否意味着过度设计呢?按需创建数据库连接的方式确实简单,但它对于客户的需求而言,是否又是最有效的解决方案?在此刻,或许被指责为过度设计的方案,在彼刻或许又被推许为最完美的方案,那么我们应该按何种标准来评判解决方案是过度设计,还是简单设计?
事实上,问题并不在于设计是否过度,而在于设计的理念。是只做目前需要的事,还是未雨绸缪,想好今后的功能扩展?这个问题的答案还需要实际的项目开发来检验,根据不同的需求,答案会因此而异。最流行的未必就是最好的,极限编程自有它的适用场景,迭代开发模式在特定的场景下也会成为最佳的软件过程方法。
需要设计模式吗
如果我们仅考虑实现当前的功能需求,我们还需要设计模式吗?坦白说,我并不认为设计模式与过度设计有关。过度设计的导火索是设计模式的滥用。很多时候,合理地利用设计模式反而能使我们的程序结构简单化,特别是,它能够让我们的开发过程更简单。
Christopher Alexander在描述城市和建筑模式时说,“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。”设计模式正是为解决软件开发领域中的类似问题,从而提出的解决方案,它作为面向对象编程经验的总结,是软件设计技巧中最重要的方法与原则。放弃设计模式,意味着希望成为优秀设计师的你,在将历史的车轮向前倒滚了十年之后,有幸成为了和GOF(Gang of Four,即设计模式的提出者或者集大成者Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides)同行的伟大先驱者。你放弃了站在前人肩膀上眺望远方的机会,而选择自己艰难吃力地踮起脚尖,最后失去的却是更远处美丽的风景。
需要设计模式吗?答案看来是不言而喻。关键一点是我们需要确定模式的应用是否过度?世界上很多天才横溢的程序员,可以在一段代码中包含6种设计模式,也可以不利用模式就能把设计做得很好。然而,软件设计的目标并不是炫耀茴香豆的“茴”字有几种写法,我们需要的是有效的设计。学习设计模式,不是为了炫耀、吹嘘,不是为了故作艰深,而是为了改善我们的设计。它可以为某种功能实现提供参考模型、设计方法以及应用范例。我们不需要奉GOF的设计模式为圭臬,盲目地膜拜它。合理地运用设计模式,才是明智的抉择。
很多开发人员都读过GOF所著的《设计模式》,对这23种模式也背得滚瓜烂熟。但重要的不是你熟记了多少个模式的名称,关键还在于付诸实践的运用。设计模式并不是西方魔法的神奇咒语,背熟了就能成为大魔法师。一定要理解各种设计模式内含的设计思想与设计准则。为了有效地设计,去熟悉某种模式所花费的代价是值得的;但如果能将它应用在实际的项目开发中,才能够体现设计模式的真正价值。
其实在软件设计人员中,唾弃设计模式的可能很少,盲目夸大设计模式功用的或许更多。言必谈“模式”,并不能使你成为优秀的架构师。真正出色的设计师,懂得判断运用模式的时机。应用设计模式应该使得程序的结构更简单,而不是更复杂。评判复杂度的标准不是类的个数与代码量,而是看功能的实现方式是否能够消除重复编码,合理应对变化。
还有一个问题是,很多初涉软件设计领域的人员,往往对设计模式很困惑。对于他们来说,由于没有项目的实际经验,面向对象的思想也还未曾建立,设计模式未免过于高深了。其实,即使是非常有经验的程序员,也不敢夸口对各种模式都能合理应用。水滴石穿,非一朝一夕之功,不要寄希望于一蹴而就。
虽然,设计模式并非衡量软件开发者水平的一杆标尺,不过,若能掌握设计模式,至少对你的设计能力有所助益。如虎添翼,又何乐而不为呢?
重构是必然的
如前所述,由于客户的需求总是变化的,我们无法给出一个完美的设计方案。摆在面前的问题是如何及时应对这样的改变?明智的做法是,改善现有的程序结构,使得系统能够更好地支持需求的扩展。重构技术,正是“改善既有代码设计”的利器。问题是,这样没有添加任何功能的重构,你是否愿意为此付出精力与时间去完成。当客户要求的Deadline将要到来的时候,你愿意为你的重构工作“买单”吗?
有时候,软件设计常常身不由己。我们常常抱怨项目经理们像老鹰一般盯着自己的一举一动,殊不知经理们其实已经被客户们压迫得鸡飞狗跳了。此时,哪里还谈得上软件系统的重构呢。有时候,软件开发者的命运真如那被蒙了双眼,整日里转圈拉磨的驴子一般悲惨,在项目Deadline的驱策下,不死不休。然而,“文武之道,一张一弛”,一个技术团队如果没有一段时间用以技术的积累与沉淀,迟早会分崩离析。项目在交付之后,并不代表项目已经结束,除了必要的技术支持之外,项目的重构其实更为重要。所谓,“退一步海阔天空”,适时地重构不仅能改善系统的整个架构,也能为今后的设计提供指引。甚至在数度重构之后,因为设计的合理性,会成为今后项目开发的框架平台或者公共类库。
如果从纯技术的角度来看,重构非但必然,而且重要。软件产品一个显著特征是具有锈蚀性,就好比铁制品一般,如果不勤加擦拭,难免会因为需求的修改而变得锈迹斑斑,慢慢腐烂。重构虽然没有改变产品的功能,却能够抛光这些锈迹,使得软件系统避免氧气的腐蚀,降低了引入Bug的可能性;也使得系统架构更加完善,不至于因为新功能的引入而变得臃肿不堪。引入重构技术,完全符合简单设计的原则。此外,对于系统架构师而言,重构技术能够降低糟糕的设计给软件开发带来的风险。架构设计难以一蹴而就,演进的设计证明了这一点。即使采用计划的设计,同样需要在设计过程中对架构完成重构。
Martin Fowler在《重构——改善既有代码的设计》一书中为重构总结了许多条款。这些条款并不是政治课本的教条,也不是“日月神教”的神奇咒语,念着它们就可以刀枪不入、内力大增。条款确实很重要,但你需要的是学会它后,然后忘记它,就像张三丰传授张无忌太极拳那样,需得彻底忘记所学的招数,方能去掉拳招中的棱角,圆转不断如长江之水滔滔不绝。我并不是故弄玄虚,事实上唯有如此,重构的精神才能完全融入到软件的设计中。
UML重要吗
UML(Unified Modeling Language,统一建模语言)主要创始人是Jim Rumbaugh、Ivar Jacobson和Grady Booch,他们在整合了各自的建模方法后,联合创造了一种开放的标准。之后,UML部分地统治了软件世界,在成为软件设计的世界语后,建立了梦想中通天的巴比塔。
在软件设计过程中,几乎很难绕过UML,就好像修建一幢建筑,离不开建筑图纸一样。如果需要详细了解一个软件系统的设计方案时,我更希望先看看UML图,然后再看文档的文字描述。如果让我阅读一段代码,我也希望能先看看类图,或许更容易理解代码的含义。UML是面向对象世界的世界语,它便于程序员间无差别的交流,让其他人更容易理解你的意图。同时,在设计UML图的过程中,也是一种对思路的清理,对客户需求的把握,对设计思想的跟踪。
UML作为一种基于对象的统一建模语言,它能够为系统架构提供清晰直观的设计。在面向对象世界里,UML的地位弥足轻重,甚至被称为是软件设计的一场革命。对于有计划的设计,UML的价值体现得淋漓尽致。如果我们要清晰地表现模块的功能,模块间的关系和系统分布的层次,使用UML可以使设计师减少很多麻烦,同时降低了语义描述的二义性。
演进的设计降低了UML的重要性。按照极限编程的开发原则,我们仅需要对眼前的需求进行编码、测试,然后重构。极限编程往往对规范文档深恶痛绝。事实上,没有及时更新的文档有时候比没有文档给项目带来的负面影响更大,它容易引导开发人员走向错误的道路。UML图作为文档的一部分,同样承担了这样的责任。在敏捷开发过程中,有时候我们可以抛开UML烦琐而死板的设计,毕竟最能忠实体现设计思想的,不是文档,不是用例图或者类图,而是代码。
也许我有些贬低UML的价值,但我始终认为UML其中一个价值就是用于设计思想的交流与理解,即使是在极限编程中,我们也可以在Pair Programming(结队编程)过程中利用UML探讨设计方案。不同的是,这样的UML图可以写入到文档中,也可以画在会议室的白板上,然后被清洁工无意间擦去。
在软件开发方法中,用例驱动开发更能凸现UML的价值。UML的创始人之一Ivar Jacobson提出通过用例(User Case)来说明功能需求,从而驱动用户接口设计、软件设计与测试等开发过程。在软件领域里,很多人持有的一个观点是领域专家也应该明白UML用例,这样在进行需求讨论时,技术专家才能与领域专家有效地沟通。
测试驱动开发
传统的软件开发过程,强调首先进行需求分析,再从需求分析中抽象出概要设计,进而做出详细设计,然后编码,最后才是测试以验证代码的正确性。然而,测试驱动开发(Test Driven Development,TDD)颠覆了整个传统。在测试驱动开发提倡的软件开发过程中,仅仅包括三方面的活动:编写测试用例,编码,最后进行测试。整个过程,我们需要不停地运用重构技术去改善代码的结构,消除重复代码使其更简单、更灵活、更容易理解。
通过测试来驱动开发,听起来是那么的离经叛道,然而实施起来,又是那么合理、正确和简单。测试驱动开发如屹立不倒的基石,基于一个前提:我们不能在一开始就获得正确的设计!确实如此,即使有最资深的领域专家的支持与合作,我们在理解客户的需求时,仍然会存在偏差。利用测试驱动开发,可以避免对不完整需求造成的不成熟的设计。
有人说,测试驱动开发的过程是否过于谨小慎微了?正应了那句老话:“大胆设想,小心求证。”测试驱动开发的精髓,固然令人产生设计者是悲观主义者的遐想,但却是解决设计忧虑的药石良方。如同沙漠里的壁虎一般,既可以根据四周所居的环境变换肤色,也可以在遭遇危险时,挣断尾巴从而求得生存。归根结底,设计之道,乃是应付变化之道。小型的爬行兽虽然没有庞大的身躯与力拔山兮的巨力,但却因为它的灵活与轻便,反而更容易调转方向,不至于走向错误的不归途。
测试驱动开发的主要精神可以概括为“测试先行,快速反馈”。要求在没有任何实现代码的前提下,首先编写测试代码。过程看似荒谬,但却蕴涵一个颠扑不破的真理,即所谓 “顾客就是上帝”。客户无疑就是我们的上帝,所有的设计都围绕着客户的需求而来。需求实则是客户假想的操作体验,如果把客户看作是测试用户的话,这些假想的操作体验,其实就是测试用例。从测试的角度,而非开发的角度出发,就能最大程度体贴客户的需求。由于你一直处于试验的阶段,就会迫使你不断与客户交流,然后再通过反馈得来的知识更新你的设计。又由于你不停地完成测试,从而保障了你的代码经得起客户的考验。
我不认为测试驱动开发能够包治百病,我甚至已经能够想象得到,如果在一个大型的软件系统中,自始至终都运用测试驱动开发,会给整个开发过程带来怎样令人胆战心惊的灾难性后果。于是,事情又回到了我在本章中描述的起点。我将软件设计方式分为计划的设计与演进的设计两种方式,会让人误以为这两种方式会是壁垒分明的两大对立阵营。然而现实情况却是,它们非但不会在战场上成为敌我双方,反而是相辅相成、并行不悖的协作部队。当我们面对一个大型系统的时候,“分而治之”是我们唯一能够选择的解决之道。如何“分之”,正是计划的设计所需要关注的,解构出系统的层次、构成及其之间的关系,将整个系统分为能与外界通信,同时又独立为一体的不同模块。如何“治之”,则可以应用演进的设计,以简单、适用作为设计前提,从不断的演进中创造客户所需。
老子云,道可道,非常道。所谓的道,是一件虚幻至难以言说的东西。不过软件设计之道,却不是故弄玄虚的产物,归根结底,还是要用大量的实践去锤炼。如此,璞玉方能琢而成器,而所谓的设计方法,就好比是炼制过程中加入的适量合金元素,在化学反应之后,锻造为品质优良的精钢。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/13770856/viewspace-231030/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/13770856/viewspace-231030/