前面几节关注的都是与设计特性相关的启发式方法——你希望完成后的整体设计成为什么样子。这一节将会讲解一些设计实践的启发式方法,一些你可能采用而且常常可以获得良好结果的工作步骤。
Iterate
迭代
你可能有过这样的经验:有时候你会从开发某个程序中学到很多的知识,多得让你想带着写第一遍时所获得的体会再写一遍。这种现象也同样出现在设计活动中,只是设计的周期更短,向下进行的压力更大,因此你只能负担不多的几次设计循环。
设计是一种迭代过程。你并非只能从A点进行到B点,而是可以从A点到达B点,再从B点返回到A点。
当你在备选的设计方案之中循环并且尝试一些不同的做法时,你将同时从高层和低层的不同视角去审视问题。你从高层视角中得出的大范围图景会有助于你把相关的底层细节纳入考虑。你从底层视角中所获得的细节也会为你的高层决策奠定基础。这种高低层面之间的互动被认为是一种良性的原动力,它所创建的结构要远远稳定于单纯自上而下或者自下而上创建的结构。
很多程序员——或者说很多人——对在高层和低层思考之间的升降感到困惑。从系统的一个视角转到另一个视角,从智力上来说是很费力的,但对于创建有效的设计方案而言却是极其重要的。如果你想获得愉悦的、提高智力灵活性的练习,请阅读本章后面“更多资源”一节里提到的《Conceptual
Blockbusting》一书(Adams 2001)。
当你首次尝试得出了一个看上去足够好的设计方案后,请不要停下来!第二个尝试几乎肯定会好于第一个,而你也会从每次尝试中都有所收获,这有助于改善整体设计。在经历了1 000次研究灯丝材料的失败以后,有人问爱迪生,因为他什么也没有发现,会不会感到时间全都浪费了。“当然不会”,爱迪生大概是这样回答的,“因为我已经发现了1 000种材料是不能用的。”在大多数情况下,用一种方法来解决问题会为你带来一些新的洞察力,从而帮助你使用另一种更好的方法来解决问题。
Divide and Conquer
分而治之
正如Edsger Dijkstra所说,没有人的头脑能大到装得下一个复杂程序的全部细节,这对设计也同样有效。把程序分解为不同的关注区域,然后分别处理每一个区域。如果你在某个区域里碰上了死胡同,那么就迭代!
增量式地改进是一种管理复杂度的强大工具。正如Polya在数学问题求解中所建议的那样——理解问题、形成计划、执行计划,而后再回顾你的做法(Polya 1957)。
Top-Down and Bottom-Up Design Approaches
自上而下和自下而上的设计方法
“自上而下”和“自下而上”听起来有点陈旧,不过它们确实为创建面向对象设计方案提供了很有价值的认识。自上而下的设计从某个很高的抽象层次开始。你定义出基类或其他不那么特殊的设计元素。在开发这一设计的过程中,你逐渐增加细节的层次,找出派生类、合作类以及其他更细节的设计元素。
自下而上的设计始于细节,向一般性延伸。这种设计通常是从寻找具体对象开始,最后从细节之中生成对象以及基类。
有些人坚持认为从一般性出发向具体延伸是最佳做法,而有些人则表示除非你已经解决了大量的细节问题,否则无法真正地定义出一些一般性的设计原则。下面是这两种观点各自的论据。
Argument for Top Down
自上而下的论据
居于自上而下方法背后的指导原则是这样的一种观点:人的大脑在同一时间只能集中关注一定量的细节。如果你从一般的类出发,一步步地把它们分解成为更具体的类,你的大脑就不会被迫同时处理过多的细节。
这种分而治之的过程从某些意义上来说也是迭代的。首先,说它是迭代的,因为你通常不会在完成一层分解之后停下来。你还会继续分解几层。其次,说它是迭代的,还因为你通常不会满足于你的第一次尝试。你用一种方法来分解程序。在分解过程的不同阶段,你需要就采用什么方法去分解子系统做出选择,给出继承关系树,形成对象的组合。你做出选择然后看看是什么结果。接下来,你又换用另一种方法重新开始分解,以便看新方法是否效果更佳。这样尝试几次以后,你就会很清楚哪些方法会奏效,以及它们能够奏效的原因。
如何确定分解的程度呢?持续分解,直到看起来在下一层直接编码要比分解更容易。一直做到设计思路已显而易见而且非常容易,以致你对继续分解下去已经产生了厌倦。那时候你就完成了分解工作。如果设计思路还不明了,那么请再多做些。如果目前的解决方案对你来说都有些棘手,那么当日后他人再去面对的时候也肯定会感到负担重重。
Argument for Bottom Up
自下而上的论据
有时候自上而下的方法会显得过于抽象,很难入手去做。如果你倾向于用一种更实在的方法,那么可以尝试自下而上的方法。问你自己,“我对这个系统该做的事情知道些什么?”毫无疑问,你可以回答这个问题。你可能会找出一些能够分配给具体类的低层的职责。例如,你知道一个系统需要对报表进行格式化,为报表计算数据,把标题居中,在屏幕上显示报表,以及打印该报表等等。一旦你找出了一些低层的职责,你通常会感到,再从顶上去观察系统已经舒服些了。 软件开发网
www.mscto.com
在另一些情况中,设计问题里的一些主要属性是由底层决定的。你可能需要去与硬件设备打交道,它们的接口需求决定了你的设计里很大的一部分。
下面是在做自下而上合成的时候你需要考虑的一些因素:
■ 问你自己,对系统需要做的事项,你知道些什么。
■ 根据上面的问题,找出具体的对象和职责。
■ 找出通用的对象(common objects),把它们按照适当方式组织起来——子系统、包、对象组合,或者继承——看哪种方式合适。
■ 在更上面一层继续工作,或者回到最上层尝试向下设计。
No Argument, Really
其实并没有争议
自上而下策略和自下而上策略的最关键区别在于,前者是一种分解(decomposition)策略而后者是一种合成(composition)策略。前者从一般性的问题出发,把该问题分解成可控的部分。后者从可控的部分出发,去构造一个通用的方案。这两种方法都有各自的强项和弱项,如果你想在设计中采用它们的时候,就需要予以考虑。
自上而下设计的强项是它很简单,因为人们是很善于把一些大的事物分解为小的组件,而程序员则更是精于此道。
自上而下设计的另一强项是你可以推迟构建的细节。软件系统常常会受到构建细节变化(例如文件结构或者报表格式的变化)的骚扰,因此,尽早知道应该把这些细节信息隐藏在继承体系的底层类中,是非常有益的。
自下而上设计的一个强项是通常能够较早找出所需的功能,从而带来紧凑的、结构合理的设计。如果类似的系统已经做过,那么自下而上设计让你能审视已有的系统,并提出“我能重用些什么?”一类的问题,由此出发开始新系统的设计。
自下而上设计的一个弱项是很难完全独立地使用它。大多数人都很善于把大概念分解为小概念,而不擅长从小概念中得出大概念。这就像在自己组装玩具:我想自己已经组装完了,可为什么盒子里还有零件呢?所幸的是,完全不必仅使用自下向上这一种设计方法。
自下而上设计的另一弱项是,有时候你发现自己无法使用手头已有的零件来构造整个系统。你不可能用砖块来建造飞机。你可能要先做高层设计,才能知道底层需要什么零件。
总而言之,自上而下设计通常比较容易上手,但是有时候会受底层复杂度的影响,这种影响甚至有时会使事情变得比实际的情况更复杂。自下而上设计开始起来比较复杂,但是在早期鉴别出系统的复杂度,却有助于设计出更好的高层类。当然这样做的前提是复杂度没有先把整个系统破坏掉!
最后要说的是,自上而下和自下而上设计并不是互相排斥的——你会受益于二者的相互协作。设计是一个启发式(试探)的过程,这意味着没有任何解决方案能够保证万无一失。设计过程中充满了反复的试验,请多尝试些设计方法,直到找到最佳的一种。
Experimental Prototyping
建立试验性原型
有些时候,除非你更好地了解了一些实现细节,否则很难判断一种设计方法是否奏效。比如说,在知道它能满足性能要求之前,你很难判断某种数据库的组织结构是否适用。在选定系统使用的图形界面(GUI)程序库之前,你也很可能判断不出某一特定子系统的设计是否到位。这些都是软件设计中本质性“险恶(wickedness)”的例子——除非你部分地解决了某一设计问题,否则你无法完整地定义出该设计问题。
有一种技术能够低成本地解决这个问题,那就是建立试验性原型。“建立原型(prototyping)”一词对不同人来说具有不同的含义(McConnell 1996)。在这里,建立原型指的是“写出用于回答特定设计问题的、量最少且能够随时扔掉的代码”这项活动。
如果开发人员没有把握住用最少代码回答提问的原则,那么原型方法的功效就会大打折扣。假设说,设计问题是“我们选定的数据库框架能否支撑所需的交易量?”你不需要为了这一问题而编写任何产品代码,你也不需要去了解数据库的详情。你只需要了解能估计出问题范围的最少信息——有多少张表、表中有多少条记录,等等。接下来你就可以用Table1、Table2、Column1、Column2等名字写出最简单的原型代码,往表里随意填入些数据,然后做你所需要的性能测试。
当有关设计的问题不够特殊的时候,原型同样也会失效。诸如“这样的数据库框架能否工作?”的设计问题并没有为建立原型提供多少指引。而像“这个数据库框架能不能在X、Y和Z的前提下支持每秒1000次交易?”这样的问题则能为建立原型提供更坚实的基础。
最后的一个可能会给原型带来风险的做法是,开发人员不把原型代码当作可抛弃的代码。我发现,如果开发人员相信某段代码将被用在最终产品里,那么他根本不可能写出最少数量的代码来。这样做最终其实是在实现整个系统,而不是在开发原型。相反,如果你建立了这样的概念,那就是一旦回答了所提出的问题,这段代码就可以被扔掉,那么你就可以把上述风险减到最小。避免产生这一问题的一种做法是用与产品代码不同的技术来开发原型。你可以用Python来为Java设计做原型,或者用Microsoft PowerPoint来模拟用户界面。如果你必须要用同一种技术来开发原型,那么可以采纳一个非常实用的标准:在原型中的类或者包的名称之前加上prototype前缀。这样至少能保证程序员在试图拓展原型代码之前能够三思(Stephens 2003)。
一旦依照上述原则加以应用,那么原型就会成为设计者手中用来处理险恶设计问题的有力工具。如果不遵照上述原则,那么原型就会给设计再平添许多风险。
Collaborative Design
合作设计
在设计过程中,三个臭皮匠顶得上一个诸葛亮,而不论组织形式的正式与否。合作可以以下面任意一种方式展开。
■ 你随便走到一名同事的办公桌前,向他征求一些想法。
■ 你和同事坐在会议室里,在白板上画出可选的设计方案。
■ 你和同事坐在键盘前面,用你们的编程语言做出详细的设计,换句话说,你们可以采用结对编程,第21章“协同构建”中对结对编程有描述。
■ 你约一名或者多名同事来开会,和他们过一遍你的设计想法。
■ 你按照第21章中给出的结构来安排一次正式检查(formal inspection)。
■ 你身边没有人能检查你的工作,因此当你做完一些初始工作后,把它们全放进抽屉,一星期后再来回顾。这时你会把该忘的都忘了,正好可以给自己做一次不错的检查。
■ 你向公司以外的人求助:在某个特定的论坛或者新闻组里提问。
如果这样做的目标是保证质量,那么我倾向于推荐高度结构化的检查实践——正式检察,其原因在第21章中解释。但是如果目标是提高创造力并且引入更多的备选设计方案,而不仅仅是找到缺陷的话,那么结构化程度较低的一些方法则比较适宜。在你确定选用了某一特定设计方案之后,转而使用一种更为正规的检查方式非常可取,但是这也要取决于你的项目本身的情况。
How Much Design Is Enough
要做多少设计才够
有些时候,编码之前只制订出系统架构的一个最小梗概。而在另一些时候,开发团队会把设计做得非常详细,使编码变成了一种近乎机械式的工作。那么在编码前到底需要做多少设计呢?
有一个问题与此相关,那就是上述设计应该做得有多正规。你需要正式的、精美的设计图表,还是只需要给画在白板上的设计草图排个数码照片作为设计文档呢?
对于实施正式编码阶段前的设计工作量和设计文档的正规程度,很难有个准确的定论。有很多因素,如团队的经验、系统的预定寿命、想要得到的可靠度、项目的规模和团队的大小等等都需要考虑进去。表5-2总结了这些因素是如何影响设计活动的。
表5-2 设计文档的正规化以及所需的细节层次
因素 |
开始构建之前的设计所需的细化程度 |
文档正规程度 |
设计或构建团队在应用程序领域 |
低 |
低 |
设计或构建团队有很丰富的经验, |
中 |
中 |
设计或构建团队缺乏经验 |
中到高 |
低到中 |
设计或构建团队人员变动适中或者较高 |
中 |
— |
应用程序是安全攸关的 |
高 |
高 |
应用程序是使命攸关的 |
中 |
中到高 |
项目是小型的 |
低 |
低 |
项目是大型的 |
中 |
中 |
软件预期的生命周期很短 |
低 |
低 |
软件预期的生命周期很长 |
中 |
中 |
对于一个特定的项目来说,这其中的两个或者更多因素可能起着关键作用。在某些情况下,从这些因素也可能得出有矛盾的建议。比如说,你带一支经验非常丰富的团队开发对安全性要求很高的软件。这时,你可能宁愿采用更高的设计细化程序以及更正规的设计文档。也就是说,在这种情况下,你需要去评估每一项因素的重要性,然后根据结果来做出重要性的权衡。
如果设计层次的问题是留给程序员个人去解决的话,那么,当设计下降到你曾经完成过的某项任务的层次,或者变成了对这样一项任务的简单修改或扩充的时候,你很可能就会停止设计而马上开始编码。
如果在编码之前我还判断不了应该在做多深入的设计,那么我宁愿去做更详细的设计。最大的设计失误来自于我误认为自己已经做得很充分,可事后却发现还是做得不够,没能发现其他一些设计挑战。换句话说,最大的设计问题通常不是来自于那些我认为是很困难的,并且在其中做出了不好的设计的区域;而是来自于那些我认为是很简单的,而没有做出任何设计的区域。我几乎没有遇到过因为做了太多设计而受损害的项目。
另一方面,我偶尔会看到一些项目因太过于专注对设计进行文档化而导致失败。Gresham法则是这么说的,“程序化的活动容易把非程序化的活动驱逐出去”(Simon 1965)。过早地去润色设计方案就是这一法则所描述的例子。我宁愿看到有80%的设计精力用于创建和探索大量的备选设计方案,而20%的精力用于创建并不是很精美的文档,也不愿看到把20%的精力花在创建平庸的设计方案上,而把80%的精力用于对不良的设计进行抛光润色。
Capturing Your Design Work
记录你的设计成果
传统的记录设计成果的方式是把它写成正式的设计文档。然而,你还可以用很多种方法来记录这一成果,而这些方法对于那些小型的、非正式的项目或者只需要轻量级的记录设计成果的方式的项目而言效果都很不错。
把设计文档插入到代码里 在代码注释中写明关键的设计决策,这种注释通常放在文件或者类的开始位置。如果你同时使用类似于JavaDoc这样的文档提取工具,那么这种方法会确保设计文档对于开发这部分代码的程序员来说是立等可取的,同时也有助于程序员保持代码和设计文档之间的相当不错的同步。
用Wiki来记录设计讨论和决策 把你们的设计讨论写到项目的Wiki里去(Wiki是指一组可以由项目组所有成员用网络浏览器轻松编辑的网页)。尽管文字录入要比交谈麻烦一些,但这样会自动地记录下你们的设计讨论和设计决策。如果使用Wiki,你可以用图片来弥补文字讨论的不足,并链接支持该设计决策的网站、白皮书及其他材料。如果你的开发团队在地理位置上是分布式的,那么这种技术会非常有帮助。