设计实践
1.迭代
打你你在备选的设计方案之中循环并且尝试一些不同的做法时,你将同时从高层和低层的不同视角去审视问题。你从高层视角中得到的大范围途径会有助于你把相关的低层细节纳入考虑。你从低层视角中所获得的细节也会为你的高层决策奠定基础。这种高低层面之间的互动被认为是一种良性的原动力,它所创建的结构要远远稳定与单纯自上而下或自下而上创建的结构。
当你首次尝试得出了一个看上去足够好的设计方案后,请不要停下来!第二个尝试几乎肯定会好于第一个,而你也会从每次的尝试中都有所收获,这有助于改善整体设计。
2.分而治之
没有人的头脑能够装得下一个复杂程序的全部细节,这对设计也同样有效。把程序分解为不同的关注区域,然后分别处理每一个区域。如果你在某个区域里碰到了死胡同,那么就跌代。
增量式地改进是一种管理复杂度的强大工具。正如 Polya 在数学问题求解中所建议的那样——理解问题、形成计划、执行计划,然后再回顾你的做法。
3. 自上而下和自下而上的设计方法
自上而下的设计从某个很高的抽象层次开始。你定义出基类或者其他不怎么特殊的设计元素。在开发这一设计的过程中,你逐渐增加细节的层次,找出派生类、合作类以及其他更细节的设计元素。自下而上的设计始于细节,向一般延伸。这种设计通常是从寻找具体对象开始,最后从细节之中生成对象以及基类。
-
自上而下的论据
居于自上而下方法背后的知道原则是这样一种观点:人的大脑在同一时间只能集中关注一定量的细节。如果你从一般的类出发,一步步地把他们分解成为更具体的类,你的大脑就不会被迫同时处理过多的细节。
这种分而治之的过程在某些意义上来说也是迭代的。首先,说它是迭代的,因为你通常不会再完成一层分解之后停下来。你还会继续分解几层。其次,说它是迭代的,是因为你通常不会满足于你的第一次尝试。你用一种方法来分解程序。在分解过程中的不同阶段,你需要就采用什么方法去分解子系统做出选择,给出继承关系树,形成对象的组合。你做出选择,然后看看是什么结果。接下来,你又换用另一种方法重新开始分解,以便看清方法是否效果更佳。这种尝试几次之后,你就会很清楚哪些方法会奏效,以及他们能够奏效的原因。
如何确定分解的程度呢?持续分解,直到看起来在下一层直接编码要比分解更容易。一直做到设计思路已显而易见而且非常容易,以致 你对继续分解下去已近产生了厌倦。那时候你就完成了分解工作。如果设计思路还不明了,那么请再多做些。如果目前的解决方案对你来说都有些棘手,那么日后他人再去面对的时候也肯定会感到负担重重。
-
自下而上的论据
有时候自上而下的方法会显得过于抽象,很难入手去做。如果你倾向于用一种更实在的方法, 那么可以尝试自下而上的方法。问你自己,“我对这个系统该做的事情知道些什么?”毫无疑问,你可以回答这个问题。你可能会找出一些能够分配给具体类的底层的职责。例如,你知道一个系统需要对报表进行格式化,为报表计算数据,把标题居中,在屏幕上显示报表,以及打印该报表等等。一旦你找出了一些底层的职责,你通常会感到,再从顶层去观察系统已经舒服些了。
在另一些情况中,设计问题里的一些主要属性是由底层决定的。你可能需要去与硬件设备打交道,他们的接口需求决定了你的设计里很大一部分。
下面是在做自下而上合成的时候你需要考虑的一些因素:
(1)问你自己,对系统需要做的事项,你知道些什么。
(2)根据上面的问题,找出具体的对象和职责。
(3)找出通用的对象,把它们按照适当方式组织起来——子系统、包、对象组合,或者集成——看哪种方式适合。
(4)在更上面一层继续工作,或者回到最上层尝试向下设计。 -
其实并没有争议
自上而下的策略和自下而上的策略的最关键区别在于,前面是一种分解策略而后者是一种合成策略。前者从一般性的问题出发,把该问题分解成可控的部分。后者从可控的部分出发,去构造一个通用的方案。这两种方法都有各自的强项和弱项,如果你想在设计中采用它们的时候,就需要予以考虑。
自上而下设计的强项是它很简单,因为人们很善于把一些大的事情分解成为小的组件,而程序员则更精于此道。
自上而下设计的另一个强项是你可以推迟构建的细节。软件系统常常会受到构建细节变化(例如文件结果或者报表格式发生的变化)的骚扰,因此,尽早知道应该把这些细节隐藏在 继承体系的底层类中,是非常有益的。
自下而上设计的一个强项是通常能够较早找出所需的功能,从而带来紧凑的、结构合理的设计。如果类似的系统已经做过,那么自下而上的设计让你能审视已有的系统,并 提出“我能重用些什么?”一类的问题,由此出发开始新系统的设计。
自下而上设计的一个弱项是很难完成独立的使用它。大多数人都很善于把大概念分解为小概念,而不擅长从小概念中得出大概念。这就像在自己组装玩具:我想自己已经组装完成了,可为什么盒子里还有零件呢?所幸的是,完全不必仅适用自下而上这一种设计方法。
自下而上设计的另一弱项是,有时候你发现自己无法适用手头已有的零件来构造整个系统。你不可能用砖块来建造飞机。你可能要先做高层设计,才能知道底层需要什么零件。
总而言之,自上而下设计通常比较容易上手,但是有时候会受底层复杂度的影响,这种影响甚至有时候会使事情变得比实际的情况更复杂。自下而上设计开始起来比较复杂,但是在早期鉴别出系统的复杂度,却有助于设计出更好的高层类。当然这样做的前提是复杂度没有把整个系统破坏掉。
最后要说的是,自上而下和自下而上设计并不是互相排斥的——你会收益于二者的相互协作。设计时一个启发式(试探)的过程,这意味侄儿没有任何解决方案能够保证万无一失。设计过程充满了反复的试验,请多尝试些设计方法,知道找到最佳的一种。
4. 建立试验性原则
有些时候,除非你更好地了解了一些实现细节,否则很难判断一种设计方法是否奏效。比如说,在知道它能满足性能需求之前,你很难判断某种数据库的组织结构是否适用。在选定系统使用的图形界面(GUI)程序之前,你也很可能判断不出某一特定子系统的设计是否到位。这些都是软件设计中本质性“险恶”的例子——除非你部分的解决了某一设计问题,否则你无法完整地定义出该设计问题。
有一种技术能够低成本地解决这个问题,那就是建立试验性原型。“建立原型”一词对不同人来说具有不同的含义。在这里,建立原型指的是“写出用于回答特定设计问题的、量最少且能够随时扔掉的代码”。
如果开发人员没有把握住用最少代码回答图问题的原则,那么原型方法的功效可能就会大打折扣。假如说,设计问题是“我们选定的数据库框架能否支撑所需的交易量?”你不需要为了这一问题而编写任何产品代码,你也不需要去了解数据库的详情。你只需要了解能估计出问题范围的最少信息——有多少张表、表中有多少条记录,等等。接下来你就可以用Table1、Table2、Column1、Column2等名字写出最简单的原型代码,往表里随意填入些数据,然后做你所需要的性能测试。
当有关设计的问题不够特殊的时候,原型同样也会失效。诸如“这样的数据库框架能否工作?”的设计问题并没有为建议原型提供多少指导。而像“这个数据库框架能不能在X、Y和Z的前提下支持每秒1000次交易?”这样的问题则能为建立原型提供更坚实的基础。
最后一个可能会给原型带来风险的做法是,开发人员不把原型代码当做可抛弃的代码。我发现,如果开发人员相信某段代码将被 用在最终产品里,那么他根本不可能写出最少数量的代码来。这样做做种其实是在实现整个系统,而不是在开发原型。相反,如果你建立了这样的概念,那就是一旦回答了所提出的问题,这段代码就可以被扔掉,那么你就可以把上述风险减到最小。避免产生这一问题的一种做法就是用于产品代码不同的技术来开发原型。你可以用Python来为Java设计做原型,或者用 Office PPT 来模拟用户界面。如果你必须要用同一种技术来开发原型,那么可采纳一个非常实用的标准:在原型中的类或者包的名称之前加上 prototype 前缀。这样至少能保证程序员在试图拓展原型代码之前能够三思。
一旦依照上述原则加以应用,那么原型就会成为设计者手中用来出库险恶设计问题的有力工具。如果不遵照上述原则,那么原型就会给设计再平添许多风险。
5. 合作设计
在设计过程中,三个臭皮匠顶得上一个诸葛亮,而不论组织形成的正式与否。合作可以以下面的任意一种方式展开。
-
你随便走到一名同事的办公桌前,向他征求一些想法。
-
你和同事坐在会议室里,在白板上画出可选的设计方案。
-
你和同事坐在键盘前面,用你们的编程语言做出详细的设计,换句话说,你们可以采用结对编程。
-
你约一名或者多名同事来开会,和他们过一遍你的设计想法。
-
你身边没有能检查你的工作,因此当你做完一些初始工作后,把他们全放进抽屉,一星期后再回来回顾。这时候你会把该忘的都忘掉,正好可以给自己做一个不错的检查。
-
你向公司以外的人求助:在某个特定的论坛或者新闻组里提出。
6. 要做多少设计才够
有些时候,编码之前只制定出系统架构的一个最小梗概.而在另一些时候,开发团队会把设计做的非常详细,是编码变成了一种近乎机械的工作。
如果设计层次的问题是留给程序员个人去解决的话,那么,当设计下降到你曾经完成过的某项任务的层次,或者变成了对这样一想任务的简单修改或者扩充的时候,你很可能就会停止设计而马上开始编码。
如果在编码之前我还判断不了应该再做多深的设计,那么我宁愿去做更详细的设计。最大的设计失误来自于我误认为自己已经做得很充分,可时候却发现还是做得不够,没能发现其他一些设计挑战。换句话说,最大的设计问题通常不是来自于那些我认为是很困难的,并且在其中做出了不好的设计的区域;而是来自于那些我认为很简单的,而没有做出任何设计的区域。我几乎没有遇到过因为做了太多设计而受损伤的项目。
另一方面,我偶尔会看到一些项目因太过于专注对设计进行文档化而导致失败。Gresham 法则是这样说的,“程序化的活动容易把非程序化的活动驱逐出去”。过早的去润色设计方案就是这一法则所描述的例子。我宁愿看到有80%的设计精力用于创建和探索大量的备选设计方案,而20%的精力用于创建并不很精美的文档,也不愿看到把20%的精力花在创建平庸的设计方案上,而把80%的精力用于对不两个设计进行抛光润色。
7. 记录你的设计成果
传统的记录设计成果的方法是把它写成正式的设计文档。然而,你还可以用很多种方法来记录这一成果,而这些方法对于那些小型的、非正式的项目或者只需要轻量级的记录设计成果的方式的项目而言效果都很不错。
-
把设计文档插入到代码里
在代码注释中写明关键的设计决策,这种注释通常放在文件或者类的开始位置。如果你同事使用类似于 JavaDoc 这样的问题提取工具,那么这种方法会确保设计文档对于开发这部分代码的程序员来说是立等可取的,同时也有助于程序员保持代码和设计文档之间的相当不错的同步。
-
用 Wiki 来记录设计讨论和决策
把我们的设计讨论写到项目的WIki里去。尽管文字录入要比较麻烦一些,但这样会自动地记录下你们的设计讨论和设计决策。如果使用Wiki,你可以用图片来弥补文字讨论的不足,并连接支持该设计决策的网站、白皮书及其他资料。如果你的开发团队在地理位置上是分布式的,那么这种技术会非常有帮助。
-
写总结邮件
每次就设计展开谈论后,请采取这种做法,即指派某人来写出刚才讨论的纲要——特别是那些决定下来的事项——然后发送给整个项目组。在项目组的公共电子邮件文件夹里保留一份备份。
-
使用数码相机
把白板上画出的图表照成相片然后嵌入到传统的文档里,这样做可以带来事半功倍的效果,因为它的工作量只是用画图工具画设计图表的1%,而受益却能达到保存设计图表的80%。
-
保留设计挂图
-
使用 CRC (类、职责、合作者)卡片
-
在适当的细节层创建 UML图
一种流行的绘制设计图的方法是由对象管理组织定义的统一建模语言(UML)。UML 提供了一套丰富的、形式化的表示法,可用于设计实体及其关系。你可以用非正式的 UML 图来帮助讨论和发现设计细节。由于 UML 是标准化的,因此在交流设计观念时大家都能理解它,同时还能加快团队共同讨论各种设计方案的速度。
8. 对流行的设计方法的评价
你怎样擦能判断出需要多少设计才够呢?这是一个主观判断,没有人能完美地回答它。不过,在你没有足够的信心去判断最佳设计量的时候,请记住有两种情况一定是不对的:设计所有细节和不做任何设计。这两个由位于立场两端的极端主义者所倡导的做法,恰恰被证明是仅有的两个永远是错误的做法。
正如 P.J. Plauger所言,“你在应用某种设计方法时越教条化,你所能解决的现实问题就会越少”。请把设计看成是一个险恶的、杂乱的和启发式的过程。不要停留于你所想到的第一套解决方案,而是去寻求合作,探索简洁性,在需要的时候做出原型,迭代,并进一步迭代。你将对自己的设计成果感到满意。