23章开发和设计
没有银弹。
------ F. Brooks

 构筑软件 ---- 目的与手段 ---- 开发过程 ---- 开发循环 ---- 设计目标 ---- 设计步骤 ---- 发现类 ---- 描述操作 ---- 描述依赖性 ---- 描述界面 ---- 类层次结构的重组 ---- 模型 ---- 试验和分析 ---- 调试 ---- 软件维护 ---- 效率 ---- 管理 ---- 重用 ---- 伸缩性 ---- 个人的重要性 ---- 混成设计 ---- 参考文献 ---- 忠告

23.1 概述

本章是介绍软件生产的三章中的第一章,这个讨论将逐渐细化,从有关设计的相对高层次的观察开始,结束在直接支撑这些设计的C++ 特殊程序设计技术和概念。在引言和23.3节中关于目的与手段的简短讨论之后是本章的两个主要部分:

22.4节是有关软件开发过程的一种观点。

22.5节是有关软件开发过程组织的实践性评述。

24章将讨论设计与程序语言之间的关系。第25章将从设计的观点出发,介绍类在软件组织中所扮演的一些角色。总而言之,第四部分中这三章的目标,就是填补起所谓的独立于语言的设计和程序设计之间裂隙,这方面细节尚未引起足够的重视。这个连续谱的两端在大项目中都有其重要位置,但为了避免灾难和高昂的代价,它们必须成为一些具有连续性的概念和技术的有机组成部分。

23.2 引言

构造出任何非平凡的软件片段都是一项很复杂的,而且常常使人气馁的工作。即使是对于作为个人的程序员,实际写程序语句也仅仅是这个过程的一部分。在典型情况下,分析问题,程序的整体设计,写文档,调试,以及维护,还有对全部这些事务的管理,大量的工作使写出代码片段及排除其中错误的活动相形见绌。当然,有人也可以简单地将所有这些活动都贴上“程序设计”的标签,并随之做出一种富于逻辑性的断言,“我不做设计,我只是编程”。但是,无论人们如何称呼这些活动,关注其中各个独立的部分都是极其重要的----就像偶尔地需要去关注这整个过程一样重要。在突击工作使系统得以发布的过程中,绝不应该丢掉了其中的细节或大尺度的图景----虽然这些正是经常发生的事情。

本章将集中关注程序开发过程中不涉及到写出代码片段及排除其中错误的那些部分。与本书其他地方有关个别语言特征和特定程序设计技术的讨论相比,这里的讨论不大精确,也没有那么细致。这是必需的,因为并不存在创造出好软件的烹调手册。对于某些专门工作者已经深入理解的应用,有可能存在细节的“怎样做”的条条,而对更一般的应用则没有这种东西。任何东西都不可能代替程序设计中的智慧、经验和鉴赏力。由于这些,本章将只能提供一些一般性的忠告,可以选择的途径,以及警示性的观点。

这个讨论受到软件的抽象性质的阻碍,事实上,一些对小项目(比如说,一个或两个人写10000行代码)行之有效的技术,却未必能扩展后用到中型或者大型项目中。由于这个原因,这里的一些讨论将采用与抽象的工程实践相类比的方式进行,而不是通过代码实例。请记住,“通过类比证明”是有意的欺骗,所以,这里只是想借助于类比阐述一些问题。采用C++ 特定术语,并附以实例的关于设计问题的讨论留待第24章和第25章里去做。本章所表述的思想既反映在C++ 语言本身的设计中,也反映在贯穿本书的各个具体实例的讨论里。

还要请读者记住在应用领域、人、程序开发环境等方面异乎寻常的多样化,你绝不要期望这里所提出的每个观点都能直接应用于你当前的问题。这些观点来自真实生活中的项目,能够应用于范围广泛多变的许多情况,但绝不应该认为它们是放之四海而皆准的。在考察这些观点时,请一定带着一种健康的怀疑态度。

C++ 可以简单地作为一种更好的C,但那样做,实际上就是将最有力的技术和语言特征弃之不顾,只获取由使用C++中能够得到的潜在利益中很小的一部分。本章将集中关注那些能有效地利用C++ 的数据抽象和面向对象程序设计功能的设计途径,这种技术通常被称为面向对象的设计

贯穿本章的若干主要论题是:

- 软件开发中最重要的一个方面就是弄清楚你试图去构造的东西。

- 成功的软件开发是一项长期活动。

- 我们开发的系统倾向于位于我们和我们所用的工具能够处理的复杂性的边缘上。

- 不存在能够代替设计和编程中的智慧、经验和鉴赏力的“烹调手册”方法。

- 试验对所有非平凡的软件开发都是必不可少的。

- 设计和编程是相互交叉互相影响的活动。

- 软件项目的各个阶段,例如设计、编程和调试,不可能严格分离。

- 如果没有对编程和设计活动的管理,就无法考虑这些活动。

很容易----但常常将是代价沉重地----低估这些观点的重要性。很难将它们蕴于其中的抽象思想转变成实际活动。这里特别要提出的是经验。就像造小船、骑自行车和编程一样,设计也不是一种只通过理论学习就能够掌握的技能。

一些情况太常见了,我们往往忘记了系统构筑中人的因素,而将软件开发简单地看作是“一系列具有良好定义的步骤,每一步都按照预先定义好的规则,对输入执行一些特定动作,去产生所需要的输出”。这种话完全忽略了所涉及的人!设计和编程都是人的活动,忘记这一点就会丧失一切。

本章将特别关心这样一类系统的设计,相对于构造它们的人们的经验和资源而言,完成这些系统需要竭尽全力。这看起来正是个人或者组织试图去做那种位于他们能力极限上的项目时所遇到的情况。如果一个项目没有提出这种挑战,那么就不需要认真地讨论设计,这类项目早已有现成的框架,没有必要去颠覆它。只有在某种东西需要竭尽全力才可能完成时,人们才需要采纳新的更好的工具或者过程。在选定项目时也常常有这种倾向,那些相对的新手常常认为“我们知道怎么做”,但实际上却不是这样。

绝没有设计和构筑所有系统的“唯一正确道路”。我确实希望把有关“唯一正确道路”的想法看作一种幼稚病,但许多有经验的程序员和设计师也经常趋附于这种认识。请记住,只因为某种技术对于你在去年可行,或者对于一个项目可行,并不意味着它可以不加修改地适用于其他人或不同的项目。保持一种开放的心态是最重要的。

很清楚,这里的大部分讨论将与大规模软件的开发有关。未涉足这种开发的读者可以舒舒服服地坐下来,愉快地观看自己悠然处于事外的那些恐怖场面。换种方式,他们也可以只去看这个讨论中与个人工作方式有关的那些部分。关于什么时候就必须先做设计而后再编程,在代码规模上并不存在一个下限。不过,用于设计和文档的每种途径都有其适用下限,请看第23.5.2节有关伸缩性的讨论。

软件开发中最基本的问题就是复杂性。只存在一种对付复杂性的基本办法:分而治之。如果一个问题可以分成两个能分别处理的子问题,这个划分就已经解决了问题的一多半。这一简单原理可能以令人吃惊的丰富多彩的方式去应用。特别是,在系统设计中使用一个模块或者类,就把程序分成了两部分----实现与其用户----它们之间只通过一个(理想情况)定义良好的界面相互联系。这是处理程序中内在复杂性的基本方式。与此类似,设计一个程序的过程也可以分割成一些独立活动,在所涉及的人之间(理想情况)也有定义良好的相互关系。这就是处理开发过程的和所涉及人们的内在复杂性的基本途径。

在两种情况下,各个部分的选择以及不同部分之间界面的刻画都是最需要经验和鉴赏力的地方。这种选择不是一种简单的机械性过程,通常都需要某种洞察力,而这种洞察力的获得只能通过在某个适当抽象层次上对一个系统的透彻理解(参见第23.4.224.3.125.3节)。对程序或者软件开发过程的短视观点经常导致带有严重缺陷的系统。还应当注意,无论是对人还是对程序,分开总是容易的,更困难的部分是保证分界两边各个部分间的有效通讯,而又不破坏这种分解,不抑制为协作所需要的通讯。

本章将介绍的是一种设计途径,而不是一种完整的设计方法。某种完全形式化的设计方法已超出了本书的范围。这里介绍的途径可能被用于不同程度的形式化,或者作为不同形式化的基础。同样,本章不是一个文献综述,不企图去触动与软件开发有关的每一个论点或者介绍每一种观点。那同样也超出了本书的范围。有关文献的综述可以在 [Booch,1994] 里找到。还请注意,这里对术语的使用也采取了最普通最方便的方式。最“有意思”的术语,例如设计原型程序员,在各种文献中都有许多不同的甚至相互冲突的定义。请小心,不要基于某些术语的特定的或者具有局部精确性的定义,从这里所写的文字中读出了某些并不是这里原意的东西。

23.3 目的与手段

专业程序设计的目标就是发布满足用户需要的产品。达到这一目的的基本手段就是生产出具有清晰内部结构的软件,培养出一批设计师和程序员,使之具有足够的技能和动力去快速有效地应对各种变化和机遇。

为什么?程序的内部结构和将它开发出来的过程,按照理想情况,都是最终用户不必关心的。说得更重一点,如果最终用户不得不关注程序是怎样写出来的,那么这个程序一定有什么地方出了毛病。承认了这些事实之后,一个程序的结构以及创建它的人们的重要性又体现在哪里呢?

程序需要有一个清晰的结构,以便较容易:

- 调试,

- 移植,

- 维护,

- 扩充,

- 重组,以及

- 理解。

这里的重点是,软件中每个成功的主要部分都会有一个很长的生命周期,一系列程序员和设计师将在它上面工作,移植到新的硬件,去适应原本没有预料到的用户,并反复地重新组织。贯穿着这个软件的整个生命期,它的新版本必须被生产出来,带着可接受的错误率,而且要及时。没有这种计划就是计划去失败。

注意,尽管按照理想的情况,最终用户不必知道一个系统的内部结构,但他们也可能希望知道。例如,一个用户可能希望知道某系统的设计细节,以便能评价它大致的可靠性,以及修改和扩充潜力。如果所论及的软件不是一个完整系统----而是为构造其他软件的一组库----那么用户将希望知道更多的“细节”,以便能更好地使用这些库,并更好地将它们作为获取思想的资源。

必须努力在缺乏对软件整体设计和过度强调结构之间寻找一种平衡。前者将引起无穷无尽的修改(“我们将立即发布这一个,并将在下一个版本里解决这个问题”)。后者导致一个过于精致的设计,使最具根本性的东西被淹没在形式主义之中,还由于不停地重新组织而导致实现的延误(“但这种新结构比原来的那个好得多,人们会愿意等着它”)。作为结果的系统对资源的需求将如此强烈,以至于大部分潜在用户都无法承受其代价。这种平衡总是在设计中最困难的方面出现,这也正是才能和经验发挥作用的地方。对作为个人的设计师和程序员,做出这种选择也是很困难的;对于涉及到许多具有不同技能水平的人们的大型项目,做出这种选择就更加困难。

一个程序需要由一个组织生产出来并继续维护,它应该能完成这件事情,无论其人事、方向和管理结构发生了什么变化。对付这一问题有一种很流行的方式,那就是试图将系统的开发归结到能够塞进一个严格框架的一些相对低层次的任务里。也就是说,其基本思想是创建一支容易训练的(廉价的)并具有互换性的低水平程序员(“编码员”)队伍,和另一个不那么廉价但同样具有互换性(因此有同样可依赖性)的设计师队伍。假定编码员不做任何设计决策,而且假定设计师本人都不涉足卑微的代码细节。这种途径经常失败。在它还能行的地方,将生产出性能可怜且过于庞大的系统。

这种途径的问题在于:

- 实现者与设计者之间不充分的通讯。这将导致机会的丧失、延期、低效率,以及缺乏从经验中学习而引起的种种问题。

- 未给实现者的主动精神提供充分的发展空间。这将导致专业培养不足,缺乏原动力,懒散,很高的人员变动比率。

简而言之,这样一种系统缺乏反馈功能,不能使人们从其他人的经验中获益。它是对稀有的人类才能的浪费。我们需要创建一种框架,使人们能在其中应用各自的聪明才智,开发出新的技能,贡献各种思想,并为自己所完成的东西不仅能说得过去,而是很实际很经济而感到愉快。

另一方面,如果没有某种形式结构,一个系统也不可能无限期地构筑、写文档、维护下去。简单地找到一批最好的人,让他们按自己所想的最佳方式去进攻某个问题,对于一个需要创新性的项目,这常常是一个很好的起点。然而,随着项目的进展,在项目中引进更多安排好的、专门的、更形式化的交流就变得越来越必要了。对于“形式化”,我的意思并不是某些数学的或者能够机械验证的记法(虽然这很好,在那些可用的,且可以应用的地方),而是一集有关记法、命名、文档书写、调试等等的指导性规则。再说一次,取得平衡和有意识的适度很有必要。过分严格的系统将阻碍成长,窒息创新。在这种情况下,被测试的将是管理者的才能和经验。对于个人而言,与之等价的两难问题是要做出选择:在哪里试着做得更聪明些,而在哪里就简单地“按书本行事”。

这里的基本建议是,我们不只要为当前项目的下一个版本做计划,还要考虑更长远的问题。只计划下一个版本就是计划失败。我们必须面向生产和管理许多项目的许多版本,去发展组织结构和软件开发策略,我们必须计划一系列的成功。

“设计”的宗旨在于为程序创建起一种清晰且相对简单的内部结构,有时被称为体系结构。换句话说,我们希望建立起一个框架,使单独的代码片段可以纳入其中,并能对这些单独代码片段的书写起指导作用。

设计是有关的设计过程的最终产品(就算是一个不断重复的过程,也存在着一个最终产品)。它是设计师和程序员之间、程序员之间交流的集中点。在这里有权衡轻重的能力就非常重要了。如果我作为一个个人程序员设计了一个小程序,准备明天去实现,其精确性和细节程度可能就只是在一个信封背面的随意几笔。在另一个极端,在开发一个涉及到数以百计的设计师和程序员的系统时,就可能需要用形式化或者半形式化的记法,仔细写出很多本规范。为设计确定一种恰当层次的细节、精确程度和形式方式,这本身也是一项富于挑战性的技术和管理任务。

在本章和随后的两章里,我假定一个系统的设计被表述为一集类声明(典型情况是将它们的私用声明忽略,作为假想的细节)及其相互关系。这是一种简化。在一个特定设计中还要加入比这多得多的细节,例如,并发性、名字空间管理、非成员函数和数据的使用、参数化类和函数、为最少的重新编译而做的代码组织、持续性功能,以及多种计算机的使用等等。然而,为了能在这个层次上讨论就必须做一些简化,而类正是在C++ 里进行设计的最恰当的注目点。在这一章的进程中将提到了其中的一些问题,那些直接影响C++ 程序的设计的问题将在第24章和第25章讨论。有关一种特殊的面向对象设计方法的细节讨论和实例,可以参看 [Booch, 1994]

我没有去划清分析和设计之间的界线,是因为这个问题的讨论已经超出了本书的范围,而且它对特定设计方法的变化也比较敏感。找到一种能够与设计方法相匹配的分析方法,找到一种能够与程序设计风格和所用语言相匹配的设计方法,这些都是最重要的事情。

23.4 设计过程

软件开发是一个不断重复的递进的过程。在开发中将不断地重新历经这个过程中的各个阶段,每次历经时,通过精化产生出一个阶段的最终产品。一般说,这一过程既没有开始,也没有结束。在设计和实现一个系统时,你在其他人的设计、库和应用软件的基础上开始工作。当你结束时,你留下了一个包括设计和代码的实体,供其他人去精化、修改、扩充和移植。自然,一个特定项目必然有其确定的开始和结束,将一个项目的时间和范围划分得清晰精确是极其重要的(但又常常困难得令人不可思议)。然而,自以为你是从一个清晰的状态开始却可能引起严重的问题,自以为那个世界结束在“最终交付”将给你的继任者造成类似的严重问题,而这个继任者常常就是扮演着另一个角色的你自己。

这种情形蕴含着的一件事,就是下面几节可以按任意顺序阅读,因为在实际项目中,设计和实现的各个方面几乎能任意地交叉出现。也就是说,“设计”几乎总是在原来设计和某些实现经验基础上的重新设计。进一步说,这个设计又受到计划安排、所涉及的人员的能力、兼容性等等因素的约束。对于设计师/管理人员/程序员的一项主要挑战就是为此过程建立一种秩序,而又不会窒息创新,不会破坏反馈循环,因为它们对于成功的开发都是必不可少的。

开发过程包含三个阶段:

- 分析:定义需要解决的问题的范围

- 设计:建立系统的整体结构

- 实现:写出代码并完成调试

请切记这个过程不断重复的本性----这些阶段不是编号的,这一点非常重要。注意,程序开发还有一些重要方面没有作为单独的阶段出现,是因为它们应该弥漫在整个过程之中:
- 试验

- 调试

- 对设计和实现的分析

- 撰写文档

- 管理

软件“维护”不过是穿过这一开发过程的更多循环罢了(23.4.6节)。

最重要的是,分析、设计和实现不应该过分地相互分离,应该使涉足其中的所有人员能共享一种文化,使他们能有效地交流。在大型项目中,实际情况却常常不是这样。理想的情况是,参与项目的个人应该从一个阶段转移到另一个阶段,因为传递精微信息的最好方式就是通过人的头脑。不幸的是,组织机构常常建立起某些阻碍,阻止这种转移,例如,给予设计师比“仅为程序员者”更高的地位以及/或者更高的待遇。如果让人们移来移去参与学习和讲授的想法不能实现,那么至少也应鼓励他们,与涉足同一开发中的“其他”步骤的人们开展常规性的交流讨论。

对于小型到中型的项目,在分析和设计之间常常没有清晰的划分,这两个阶段常常被合而为一。与此类似,在小项目中,设计和实现之间常常也没有分开。当然,这样就解决了交流的问题。重要的是,对给定项目应当有适度的形式规定,并维持各个阶段间适度的分离(23.5.2节)。并不存在做这些事情的唯一正确方法。

这里所描述的软件开发模型与传统的“瀑布模型”截然不同。在瀑布模型中,开发过程具有一种顺序的线性形式,开发从分析阶段一直到调试阶段。瀑布模型受到这种单向信息流动所造成的基本缺陷的损害。在“下游”发现问题时,通常存在着强有力的方法学上的和组织上的压力,要求去做局部修正,也就是说,强烈要求解决问题时不影响开发过程的前面阶段。缺乏反馈将导致有缺陷的设计,局部修正将导致扭曲的实现。当出现不可避免的情况,信息必须流向源头并引起设计的改变时,结果只是一股极慢的拖累沉重的涓涓细流穿过整个系统。该系统特别适合阻止这种改变,至多是极不情愿地极其缓慢地做出回应。这样,有关“不要改变”或者“局部修正”的论点就变成了另一种论点:一个子组织不能“为了它自己的方便”将大量的工作强加给其他的子组织。特别是,在发现了一个主要缺陷的时候,常常出现大量与决策缺陷有关的纸面工作,涉及修改文档的工作常大大超过修正代码所需要的工作。按照这种方式,纸面工作很可能变成软件开发中的主要问题。很自然,这一问题可能----而且确实----出现了,无论人们如何去组织大型系统的开发。话说回来,有些纸面工作毕竟是不可或缺的,但是,采用线性开发模型(一道瀑布)将极大地增加这一问题变得失去控制的可能性。

瀑布模型的问题在于不充分的反馈,以及无能力对修改做出响应。而这里勾勒出轮廓的交互式途径中也有危险,在这里存在着一种诱惑,使我们可能用一系列不收敛的变化去代替真正的思想和进步。这两个问题都很容易诊断,但却都很难解决。无论人们如何去组织一项工作,都很容易受到诱惑,将错误的行为当作进步。很自然,随着项目进展,对开发过程不同阶段的重视程度也应有所变化。开始时,重点是分析和设计,较少关心编程问题。随着时间的推移,更多的资源将移到设计和编程,而后将更多地关注编程和调试。但无论如何,关键点是绝不能在关注分析/设计/实现这个连续谱中某部分时完全忽略了其他各个部分。

请记住,如果你对试图达到什么没有一个很清晰的想法,无论你将多少注意力放在细节上,使用的是多么合适的管理技术,采用了多少高级技术,都不会有多少帮助。因为缺乏定义清晰的实际的目标而失败的项目,比因为其他原因而失败的项目更多。无论你要做什么,也不管你怎样去做它,请定义好一些实实在在的目标和阶段性标志,而且不要企图去为社会问题寻找技术性解决方案。而在另一方面,应当使用任何可用的合适技术----即使它涉及到投资。有了合适的工具,在合理的环境中,人们可以工作得更好。请不要欺骗自己,以为按照这些建议去做很容易。

23.4.1 开发循环

开发一个系统是一件不断重复的活动。其主要循环是通过如下序列的一再重复的旅行:

  1. 考察问题。
  2. 创建一个整体设计。
  3. 找出一些标准部件。

  4. 为本设计而改制这些部件。
  5. 创建一些新的标准部件。

  6. 为本设计而改制这些部件。
  7. 装配起整个设计。
作为类比,让我们来考虑一个汽车工厂。在一个项目开始时,需要有对一种新型汽车的总体设计,最先的决策将基于某些分析,用一些一般性的术语刻画这种汽车,主要是根据对该车所设想的应用,并不牵涉如何才能达到所需要的性质的具体细节。确定需要有哪些性质----或者采取更好的方式,为确定究竟需要什么性质提供一个相对简单的准则----常常是一个项目中最困难的部分。如果这件事真能做好,那么通常总是由某个富于洞察力的个人独立完成的,这常常被称为一个vision(设计目标)。一个项目缺乏这种清晰目标的情况也是相当常见的,许多项目因此而踉踉跄跄以至于失败。

比如说我们希望制造一种四门中型车,带有相当强劲的引擎。设计中的第一步通常都不是从空白开始去设计这辆车(及其部件)。软件设计师和程序员也处于类似环境中的,但却可能很不明智地想去空白开始。

第一步是考虑,从工厂自身的库存和可靠供应商那里能够得到哪些部件,这样找到的部件并不一定正好适合这种新车。存在许多方式去改制这些部件,或许可能去影响这些部件的“下一个版本”的规范,使之更适合我们的项目。例如,可能存在一种具有合适特性的引擎,但它的输出功率却略显不足。或者是我们,或者是引擎的提供商可以添加一个涡轮增压器,又不影响其基本设计。注意,如果引擎的原初设计并没有结合进某种改制的可能性,那么要做这种修改而又“不影响其基本设计”就未必可行了。这种改制通常需要你与你的引擎供应商之间的合作。软件设计师和程序员也有类似的选择机会。特别的,多态性的类和模板常常可用于有效地改制。当然,如果没有一点前瞻性,或者得到这种类的提供者的合作,我们就没指望去做任意的扩充。

在穷尽了合适的标准部件后,汽车设计师还不急于去为新车设计最优化的新部件,因为这样做代价会太高昂。假定不存在合适的空调装置,而在引擎舱里有一块合适的L形空间可以利用。一种解决方案就是设计一种L形的空调单元。但是,将这种独特设备用于其他车型的可能性将非常小----即使是经过高成本的改制。这就意味着,我们的汽车设计师将不能与其他汽车设计师共同承担这种单元的生产成本,而且这种单元的可用生命期也会比较短。根据这种认识,看来值得去设计能满足更广泛需求的单元,也就是说,设计一种在设计上比我们所假想的L型怪物更清晰、也更适宜改制的空调单元。这样做,所涉及的工作通常会比做L型单元更多一些,甚至可能涉及到修改我们汽车的整体设计,以适应这种具有更一般用途的单元。又因为这种新单元的设计要比我们的L型奇异物件更加广泛可用,它也将需要做一点改制,才能正好符合我们修正之后的需要。同样,软件设计师和程序员也有类似的选择机会。也就是说,设计师可以不去写项目专用的代码,而是设计一种具有普遍意义的部件,这将使它成为某些世界中一个标准的候选品。

最后,当弄完了所有潜在的标准部件之后,我们去装配起“最后的”设计。我们根据可能采用了若干特殊部件,因为下一年我们将不得不为下一个新车型再次经历这个工作的某种变形,而这些特别设计的部件将是我们最可能重新去做或者丢掉的。非常糟糕,在传统软件设计的经验是,一个系统里很少有几个部分能看作是可分离的部件,这些之中更少有什么可以用到它们原来的项目之外。

我并没有说所有汽车设计师都像我所勾勒的这一类比中那样富于理性,也没有说所有软件设计师都犯这里所提及的错误。相反,我想说的是,这个模型对软件也可行。特别是,本章及随后几章要介绍一些技术,可以使之对于C++ 可行。不过我也确实声言,由于软件的无形的本质,要避免这些错误是更困难的(24.3.124.3.4节)。在22.5.3节里,我还要争辩说,企业文化常常妨碍人们去使用这里所描绘的模型。

注意,只有在你有长远打算之时,这里的开发模型才能真正工作得很好。如果你的视野只延伸到下一个版本,那么创建和维护标准部件就完全没有意义了,它只能被看作是一种臆造出来的额外负担。这个模型只是提供给那样一种组织,其生命期将跨过几个项目,其规模使得在工具方面(为了设计、编程和项目管理)的额外投资具有实际价值。这是一类软件工厂的蓝图。很奇怪,这种模型与最好的个人程序员的实践只有规模上差异。这些人在多年中为提高个人的工作效率,创造起一大批技术、设计、工具和库。看起来,实际中大部分组织机构都未能利用最好的个人实践活动,因为缺乏远见,也因为没有能力在超过很小的规模之上管理这种实践性活动。

注意,期望“标准部件”成为全球性的标准并不合理。确实会出现少量国际性的标准库,但是,绝大部分将只能成为一个国家、一个产业、一个公司、一条生产线、一个部门、一个领域等等内部的标准。这个世界太大,使我们不可能对所有部件和工具都实现全球性的标准,这也确实不应该成为一个目标。

在初始设计时就将全球性作为目标,实际上就是命令这个项目永远也不能完成。开发循环之所以是一个循环,也因为取得一个能工作的系统是必不可少的,因为只有从那里才能取得经验(23.4.3.6节)。

23.4.2 设计目标

设计的总体目标是什么?当然,简单性是其中之一,但简单性又要依据什么准则呢?我们应假定一个设计将要演化,也就是说,这个系统将要扩充、移植、调整,一般说,将以某些不可能都事先预见到的方式改变。因此,我们就必须确定目标,使设计和实现出来的系统比较简单,有关的约束条件使它能够以多种方式变化。事实上,应该假定在它的初始设计到第一个版本间,这个系统就已改变了几次。这种假定是很现实的。

这也就意味着,系统的设计必须能在一系列变化之后仍然尽可能的简单。我们必须为变化而设计,也就是说,我们必须将目标定在:

- 灵活性,

- 可扩展性,以及

- 可移植性。

解决这种问题的最好方式是将系统中可能变化的区域封装起来,并为设计师/程序员提供某些非侵入式的方法,使他们能够修改代码的行为。完成这项工作需要标识出应用中的关键性概念,给每个类一种排他性的责任,要求它维护着与某个单一概念有关的全部信息。在这种情况下,一项变化的实施就只需要修改那些有关的类。最理想的情况是,一个概念的修改可能通过一个派生类(23.4.3.5节),或者通过给某模板传递一个不同参数而完成。当然,说出这种理想比实现它要容易得多。

现在考虑一个例子。在一个涉及天气现象的模拟中,我们希望显示出一种雨云。但怎样做呢?我们不能有一种通用例程来完成云的显示,因为云的视觉形象依赖于云的内部状态,而这种状态完全在云的责任范围之中。

对于这种问题的第一种解决方案就是让云去显示自己。这种风格的解法在许多受限的上下文中都是可以接受的。但是它不够一般,因为存在许多观察云的方式:例如,作为一个细致的图形,作为一个粗略的轮廓,或者作为地图上的一个图标。换句话说,云的视觉形象依赖于云和它的环境。

这一问题的第二种解决方案是让云意识到它的环境,而后让云去显示自己。在更多的上下文里可以接受这种解决方案。但这仍然不是一种一般性的解法。让云了解其环境的这些细节也违反了前面的决断,即让一个类对一件事情负责,且每件“事情”都是某一个类的责任。很可能无法创造出一个具有内在和谐性的“云的环境”的概念,因为一般来说,云的视觉形象依赖于云和观察者。甚至在现实生活中,云的视觉形象也在很大程度上依赖于我怎么去看它,例如,是通过我的裸眼,通过偏振滤镜,还是通过气象雷达。除了云雨观察者之外,某些“一般性的背景”,例如太阳的相对位置等,也必须予以考虑。此外,其他实体,例如其他的云和飞机等,将进一步使问题复杂化。如果再加上可以同时存在多个观察者,设计者的日子就更难过了。

第三种解决方案使让云----以及其他物体,如飞机和太阳----将自己描述给环境。这一解法对于大部分目的而言都具有足够的一般性。然而,这样做可能带来复杂性和运行时间方面的显著代价。例如,我们将如何安排,使观察方可以理解由云和其他实体产生出的描述呢?

雨云在程序里并不经常出现(要找一个实际例子,请看第15.2节),但涉及到许多I/O操作的对象则在程序里比比皆是。这就使这个有关云的例子与程序有了一般性的联系,特别是与库设计的联系。针对逻辑上类似的实例的C++ 代码,可以在流I/O系统中格式化输出所用的操控符那里看到(21.4.621.4.6.3节)。注意,这里的第三个解决方案并不就是“那个正确的解法”,而只是最具一般性的解法。设计师需要权衡在一个系统里的各种需要,去选择适合特定系统里针对特定问题的一般性和抽象层次。作为一种经验法则,一个长寿命程序的合适抽象层次就是你可以理解和负担的层次中最一般的,当然不是绝对的最一般。超出特定项目范围和参加者经验的一般性可能很有害,即,它可能造成延期、无法接受的低效率、无法管理的设计,或者就是失败。

为使这种技术成为可管理的、经济的,我们必须为重用而设计和管理(23.5.1节),而且不能完全忽视效率(23.4.7节)。

23.4.3 设计步骤

考虑如何设计一个单独的类。通常这并不是一种好想法。概念并非孤立存在的,相反,一个概念通常总是定义在其他概念形成的环境之中。与此相似,一个类也不应孤立地存在,而应该与逻辑上相关的类一起定义。典型情况是人需要做出一集相关的类,这样一个集合常被称为一个类库或一个组件。有时一个组件里的所有类组成了一个类层次结构,有时它们是一个名字空间的成员,有时它们不过是实际汇集在一起的一些声明(24.4节)。

在一个部件里的一集类总是通过某种逻辑准则结为一体的,常常是由于一种共同风格,常常由于依赖于一种公共的服务。这样,一个部件就是一种设计、写文档、拥有和重用的单位。这并不意味着如果你使用了某个部件里的一个类,你就必须理解和使用这个部件里所有的类,也不意味着可能要把该部件中每个类的代码都装入你的程序。正相反,我们总是要努力去保证,一个类的使用只给机器资源和人们的付出带来最小的开销。当然,为了使用一个组件里的任何一部分,我们将需要理解定义这一组件的逻辑准则(希望文档里写得足够清楚)、使用约定,该部件设计中所蕴涵的风格以及它的文档,还有那些公共服务(如果存在的话)。

现在考虑人应该如何完成一个组件的设计。因为这常常是一项挑战性的工作,我们值得将它分解为一些步骤,以便能以一种逻辑的和完全的方式,集中关注其中的各项子工作。如常,做这件事也不是只有一条正确道路,不管怎样,下面是一些人所采用的一系列步骤:

  1. 找出概念/类及其最基本的相互关系。
  2. 精化这些类,描述它们的一集操作。

  3. - 将这些操作归类,特别是考虑所需要的建构函数、复制和析构函数。
    - 考虑最小化、完全性和方便性。
  4. 精化这些类,描述它们之间的依赖关系。

  5. - 考虑参数化、继承和使用方面的依赖关系。
  6. 描述界面。

  7. - 将函数分为公用的和保护的操作。
    - 描述这些类中各操作的确切类型。


注意,这也是在一个重复性过程中的一些步骤,要为初始实现或者重新实现产生一个用起来很舒服的设计,通常需要穿过这一序列的几次循环。像这里所描述的很好完成的分析和数据抽象有一个优点,它能使重新安排类之间的关系变得相对容易些,即使是在代码已经写成之后。当然,这绝不会是一件平凡工作。

23.4.3.1 步骤1:发现类

找出概念/类及其最基本的相互关系。一项好设计的关键是直接模拟“真实世界”的某个侧面----也就是说,将应用领域中的概念拿来作为类,用良好定义的方式表示类之间的关系,例如用继承,并且在不同的层次上反复地这样做。但是我们怎么能找出这些概念?什么是确定我们所需要的类的实际途径呢?

开始寻找的最佳地方应该是在应用本身,而不是去看计算机科学家口袋里的抽象和概念。听听某些在系统完成后将成为专家用户的人,以及对现有的将被取代的系统不满意的人们说些什么。注意他们所用的词汇。

人们常说名词将对应于程序里所需的类和对象,确实经常如此。但是,这并不意味着故事的结束。动词有可能指称对象上的操作,或者传统的基于其参数值产生新值的(全局)函数,或者甚至是类。作为最后一种情况的例子,请注意函数对象(18.4节)和操控符(21.4.6节)。像“重复”或者“提交”一类的动词可能用一个迭代器对象或者代表数据库提交操作的对象表示。再考虑形容词“可存储的”、“并行的”、“注册的”和“约束的”,这些也可能成为一些类,其目的就是使设计师或程序员能通过描述虚基类,在所需要的属性里捡取或者选择后面设计的类(15.2.4节)。

并不是所有的类都与应用层中的概念遥相对应。例如,那些表示系统资源和实现层的抽象(24.3.1节)。避免过近地模拟老系统也非常重要。例如,我们不会希望一个以数据库为中心的系统亦步亦趋地重复一个手工系统的各个方面,原来系统的存在可能不过是为使人们能物理地将纸片移来移去。

继承是用于表述概念之间的共性。最重要的是,用它表示一种基于各个类所代表的个别概念的层次性组织(1.712.2.624.3.2节)。这有时被称为分类(classification)或甚至分类学(taxonomy)。共性必须去主动寻找,推广和聚类都是高层次的活动,需要借助于洞察力,才能取得有用且耐久的结果。公共基类应当代表某种一般性的概念,而不是某个与之相近的概念,只不过需要表示的数据恰好比较少。

请注意,这种分类应该基于所模拟系统中的概念的某些方面,而不是基于在别的领域中可能合法的一些东西。例如,在数学里圆是椭圆中的一类。但是在大部分程序里,圆不应该由椭圆派生,椭圆也不应由圆派生。常听到这样的论点,如“因为这就是数学里的情况”,或者“因为圆的表示是椭圆表示的子集”。这些都不是决定性的,常常都是错误的。这是因为,对于大部分程序而言,一个圆的关键属性是它有一个中心和到其圆周的固定距离,圆的所有行为(所有操作)必须维护这些属性(不变式,第24.3.7.1节)。而在另一方面,椭圆则由两个焦点描述,在许多程序里这两个焦点可以独立地改变。如果焦点重合,这个椭圆看起来就像圆,但是它不是圆,因为它的操作并不能维持圆的不变式。许多系统里都对这种差异有所反应,方式是为圆和椭圆提供的操作集合相互都不为子集。

我们不会一下就想出了一集类和这些类间的相互关系,并将它们用于最终的系统。相反,我们会先创建出一集初始的类和关系,而后又反复对它们做精化(23.4.3.5节),以达到一集类关系,使之足够地一般、灵活和稳定,确实能对系统地进一步演化有所帮助。

找出初始的关键性概念/类的最好工具就是黑板,对它们的初始精化的最好方法就是与应用领域的专家和一些朋友讨论。要开发出一集有活力的词汇和概念框架,这种讨论是必不可少的,少数几个人就可以独立地去做。为从初始的候选类集合中发展出一集有价值的类,一种方式就是去模拟一个系统,让设计师去扮演类的角色。这样做,能使初始想法中不可避免的荒谬情况暴露到一种开放的、使人兴奋的有关替代方案的讨论中,使人们在如何改变设计上取得共识。这种活动也可以用索引卡片支持,在卡片上记下文档。因为在这种卡片上记录的信息,人们通常将它们称为CRC卡片(类,责任、合作,“Class, Responsibility, and Collaboration”)。

一个用例use case)就是关于一个系统的一次特定使用的描述。这里是某电话系统的用例的一个简单例子:将电话拿起,拨号,在另一端的电话振铃,另一端的电话被拿起。做出一集这样的用例,对于开发的各个阶段都有巨大的价值。在开始阶段,找出这种用例能帮助我们认识准备去构造的是什么。在设计阶段,可以利用它们追踪贯穿这个系统的路径(例如,使用CDC卡片),从某种用户观点检查相对静态的的系统描述,看看各种类和对象是否真正有意义。在程序设计和调试中,这些用例也成为调试实例的源泉。以这种方式,一组用例为观察所开发的系统提供了一种正交的方式,作为一种实在的检查。

用例将系统看作一种(动态的)工作的实体。因此,它们有可能诱使设计者从功能性的角度去对观察这个系统,使他们偏离了找出能映射到类的有用概念的本质性工作。特别是到了那些有着结构分析背景,较少面向对象程序设计经验的人们手里,强调用例很容易引向某种功能分解。一集用例并不是一个设计,对于系统使用的关注也必须和与之互为补充的对系统结构的关注相配合。

一支开发队伍也可能被引诱到想去发现和描述所有的用例。这实际上并无益处,而且是一项代价高昂的作业。就像我们在为系统寻找候选类中的情况一样,在某个时候我们必须说,“够了就是够了,现在是去试一试我们已经找出的东西,看看会出现什么情况的时候了”。只有通过一集表面上能说得通的类和一集说得通的用例,我们才能在进一步的开发中获得反馈,这种反馈对于最终得到一个好系统是必不可少的。要确定何时应该结束一种有价值的活动确实很困难,而在这里,我们明明知道以后还会再转回来以便完成整个工作,确定何时结束这种活动就更困难了。

多少用例就足够了?一般说,这个问题不可能有答案。当然,对于一个特定项目,会有一个时刻,那时我们很清楚已经覆盖了系统的大部分常规功能,更多不那么平常的情况以及错误处理问题中的相当一部分也有了处理。这就是进入下一轮设计和编程的时候了。

当你试图评价一集用例对系统的覆盖情况时,将它们划分为主要用例和次要用例常常很起作用。主要用例描述了系统的最常见的和“正常的”活动,次要者描述那些不那么正常的或者出错时的情景。次要用例的一个实例是上面“打电话”的一个变形,其中在拿起话筒后拨的是自己的号码。人们常说,如果已经覆盖了80% 的主要用例和某些次要用例,这就是前进的时候了。当然,因为我们并不知道到底哪些东西组成了“所有的用例”,所以这只能是一条经验规则。实际经验和明智的判断将能在这里起作用。

这里所提到的概念、操作和关系都是从我们对应用领域的理解中自然产生的,或者是在对类结构的进一步工作中浮现出来的。它们代表了我们对应用的理解,经常是对基本概念的一种分类方式。例如,云梯车是一种救火车,救火车是一种卡车,卡车是一种车辆。第23.4.3.2节和节23.4.5节从取得进展的角度解释了几种观察类和类层次结构的几种方式。

注意视觉图形工程。在某个阶段,你将被要求将设计展示给某些人,你将做出一组图形,以解释将要构造的系统之结构。这将是一种极其有益的演习,因为它能帮助你将注意力集中到系统最主要的方面,要求你用其他人能够理解的方式去表述你的想法。做报告是一种最宝贵的设计工具。为那些有兴趣并能提出建设性批评意见的人准备一个意在使他们能真正理解的报告,这是一个将自己的想法概念化,并清晰地表达出来的很好练习。

当然,有关设计的正式报告也是一种很危险的活动,因为存在着很强的诱惑力,使人想去描述一个理想的系统----一个你盼望自己能构造出的系统,一个你的高级管理层盼望他们能够拥有的系统----而不是你所有的,或者你可能在合理时间内生产出来的系统。当存在着相互竞争者,行政部门并不真正理解或者不关心“细节”时,报告也可能变成一种编造谎言的比赛。开发队伍在这里展示其最浮夸的系统,以便保住自己的工作。在这种情况下,代替思想的清晰表述的常常是大量莫名其妙的难懂辞藻和缩略语。如果你是这种报告的听众----特别当你是决策者或者资源的控制者时----绝对重要的就是你应该从实际计划中区分出那些仅仅是意愿性的想法。高质量的报告材料并不能保证所描述的系统也有高的质量。事实上,我常常看到一些真正关心实际问题的组织,在与那些较少关心实际产出系统的组织竞争中,在展示其结果时却很快就落入了陷阱。

在寻找能够用类表述的概念时,应该注意到,系统里的某些重要性质是不可能用类表示的。例如,可靠性、性能和可测试性都是系统里能实测的重要性质,然而,即使是最彻底的面向对象系统,也无法将其可靠性局部化为一个可靠性对象。系统里的这种弥漫性的属性只能描述,去为它们而进行设计,并最终通过实测进行验证。对这些性质的关注必须应用于所有的类中,可能需要反应在一些类和组件的设计与实现规则中(23.4.3节)。

23.4.3.2 步骤2:描述操作

精化有关的类,描述它们的一集操作。很自然,不可能将找类的工作与确定它们所需操作的工作截然分开。然而这里也存在着一种实际的区分,确定类时的注目点在于关键性的概念,有意淡化了类的计算性质;而在描述操作时,关心的就是找出一集完整可用的操作。同时考虑这两方面的问题常常会因为太困难而没法做,特别是因为许多相关的类还需要一起设计。在需要同时考虑这些问题的时候,CRC卡片常常很有帮助(23.4.3.1节)。

在考虑需要提供那些函数时,存在几种不同的哲学。我建议采用如下策略:

  1. 考虑这个类的对象应该如何建构、复制(如果允许)和析构。
  2. 定义由该类所代表的概念所需要的最小操作集合。典型情况是将它们作为成员函数(10.3节)。
  3. 考虑为了记述的方便需要增加哪些操作,只将其中最重要的几个包括进来。这些操作常常成为非成员的“协助函数”(10.3.2节)。
  4. 考虑哪些操作应该是虚的,也就是确定那些将这个类作为界面,应该由派生类提供实现的函数。
  5. 对于本组件中所有的类,通盘考虑它们在命名方面和功能方面可能取得哪些共性。
这很显然是一种最小化的说法。把想象中可能有用的函数都加进去,把所有函数都做成虚的,这样做将容易许多。但是,函数越多,其中的某些函数始终都不使用的可能性也就越大,它们也更容易限制随后的实现以及将来系统的演化。特别是那些直接读写对象状态中某些部分的函数,它们常常会将类束缚于某种单一的实现策略,从而严重地限制重新设计的可能性。这种函数降低了抽象的层次,使之脱离了被实现的概念。增加函数也增加了实现者的工作----还有重新设计时的设计师的工作。与某个函数已变成一种责任之后再去删除它相比,在某种需要已经弄清楚后加入一个函数,做起来要容易得多。

这里要求明确地做出将函数作为虚函数的决策,而没有把这件事情作为默认规定或者实现细节。提出这种要求的原因是,将一个函数做成虚函数,将对其所在类的使用以及这个类与其他类的关系产生至关重要的影响。即使在某个类里只有一个虚函数,该类的对象的布局就不会像C或者Fortran语言的对象那样简单了。还有,如果某个类有了一个虚函数,它也就有潜力作为一些尚未实现的类的界面,而这个虚函数也就隐含着对那些尚未实现的类的依赖性(24.3.2.1节)。

注意,这种最小需求要求设计师做更多的工作,而不是更少。

在选择函数时,最重要的就是集中关心它应该做什么,而不是去关心它应该怎样做。也就是说,我们应该更多地注意所需要的行为,而不是去关注实现中的问题。

按照函数对于对象内部状态的使用情况将它们分门别类,有时也很有用处:

- 基础操作:建构函数、析构函数和复制操作

- 探查函数:那些不改变对象状态的函数

- 修改函数:修改对象内部状态的函数

- 转换函数:那些基于操作所针对的对象的值,产生其他类型的对象的函数

- 迭代函数:访问或使用所包容的一系列对象的操作

这些分类并不是互不相关的。例如,一个迭代函数也可以设计为一个探查函数或者修改函数。这些类比也就是一种分类方式,可以帮助人们处理类界面的设计问题。很自然,同样可以有其他分类方式。这种分类方式在维持一个组件内部各个类间的统一性方面特别有用。

C++ 通过const和非const成员函数的方式支持对探查函数和修改函数的划分。与此类似,这里还直接支持建构函数、析构函数、复制操作和转换函数的概念。

23.4.3.3 步骤3:描述依赖性

精化有关的类,描述它们之间的依赖关系。第24.3节里讨论了各种各样的依赖关系。在设计环节中,特别应该考虑的是参数化、继承使用关系。这里的每种关系都涉及到有关一个类在为系统里某单一性质负责的意义方面的考虑。担负起明确的责任并不意味着这个类本身就要保存所有的数据,也不意味着它的成员函数需要直接完成所有必须的操作。与此相反,每个类只需保有责任中的一片领地,要保证这个类完成的大部分工作是通过直接请求“其他地方”,由另一些以有关的子工作为己任的类去处理。当然也要当心,这种技术的过度使用可能导致低效率和无法理解的设计,可能做出大量的类和对象,以至于它们什么都不做,只去产生瀑布式向前传送的服务请求。如果某件事情现在可以在这里做,就应该在这里完成。

在设计阶段(而不是在实现阶段)就需要考虑继承关系和使用关系,这一需求直接源于采用类来表示概念。它也意味着设计的单位应该是组件(23.4.324.4节),而不是类。

参数化----常常引向使用模板----是一种将隐含的依赖关系显式化的方法,这样可以同时表述几种选择,而不必增加新概念。经常有这样的选择:是把某种东西留作对上下文的依赖关系呢,是将它表述为继承树上的一个分支?还是采用一个参数(24.4.1节)。

23.4.3.4 步骤4:描述界面

描述界面。私用函数通常不必在设计阶段考虑。在设计阶段必须考虑的实现问题最好是作为关于依赖性的步骤2中的一部分进行处理。说得更强一些,我使用一条经验规则:对于一个类而言,除非它存在着至少两种有显著差异的实现方式,否则这里大概就有些问题。也就是说,这可能是一个伪装成类的具体实现,而不是某个真实概念的代表。在许多情况下,考虑对于一个类是否可能有某种形式的延迟求值,是回答下面问题的一种很好方式:“这个类的界面是与实现无关的吗?”

请注意,公用基类和友元也是一个类的界面的一部分,参看第11.5节和第24.4.2节。通过分别定义保护界面和公用界面,为继承和普通客户提供分离的界面,这种工作会有很好的回报。

正是在这个步骤中,应该考虑和描述产生出来的确切类型。这里的理想应该是使尽可能多的界面能利用应用层的类型静态确定。参看第24.2.3节和第24.4.2节。

在描述界面时,也应该为这些类向外看一看,是不是某些操作支持了多于一个的抽象层次。举个例子,类File的某些成员函数可能有类型为File_descriptor类的参数,它的另一些函数可能以表示文件名的字符串为参数。File_descriptor的操作与文件名操作并不在同一个抽象层次里,所以就应该怀疑它们是否应该在同一个类中。或许最好是有两个文件类,一个支持文件描述符的概念,另一个支持文件名的概念。在典型情况下,一个类里的所有操作都应该支持同一个抽象层次。如果它们不是这样的,那么就应该考虑重新组织这个类及其相关的类。

23.4.3.5 步骤5:类层次结构的重组

在步骤1和步骤3里,我们都检查了类和类层次结构,看它们是否适合我们的需要。典型情况是并不适合,因此我们就必须去重新组织,改进其结构,改进设计和/或实现。

最常见的对类层次结构的重组是将两个类中公共的部分提取出来,形成一个新的类,还有就是将一个类分裂为两个。在这两种情况下,结果都是三个类:一个基类和两个派生类。什么时候应该去做这种重组?什么情况能说明这种重组可能有价值呢?

不幸的是,对于这些问题都不存在简单而通行的回答。这当然不会令人感到意外,因为这里谈论的不是那种小的实现细节,而是修改一个系统的结构。最基本的----也很不容易----的操作就是去寻找类之间的共性,提取出它们共同的部分。有关共性的确切准则是无法定义的,但它应该是反应了系统中一些概念之间的共性,而不只是实现的方便。也存在一些关于两个或多个类具有可能提取出的共性的线索,如公共的使用模式,类似的操作集合,类似的实现,还有就是在设计讨论中这些类常常一起出现。在另一个方向上,说一个类可能是分裂为两个的候选者,如果这个类的操作的子集合具有差异显著的使用模式,或者这种子集合访问的总是表示之中不同的子集合,或者这个类常常出现在相互无关的讨论之中等等。有时,将一集相关的类做成一个模板,也是系统地提供一集必要功能的方式(24.4.1节)。

因为类和概念之间的紧密关系,与类层次结构组织有关的问题常常表现为类的命名问题,或者有关设计的讨论中对类名字的使用问题。如果在设计讨论中所用的类名字,或者由类层次结构所蕴涵的分类方式听起来就很别扭,那么这里很可能有改进类层次结构的可能性。注意,我说的意思也包括两个人分析类层次结构比一个人好得多。如果你正好找不到其他人一起讨论设计,那么可以写一个使用类名字的有关设计的交流性的描述,这也是一种很有帮助的做法。

设计中最重要的目标之一就是提供一个界面,使之能够在变化之上保持稳定(23.4.2节)。做到这一点的最好方式是将一个被许多类所依赖的类做成抽象类,其中只给出最一般的操作。最好将细节留给更特殊的派生类,因为直接依赖它们的类和函数都会少一些。应强调的是:依赖于某个类的类越多,这个类就应该越一般,它所揭示的细节也应该越少。

有一种很强的诱惑力,推动人们把更多的函数(和数据)放进被许多地方使用的类里。这常常被看作是一种使类更有用、更少需要(进一步)改变的方法。这种想法的效果就是做出带着肥大界面(24.4.3节),保存着由一些相互无关的函数使用的数据的类。而这又隐含着,只要这个类所支持的许多类之一需要有明显的变化,这个类本身也必须跟着修改。这一情况转而又使这些变化影响到许多并无关系的用户类和派生类。对于那种处于设计中心的类,我们不应该使它复杂化,而应该保持它的一般性和抽象性。在需要时,特定的功能应当通过派生类提供,参看 [Martin, 1995] 里的例子。

按照这种方式思考,将导致一种抽象类的层次结构,其中接近根部的是最一般的类,大部分其他类和函数都依赖于它们。在树叶处的是最特殊的类,只有很少的代码片段直接依赖于它们。作为例子,请考虑Ival_box层次结构(12.4.312.4.4节)的最后版本。

23.4.3.6 步骤6:模型的使用

在我准备写一篇文章时,总会试着去找一个可以参照的模型。也就是说,不是立即投入打字,而是去找有关类似论题的文章,看看能不能找到一篇东西,可以作为开始我的文章的模式。如果选定的模型就是自己原来写的针对相关论题的文章,我甚至可以让原文字的某些部分留在那里,根据需要修改其他部分,只在那些我希望传达的信息的逻辑需要的地方加入新信息。例如,这本书就是基于它的第一版和第二版写出来的。这种写作技术的一个极端是格式信件,对于那类情况,我只是简单地填入姓名,可能再加上几行,使信件“个性化”。从本质上看,我写这种信件的方式就是只描述与模型的差异。

这样为新的设计使用现存的系统作为模型,在所有创造性的活动里都是正常情况,而不是例外。只要可能,就应该在前面工作的基础上设计和编程。这样做限制了设计师必须处理的问题的自由度,使人一下就可以将注意力集中到不多的几个问题上。让某个重要项目“完全从空白开始”可能是令人兴奋的,然而,一个更精确的描述常常是“醉人的”,结果是在不同设计选择间的一种醉鬼式的徘徊。有一个模型并不是限制,也并不要求奴隶式的追随这个模型,它只是使设计师可以比较自由地一次考虑系统的一个方面。

请注意,模型的使用是无价至宝,因为任何设计都是其设计师经验的集成。采用一个明确的模型,将使模型的选择成为一项有意识的决策,使许多假设明确化,定义了一集公共词汇,为设计提供了一个初始框架,并增加了设计师们采用公共途径的可能性。

很自然,初始模型的选择本身就是一个重要的设计决策,常常只有在查询了许多潜在模型,评价了各种替代方式之后才能决定下来。进一步说,在许多情况下,只有在理解了需要做出的主要修改,使模型适应了特定新应用的想法之时,才能说一个模型是合适的。软件设计很困难,我们需要取得所有可能的帮助,我们不应该拒绝使用模型,不应以这种方式避免“模仿”的蔑视语,那完全是用错了地方。模仿是最真挚的恭维形式,而利用模型和以前的工作作为启迪则是----在礼节和版权法律的范围之内----在任何领域中开展创新性工作的一种可以接受的技术:对莎士比亚足够好的东西对我们也一样。有些人把在设计中利用模型称为“设计的重用”。

将在许多设计中出现的一般性的元素记于文档,带上某些有关它们所解决的设计问题,以及有关使用它们的条件的描述,这是一个很明显的想法----至少你想到它之后是如此。词语模式常被用于描述这种一般性的有用的设计元素,存在着一些有关各种模式及其使用的文献(例如,[Gamma, 1994] [Coplien, 1995])。

作为设计师,去了解在某个特定应用领域中的流行模式,这也是一个很好的想法。作为程序员,我喜欢那些带着一些作为具体例子的代码的模式。与大部分人一样,在我有了具体例子(在这里是阐释模式使用的一段代码)的帮助之后,我才能更好地理解一种一般性的思想。广泛大量使用模式的人们有一套特殊词汇,使他们之间的交流更容易进行。不幸的是,这也有可能变成一种私密性的语言,有效地防止外人理解。与往常一样,保证参与一个项目中不同部分的人之间的有效通讯是至关重要的(23.3节),对于大的设计和编程社团而言也是如此。

每个成功的大型系统都是某个更小一些的正在工作的系统的重新设计。我不知道有任何事实居于这一规律之外。我能想到的最接近那类情况的东西都是一些失败的项目,一些花掉大笔开支而一塌糊涂许多年的项目,还有一些在其预定结束日期的多年之后才成功的项目。这种项目都是无意识地----常常也是不公开承认地----首先简单地做出一个不能工作的系统,而后转到一个能工作的系统,最后又经过重新设计,使之成为了一个接近初始目标的系统。这也意味着,动手从空白开始构造一个大型系统是个傻念头,正好符合最后原理。作为我们目标的系统越大、越雄心勃勃,找一个模型,从它开始工作也就越重要。对于一个大型系统而言,能实际接受的模型只有那些在某种意义上小一些的、与之相关的正在工作中的系统。

23.4.4 试验和分析

在开始一个雄心勃勃的开发项目时,我们并不知道构造这个项目的最佳方式。经常是,我们甚至不能确切地知道这个系统需要做什么,因为只有通过构造、调试和使用这个系统的努力,各种特殊情况才能变得更清晰。那么----在还没有构造出完整的系统之前----我们怎么才能取得一些信息,以设法理解哪些设计决策最为重要?怎样去估计它们所蕴涵的后果呢?

我们通过试验。还有,我们在有了一些可以分析的东西之后,立即去分析有关的设计和实现。最经常也最重要的是,我们需要讨论各种设计和实现可能性。除了极少的例外,设计都是一种社会性活动,设计在报告和讨论中逐步发展。黑板经常就是最重要的设计工具,没有它,有关设计的萌芽式概念就不能发展,不能在设计师和程序员之间共享。

最流行的试验形式看来是构造出一个原型,也就是系统的一个削减了规模的版本,或者是系统的一部分。对于原型,没有严格的性能标准,机器和程序设计环境资源通常也比较富裕,参与的设计师和程序员也都有很不平常的完好教育、经验和工作动力。这里的想法就是尽可能快地得到一个可以运行的系统版本,以便能探索各种设计和实现选择。

如果做得好,这种途径可以很成功。但它也可以成为懒散的借口。问题在于,原型的重点很容易从“探索各种设计和实现选择”转移到“尽可能快地得到某种能运行的系统”。这又很容易引向不关心原型的内部结构(“不管怎么说,这不过是个原型罢了”),并轻视那些围绕着原型实现,使之更有用的设计工作。一个暗礁是,这样一个实现很可能堕落为最坏的一类吞噬资源的饿魔,在给出“几乎完全的”系统幻像的同时,带来的是维护的噩梦。按照定义,原型几乎不必有内部结构、效率,不必具有使之能扩展为实际使用规模的能保持下去的基础结构。因此,一个“几乎是产品”的“原型”将吸引走许多时间和资源,这些最好还是用到别的地方。对开发者和管理者的诱惑是将原型做成产品,并把“性能工程”留到下一次发布。如果按这种方式误用的话,原型就会违背设计中所追求的所有东西。

与此相关的问题是原型的开发者可能爱上他们的工具,以至忘记了作为产品的系统未必能负担起他们(所需要)那些便利条件的成本。由他们的小研究组提供的在约束条件和形式化方面的自由度,在一个面对着相互锁定的截止日期的更大开发组中也很难维持。

另一方面,原型也可以极有价值。考虑一个用户界面的设计。在这种情况下,系统中不直接与用户交互部分的内部结构常常是无关紧要的,也没有其他可行方式能得到用户对系统观感的反应方面的经验。另一个实例是完全为研究一个系统的内部网络所设计的原型。在这里,用户界面非常简单,很可能采用模拟的用户,而不用真实用户。

做原型是一种做试验的方式。从构造原型中最希望得到的结果是在构造它的过程中获得的洞察力,而不是原型本身。对一个原型的最重要评判标准是,它应该是如此的不完全,是一个明显的试验性的媒介,不经过大范围的重新设计和重新实现就不可能转为产品。让原型“不完全”有助于将注意力集中在试验,也使原型变成产品的危险性减到最小。这样也使另一种诱惑减到最小程度,那就是试图将产品设计的基础尽可能地靠近原型设计----这样也会忘记或者轻视了原型的内在局限性。在利用之后,就应该将原型丢掉。

应该记住,在许多情况下,存在着许多可以代替原型的试验技术。在能够使用它们时,最好是采用它们,因为它们更严格得多,而且对设计师的时间要求、对系统的资源要求都更少。这方面的例子如数学模型和各种形式的模拟器。实际上可以看到一种连续系,从数学模型,穿过牵涉到越来越多细节的模拟器,再到原型,到部分实现,直至完整的系统。

这一情况也会引起了一种想法:让系统从一个初始设计和实现开始成长,通过一系列的重新设计和重新实现。这是一种很理想的策略,但它可能对于设计和实现工具提出极高的要求。还有,这种途径也有一种危险,它可能受到反应了初始设计决策的大量代码的拖累,以至更好的设计根本无法实现。至少在目前,这一策略看来还只限于用在小型到中型的项目,在其中不大会出现对整体设计的重要修改;还有就是在系统的初始发布之后的重新设计和重新实现,因为在那里,这样的策略是不可避免的。

除了通过试验性设计提供有关各种设计选择的洞察力之外,对设计和/或实现本身的分析也是获取深入认识的重要源泉。举例来说,研究类之间的各种依赖关系(24.3节)可能极有帮助,传统的实现工具,如调用图、性能实测等等,也都不应小看。

注意,规范(分析阶段的输出)和设计也像实现一样,存在着许多错误。事实上,它们中的错误可能更多,因为它们更不具体,其描述常常更不精确,不能执行,在典型情况下也没有足够精妙的工具的支持,至少不像在对实现进行检查和分析时可用的工具那么好。增强在表述设计时所用的语言或者记法的规范性,可以向着利用工具帮助设计师的方向有所前进。但是在这样做时,绝不应削弱了实现中所用的程序设计语言(24.3.1节)。还有,一种形式化记法本身也会成为复杂性和问题的根源。出现这种情况的例子如:当某种形式记法并不适合它所面对的实际问题时;当形式化的严格性超过了参与工作的设计师和程序员的数学背景和熟练程度时;还有,在一个系统的形式化描述与它假定应该描述的系统脱节的时候。

设计从本质上说就是一种很容易出错的,难于用有效工具支持的活动。这些都使经验和反馈成为不可或缺的。因此,将软件开发看成是一种线性的过程,从分析开始到调试结束,这种看法存在着根本性的缺陷。需要强调反复进行的设计和实现,以便从开发的各个不同阶段所取得的经验中获得反馈。

23.4.5 调试

没有完成调试的程序不能工作。设计和/或验证一个程序,使之能第一次就工作的理想是难以实现的,除了对最简单的程序之外。我们应该向着这个理想努力,但我们绝不应愚蠢到认为调试很容易。

“怎样调试?”这又是一个无法给出一般性答案的问题。然而,“何时调试”则有一个一般性的回答:尽可能早,尽可能经常去做。调试策略应该作为设计和实现工作的一部分产生出来,或者至少应该与它们平行地开发。一旦有了一个能够运行的系统,就应该开始调试。将认真地调试推迟到“整个实现完成之后”,就是要求推翻计划安排并/或发布有缺陷的产品。

在所有可能之处,都应该特别考虑如何设计系统,使调试工作相对而言比较容易进行。特别是常常应该把为调试服务的机制直接设计到系统里。有时没有这样做,是因为害怕造成代价高昂的运行时检查,或者担心为一致性检查所需的冗余可能形成急剧扩大的数据结构。这种担心通常是放错了地方,因为大部分调试代码和冗余,只要必要,都可以在系统发布之前从代码中剥掉。在这方面,断言(24.3.7.2节)有时很有用处。

与特定的调试相比,更重要的是一种思想,系统的结构应该使我们有一种合理的机会,去使我们和我们的用户/顾客确信,我们能通过静态检查、静态分析和调试的组合清除掉各种错误。在开发出了某种容错策略的地方(14.9节),也应该将调试策略设计为一种补充,并使之尽可能与整体设计中的有关方面靠近。

确定需要做多少调试常常也非常困难。当然,最常见的毛病是调试不足,而不是调试过度。在与设计和实现相比,到底需要将多少资源分配给调试,这个问题很自然地依赖于系统的性质和构造它所用的方法。不过,作为一条经验规则,我要建议在时间、工作量和智力方面,分配给系统调试的资源应该多于构造其初始实现的资源。调试应当集中于那些可能造成灾难性后果的问题,以及那些可能频繁发生的问题。

23.4.6 软件维护

“软件维护”是一种用词不当。“维护”这个词会引起一种与硬件类似的误解。软件不需要加油,没有会磨损的运动部件,不存在裂隙使水可以汇集其中而引起锈蚀。软件可以一模一样地复制,在分秒之间传送过很长的距离。软件不是硬件。

在软件维护名目下的活动实际上是重新设计和重新实现,因此属于普通的程序开发循环。在设计中强调灵活性、可扩充性和可移植性之时,也就是直接针对传统上软件维护所要解决的问题。

与调试一样,维护也不能是一种事后的思考,不能是一种游离于开发主流之外的活动。特别重要的是,项目开发的人员组织应该具有某种连续性,将维护问题成功地传给新的(通常更缺乏经验的)一组人是非常困难的,如果在他们与原设计师和程序员之间不存在某种联系的话。如果必须有很大的人员变动,那么就需要强调向新人们转移有关系统结构和系统目标的理解。如果将“维护组”放在某种位置,让他们去揣测系统的体系结构,或者仅仅根据实现去推断系统部件的用途,那么,在局部补丁的不断冲击之下,系统的结构很快就会恶化。典型的文档往往更注重谈论细节,而不是帮助新人理解关键性的思想和原理。

23.4.7效率

Donald Knuth认为“不成熟的优化是一切罪恶之源”。有些人在这方面学得实在太好了,以至于将一切对效率的关心都看作是罪恶。与此相反,在整个设计和实现工作中,都应该将效率问题放在心上。当然,这并不意味着设计者应该关心细微的效率问题,但一级的效率问题是必须考虑的。

处理效率问题的最佳策略就是产生出一个清晰简单的设计。只有有了这样的设计,才能在项目的整个生存期间,既能够作为性能调整的基础,同时又保持相对的稳定性。避免庞大化极其重要,这种情况是大型项目的黑死病。人们过分经常地加入“正好需要”的特征(23.4.3.223.5.3节),为支持这种虚饰,最终导致两倍或者四倍的系统规模和运行时间。更糟糕的是,常常很难对这种过分费神做出的系统做分析,因为难以区分那些不可避免的开销和可能避免的开销。这样,甚至基本的分析和优化也无法进行。优化应该是细致分析和性能实测的结果,而不能随机地摆弄代码。特别是在大型系统里,设计师或者程序员的“直觉”对于指导与性能有关的事项而言都是极不可靠的。

最重要的是避免那些在本质上就是低效的结构,以及那些需要花费过多的时间和聪明才智,才可能将其优化到可以接受的性能水平的结构。类似的,尽量少用那种具有内在的不可移植的结构和工具也非常重要,因为使用这种结构也就是宣告该项目将要运行在老的(性能较差且/或更昂贵的)计算机上。

23.5 管理

只要有一点点能说得通,大部分人都会去做他们被要求去做的事情。特别的,如果在一项软件项目的环境里,按照某种方式工作你会得到奖赏,否则将受到惩罚,那么只有最出色的设计师和程序员,才会面对着管理层的反对、冷漠和官僚作风,冒着职业风险去做他们认为正确的东西。这也就意味着,一个组织应该有一种与它所陈述的设计与编程目标相匹配的回报结构。然而,并非如此的情况太常见了:程序设计风格的重要改变只能通过相应的设计风格的改变,而这两者通常又要求管理风格的改变才能生效。思想和组织的惯性都太容易导致这样的情况,局部的改变无法得到使其成功所需要的全局性改变的支持。一个相当典型的例子就是转到某种支持面向对象程序设计的语言,例如C++,但却没有与此同时改变相适应的设计策略,去利用这种语言的功能(另见24.2节)。另一个例子是改为“面向对象的设计”,但却不引进语言去支持它。

23.5.1 重用

代码和设计的重用经常被用于作为采纳某种新程序设计语言或设计策略的原因。然而,许多组织却奖励那些选择去重新发明车轮的个人和小组。举例来说,程序员的生产能力可能是按照代码行数评价的,他会愿意付出收入以及可能的地位代价,去写基于标准库的小程序吗?管理员的报酬可能与她的小组里的人数有某种比例关系,在她可能在自己的组里雇佣另外一些人手的情况下,她会去采用其他小组的软件产品吗?一个公司可能获得了一项政府合同,在其利润与开发费用之间有着固定的百分比,公司会考虑去使用最有效的开发工具,以便使自己的利润最小化吗?奖励重用非常困难,但是,除非管理层能够找到一些方式去鼓励和奖赏重用,否则它就不会发生。

重用是一种社会现象,我可以利用其他人的软件,要求是:

  1. 它能工作:为了能重用,软件必须首先是可用的。
  2. 它可以理解:程序结构、注释、文档和教学材料都非常重要。
  3. 它能与并不是为了与之共存而专门写出的软件共存。
  4. 它有支持(或者我愿意自己支持自己。一般情况下并不是这样)。
  5. 它是经济的(我能与其他用户共同分担开发和维护费用吗?)。
  6. 我能够找到它。
在此之上,我们还可以加上,在有人已经“重用”了某个部件之前,它都不能算是可重用的。将部件纳入其环境的工作通常要求去精化其操作,推广其行为,改进其功能,以便与其他软件共存。直到这种事情至少已经做过一次之后,我们才能说它可以重用了,因为,即使是极其小心地设计和实现出的部件通常也有一些没想到的不希望有的粗糙角落。

我的经验是,使重用得以存在的必备条件是有人将它作为自己的事情,去为这种共享而工作。在一个小组里,这通常意味着有某个个人,无论是由于指定还是由于偶然,变成了公共库和文档的管理人。在更大的组织中,这意味着要有一个小组或者部门专门去收集、构造、写文档、推广和维护其他许多小组所使用的软件。

这样一个“标准部件”小组的重要性无论怎样估计都不过分。注意,作为一级近似,一个系统反应了生产它的组织的情况。如果一个组织中没有某种机制去推动和奖励合作与共享,那么合作与共享必定是罕见的。标准组件组必须自动地去选出自己的组件,这也意味着好的传统文档是必需品,但这还不够。在此之外,这个组件小组还必须提供教材和其他信息,使潜在的用户能够找到某个组件,并理解为什么它可能有用。这也意味着组件小组必须着手去做一些在传统上与市场推介和教育相关联的活动。

任何时候只要可能,这个组的成员都应该与构造应用的人们密切合作。只有这样,他们才能充分理解用户的需求,察觉到在不同应用之间共享组件的可能性。这也表明了这种组织方式有一种咨询作用,表明可以利用这种关系将信息传入或者传出这个组件小组。

这种“组件小组”的成功需要依据其客户的成功情况进行评价。如果只是简单去评价它说服开发组织接受的工具和服务量,这种小组就会堕落为一种叫卖商品软件的小贩,仅仅是去鼓吹不断变化的时尚。

并不是所有代码都需要重用,可重用性也不是一种具有普遍性的特征。说一个部件是“可重用的”,就意味着它可以在某个确定的框架里,要求做较少工作或者无需任何工作就可以重用。在大部分情况下,转移到不同的框架里需要做大量工作。在这一方面,重用的情况很像可移植性。最重要的是应注意到,重用是将设计目标定位于重用,基于经验去精化部件,以及有意搜寻可能重用的现存部件而取得的结果。重用不会从漫不经心地使用某些特定语言特征或者编码技术中魔术般地冒出来。C++ 的许多特征,如类、虚函数和模板,都使我们可以适当地表述设计,使重用变得更容易些(并因此更可能出现),但是这些特征本身并不保证可重用性。

23.5.2 规模

个人或者组织都很容易因为“正确行事”而感到兴奋。在机构的意义下,这常常可以翻译为“取得进展并且严格按照正确程序进行着”。在这两种情况下,在真诚的、常常是强烈的改进做事方式的愿望中,最早的牺牲品可能就是常识。不幸的是,一旦丢掉了常识,可能在不知不觉中做出的坏事也就没有了限度。

考虑在第23.4节列出的开发过程中的各个阶段,以及在第23.4.3节列出的设计步骤中的各个阶段。不难进一步将这些阶段加工成一种更完善的设计方法,对其中的每个阶段都给以更精确的定义,带有定义良好的输入和输出,并采用某种半形式化的记法去表述输入和输出。可以开发出一些核查表来保证遵守这种设计方法,开发出一些工具,以便强制性地实施过程性的和记法性的规范。再进一步,查看在第24.3节所展示的分类方式和依赖关系,人们也可以做出判决,说某些依赖关系就是好的而其他则是坏的,并提供一些分析工具,保证这种价值取向能在整个项目中统一地实施。为使这种“严紧的”软件开发过程更加完善,人们还可以定义文档的标准(包括拼写规则、语法和打字规范),代码的一般表述标准(包括有关允许或不允许使用的语言特征的规范,有关允许或不允许使用哪些种类的库的规范,有关代码缩排,函数、变量和类型命名的规定等)。

这些中的大部分对于一个项目的成功都可能有帮助。至少说,如果要着手一个系统的设计,该系统最终可能包含上千万行的代码,由数百个人参与开发,数千人在十年或更长的时期中维护和支持它,如果没有类似上面所描述的这样有着良好定义的有点严格的框架,那就是一件愚蠢的事情。

幸运的是,大部分系统并不属于这样一类东西。无论如何,一旦接受了这种思想,认为这样一种设计方法,或者坚持这样一集编码和文档标准就是“正确方式”,强制性地要求将它应用到所有的地方和每一个细节,这样做,对于小项目就可能造成很荒唐的束缚和额外开销。特别是它可能导致以倒腾纸片填写表格作为衡量进展和成功的标准,以这些取代生产性的工作。如果出现这种情况,设计师和程序员将会离开这种项目,取而代之的则是一些官僚。

一旦某种(原本完全合理的)设计方法的滑稽可笑的误用在某个社团中出现,其失败又会成为在开发过程中避免所有规范方式的口实,这将很自然地带来一类混乱和失败,而设计出这种设计方法,原来就是为了防止这些情况出现。

实际问题是,应当为特定项目的开发选择适当的规范性水平。不能期望对此问题有一个简单回答。所有的方法基本上都能对付小的项目。更糟糕的是,看起来差不多每种方法也都能用于大型项目----无论它如何病态地鼓励奇想,或者对所涉及的人有多么严酷----只要你愿意将大量的时间和金钱投入到这个问题中。

在每个软件项目中,最关键的问题就是如何维持设计的完整性。这一问题与规模增长的关系比线性增长更快些。只可能有一个个人或者一小组人能够抓住并保持着对一个重要项目的整体目标的认识。大部分人必须将他们大量的时间用在子项目、技术细节、日常管理等等上面,所以就很容易忘记总体设计目标,或更重视自己的局部和当前目标。不让每个个人或小组都有明确的任务去维护设计的完整性是一种导向失败的方法。不让每个个人或小组在作为整体的项目上付诸努力也是一种导向失败的方法。

对于一个项目或一个组织而言,缺乏一种长远目标比缺少某种孤立性质的危害性大得多。应该有一小群人去做这种工作,形成这样一种整体目标,将这一目标牢记在心,写出关键性的整体设计文档,写出对关键性概念的介绍,并一般性地帮助其他人将这一目标牢记在心里。

23.5.3 个人

采用如这里所描述的设计,将给熟练的设计师和程序员送上了一份超值礼券。同时,它也会使设计师和程序员的选择成为一个组织成功的最关键因素。

管理者常常忘记组织是由个人组成的,有一种流行的概念说,程序员都是一样的,可以互换的。这纯属谬误,它可以通过逐走大量最有成效的个人,并判决留下的人们应该在某种低于他们潜能的水平上工作,从而毁灭一个组织。说个人可以互换的,条件就是不允许他们利用自己高于为完成工作所需的最低要求的那一部分能力。所以,虚构的互换性是非人性的,具有内在的非经济性。

大部分程序设计能力的评价方式都鼓励不经济的实践活动,没有考虑到关键性个人的贡献。最明显的例子是,用生产的代码行数、生产的文档页面数、通过的调试个数等评价进展,这些都采用得相当广泛。这种数字在管理层的图表上看起来很漂亮,但它与现实间的关系却是最贫乏无力的。举例来说,如果用生产出的代码行数来衡量生产率,重用的成功实施将表现为对程序员功效的一种否定。在重新设计软件的重要部分时,最佳原则的成功实施也会带来与此类似的影响。

评价工作产出的质量远比衡量产出的数量困难得多,然而,无论个人还是小组,都应该依据其工作质量而不是粗糙的数量测度给予回报。不幸的是,实用的质量评价体系的设计----按照我的了解----很难开始。此外,不能完全描述项目状态的度量方式将束缚发展。人们将调整自己去适应局部的时限,并按照度量方式的定义去优化个人和小组的功效。作为一种直接结果,将使整个系统的完整性和性能都受到损害。例如,如果用删除的程序错误数或者已知遗留错误数来定义时限,我们就会看到,为满足这种时限,付出的代价是运行性能或者运行这个系统所需要的硬件资源。相反,如果只衡量运行性能,在开发者们针对标准测试优化性能的战斗中,错误率必然会上升。缺乏好的容易理解的质量评价体系,对管理者提出了极强的技术专门知识方面的要求,而替代这些的只能是奖赏随机性行为而非进展的系统倾向性。不要忘记管理者也是人,管理者至少需要在新技术方面受到与他们所管理的那些人同样的教育。

与软件开发的其他领域一样,我们必须有更长远的考虑。基于一年的工作去评价一个个人的成效,从本质上说就是不可能的。当然,大部分个人都有一致的长期记录,它可以作为技术评价的可靠预测器,对于评价其刚刚过去的工作也很有帮助。无视这种记录----在将个人仅仅看作在组织的车轮上可以互换的齿轮时正是如此----必将使管理者处于误导性的数量评价方式的可怜境地。

采取长期观点,避免“管理可互换的白痴学校”的一个推论就是,每个个人(开发者和管理者)都需要较长时间,才能成长到可以进入需要更高程度的技能的更有趣的工作中。这是对“履历发展”的跳槽和职位轮换的否定。关键性技术人员和关键性管理人员的低替换率必须成为一个目标,如果没有与关键性设计师和程序员的友好相处,没有最新的相关技术知识,管理者将不可能成功。在另一方面,如果没有胜任的管理者的支持,没有工作于其中的更大的非技术环境的最小程度的理解,设计师和程序员的小组也不可能成功。

在需要创新的地方,资深技术人员、分析师、设计师、程序员等等在引进新技术中扮演着最关键的,也是非常困难的角色。正是这些人必须学习新技术,在许多情况下还需要抛弃老的习惯。这不是很容易的事情。这些个人通常都已经在采用老方式做事情方面付诸了大量的个人投入,也由于采用这些方式工作的成功而获得了在技术方面的声望。许多技术管理人员也是如此。

很自然,在这些个人中常有一种对转变的恐惧。这可能引起高估改变所涉及的各种问题,以及不愿意去正视采用老方式做事引起的问题。同样也很自然,为转变而呼喊的人们则倾向于高估新的做事方式的有利效用,低估由于改变而带来的问题。这两组个人必须交流,他们必须学会用同一种语言谈话,他们必须互相帮助去塑造出一个转变的模型。代替这种方式的一定是组织的瘫痪,以及两组人中最有能力者的流失。这两组人都应该记住,最成功的“老顽固”常常就是昨天的“年轻斗士”。有了并不蒙羞的学习机会,更有经验的程序员和设计师将能够成为转变的最成功、最有远见的拥护者。他们的健康的怀疑态度,有关用户的知识,对于组织性阻碍的熟识都是极其宝贵的。立即与彻底转变的拥护者也必须认识到,这样一个转变常常涉及到逐步地采纳新技术,过犹不及。相反的,那些根本不希望转变的人们应该另找一个不需要变化的领域,而不是在一个新的需求早已显著地改变了成功条件的领域里,打一场气急败坏的后卫战。

23.5.3 混成设计

将做事情的新方法引入一个组织也可能是很痛苦的,该组织和组织中个人的分裂可能很明显。特别是,一夜之间的突然变化,将“老门派”中最有生产力的熟练成员变成“新门派”中最低效的新手,这通常是无法接受的。当然,不改变很难获得最大的收获,而重要的转变通常也有风险。

C++ 的设计就是希望将这种风险减少到最小,采用的方式是允许逐步采纳各种技术。虽然事情很清楚,要通过C++ 获取最大的利益,需要通过数据抽象、面向对象的程序设计和面向对象的设计。但不清楚的是,通过与过去彻底决裂是否能够最快地得到这些利益。这种清晰的决裂很少能够实行。更经常的是,对于进步的追求需要----而且应该----与有关如何控制这种转变的忧虑相调和。应考虑到:

- 设计师和程序员需要时间去获得新的技能。

- 新的代码需要与老代码合作。

- 老的代码需要维护(通常是无穷无尽的)。

- 在现存设计和程序上的工作需要(按时)完成。

- 需要将支持新技术的工具引进局部环境里。

这些因素会很自然地导致一种混成形式的设计----即使是在一些设计师的本意并非如此行事的地方。人们很容易低估前面两点。

C++通过支持多种程序设计风范的方式,以多种方式支持逐步引入组织中的观念:

- 程序员可以在学习C++ 的同时保持其生产能力。

C++ 可以在缺乏工具的环境中产生显著效益。

C++ 的程序片段可以与在C或者其他传统语言中写出的代码合作。

C++ 有一个很大的与C兼容的子集。

这里的想法是,程序员可以把从传统语言移向C++的转变过程规划为,首先在采纳C++ 的同时保持传统的(过程式)程序设计风格,而后再使用数据抽象技术。最后,在已经掌握了这个语言及其相关的工具时,他们再转向面向对象的程序设计和通用型程序设计。注意,经过良好设计的库很容易使用,虽然它们的设计和实现要困难得多。因此,新手们在这一进步的早期阶段就可以从抽象机制的更高级应用中获益。

分步学习面向对象的设计,面向对象的程序设计,和C++的思想得到了C++中有关机制的支持,这些机制使C++代码能与采用不支持C++数据抽象和面向对象程序设计概念的语言写出的代码混在一起(24.2.1节)。可以让许多界面保持为过程式的,因为将任何事情搞得更复杂不会得到即时的利益。对于许多关键性的库而言,这些都已经由库的提供者完成了,所以程序员仍可以停在那里,忽略真正的实现语言。采用由C一类语言写出的库是在C++里最早的,也是在初期最重要的重用方式。

下一阶段----只在那些实际需要更精致的技术的地方----就是将用C或者Fortran一类语言写出的概念用类的方式提供,将数据结构和函数封装到C++的界面类中。第11.12节中的字符串类就是将语义从过程加数据结构的层次提升到抽象数据结构的层次的一个简单实例。在那里,采用对C字符串表示和标准C字符串函数的封装,产生出一个字符串类型,它的使用就大大简化了。

类似技术可以用于将内部的或者单独的类型装入类层次结构中(23.5.1节)。这能使在为C++所做的涉及到数据抽象和类层次结构的设计中也可以出现用其他语言写的代码,而那些语言里没有上述概念,甚至存在限制,要求结果代码必须能从过程式语言里调用。

23.6 带标注的参考文献

本章只是描绘了有关程序设计项目的设计问题和管理问题的表面情况。由于这一原因,在这里提供了一个很短的带标注的参考文献表。在 [Booch, 1994] 中可以找到一个更广泛的带标注的参考文献表。

[Anderson, 1990] Bruce AndersonSanjiv GossainAn Iterative Model for Reusable Object-Oriented Software. Proc. OOPSLA'90. Ottawa, Canada。描述了重复式设计和重新设计模型,附带一个特殊实例和有关经验的讨论。

[Booch, 1994] Grady BoochObject-Oriented Analysis and Design with Application. Benjamin/Cummings. 1994. ISBN 0-8053-5340-2。详尽地描述了设计,一种特殊的带有图形记法形式的设计方法,若干个在C++里表述的大型设计实例。该书更深入地讨论了本章中的许多问题。

[Booch, 1996] Grady BoochObject Solutions. Benjamin/Cummings. 1996. ISBN 0-8053-0594-7。从管理的角度描述了面向对象系统的开发。包含范围广泛的C++代码实例。

[Brooks, 1982] Fred BrroksThe Mythical Man Month. Addison-Wesley. 1982。每个人每隔几年就应该再读一次本书。反对狂妄自大的训诫。在技术材料方面已经有点过时了,但在有关个人、组织和规模方面的内容则完全不过时。在1997年重印,ISBN 1-201-83595-9

[Brooks, 1987] Fred BrroksNo Silver Bullet. IEEE Computer, Vol.20, No. 4. April 1987。有关大规模软件开发方法的一个综述,带有我们最需要的反对相信万能灵药(“Silver Bullet”,银弹)的训诫。

[Copien, 1995] James O. CopienDouglas C. Schmidt(编辑):Pattern Languages of Program Design. Addison-Wesley. 1995. ISBN 1-201-60734-4

[DeMarco, 1987] T. DeMarcoT. ListerPeopleware. Dorset House Publishing Co. 1987。少有的几本集中关注人在软件生产中的作用的著作。每个管理者的必读书。美妙得足以作为床头读物。是对许多愚蠢行为的解药。

[Gamma, 1994] Eric Gamma等:Design Patterns. Addison-Wesley. 1994. ISBN 0-201-63361-2。有关创建灵活的可重用软件的技术的分类目录,带有一个非平凡的、有着很好解释的实例。包含广泛的C++代码实例。

[Jacobson, 1992] Ivar Jacobson等:Object-Oriented Software Engineering. Addison-Wesley. 1992. ISBN 0-201-54435-0。透彻而实际地描述了在产业环境中的软件开发,特别强调用例(23.4.3.1节)。对C++做了很不合适的描述,就像它还是10年之前的样子。

[Kerr, 1987] Ron KerrA Materialistic View of the Software "Engineering" Analogy. In SIGPLAN Notices, March 1987。本章和随后几章中所使用的类比在很大程度上应归功于这篇文章中的观点及其报告,以及此前与Ron的讨论。

[Liskov, 1987] Barbara LiskovData Abstraction and Hierarchy. Proc. OOPSLA'87 (Addendum). Orlando, Florida。讨论了继承的使用可能损害数据抽象。注意,C++有特殊的语言支持以帮助避免这里所提出的问题(24.3.4节)。

[Martin, 1995] Robert C. MartinDesigning Object-Oriented C++ Applications Using the Booch Method. Prentice Hall. 1995. ISBN 0-13-203837-4。描述了怎样从问题出发,沿着一条相当系统化的路走到C++代码。比大部分有关设计的书籍更实际也更具体。包含广泛的C++代码实例。

[Meyer, 1988] Bertrand MeyerObject-Oriented Software Construction. Prentice Hall. 1988。第1-64323-334页很好地介绍了面向对象编程和设计的一种观点,带有许多有效的实际建议。本书的其他部分描述了Eiffel语言。存在着混淆Eiffel和一般性原理的倾向。

[Parkinson, 1957] C. N. ParkinsonParkinson's Law and other Studies on Administration. Houghton Mifflin. Boston. 1957。有关管理过程可能导致的灾难的一个最好笑、最尖锐的描述。

[Shlaer, 1988] S. ShlaerS. J. MellorObject-Oriented Systems Analysis and Object Lifecycles. Yourdon Press. ISBN 0-13-629023-X0-13-629940-7。提出了一种有关分析、设计和编程的观点,与我们这里所提的很不一样。有关讨论嵌在C++里,在这样做时所用的一套词汇使它听起来与这里的讨论相象得多。

[Snyder, 1986] Alan SnyderEncapsulation and Inheritance in Object-Oriented Programming Languages. Proc. OOPSLA'87. Portland, Oregon。可能是第一篇最好的有关封装和继承间相互关系的描述。还包括对多重继承中几个概念的很好的讨论。

[Wirfs-Brock, 1990] Rebecca Wirfs-Brock, Brain WilkersonLauren WienerDesigning Object-Oriented Software. Prentice Hall. 1990。描述了一种拟人式设计方法,基于使用CDCClasses, Responsibilities and Collaboration,类、责任和合作)卡片的角色扮演。其正文,如果不是方法本身的话,是基于Smalltalk的。

  23.7 忠告
  1. 理解你试图达到什么目的;23.3节。
  2. 心中牢记软件开发是一项人的活动;23.223.5.3节。
  3. 用类比来证明是有意的欺骗;23.2节。
  4. 保持一个特定的实实在在的目标;23.4节。
  5. 不要试图用技术方式去解决社会问题;23.4节。
  6. 在设计和对待人方面都应该有长期考虑;23.4.123.5.3节。
  7. 对于什么程序在编码之前先行设计是有意义的,在程序规模上并没有下限;23.2节。
  8. 设计过程应有利于反馈;23.4节。
  9. 不要将做事情都当作取得了进展;23.323.4节。
  10. 不要推广到超出了需要、你已有的直接经验和已经测试过的东西;23.4.123.4.2节。
  11. 将概念表述为类;23.4.223.4.3.1节。
  12. 系统里也存在一些不应该用类表述的性质;23.4.3.1节。
  13. 将概念间的层次关系用类层次结构表示;
  14. 主动到应用和实现中去寻找概念间的共性,将由此得到的一般性概念表示为基类;23.4.3.123.4.3.5节。
  15. 在其他领域中的分类方式未必适合作为应用中的继承模型的分类方式;23.4.3.1节。
  16. 基于行为和不变关系设计类层次结构;23.4.3.123.4.3.523.4.3.7.1节。
  17. 考虑用例;23.4.3.1节。
  18. 考虑使用CRC卡片;23.4.3.1节。
  19. 用现存系统作为模型、灵感的源泉和出发点;23.4.3.6节。
  20. 意识到视觉图形工程的重要性;23.4.3.1节。
  21. 在原型成为负担时就抛弃它;23.4.4节。
  22. 为变化而设计,将注意力集中到灵活性、可扩充性、可移植性和重用;23.4.2节。
  23. 将注意力集中到组件设计;23.4.3节。
  24. 让每个界面代表在一个抽象层次中的一个概念;23.4.3.1节。
  25. 面向变化进行设计,以求得稳定性;23.4.2节。
  26. 通过将广泛频繁使用的界面做得最小、最一般和抽象,使设计稳定;23.4.3.223.4.3.5节。
  27. 保持尽可能小,不为“特殊需要”增加新特征;23.4.3.2节。
  28. 总考虑类的其他表示方式,如果不可能有其他方式,这个类可能就没有代表某个清晰的概念;23.4.3.4节。
  29. 反复修整、精化设计和实现;23.423.4.3节。
  30. 使用可以用于调试,用于分析问题、设计和实现的最好工具;23.323.4.123.4.4节。
  31. 尽早、尽可能频繁地进行试验、分析和调试;23.4.423.4.5节。
  32. 不要忘记效率;23.4.7节。
  33. 保持某种适合项目规模的规范性水平;23.5.2节。
  34. 保证有人负责项目的整体设计;23.5.2节。
  35. 为可重用部件做文档、推介和提供支持;23.5.1节。
  36. 将目标与细节一起写进文档里;23.4.6节。
  37. 将为新开发者提供的教学材料作为文档的一部分;23.4.6节。
  38. 鼓励为重用设计、库和类,并给予回报;23.5.1节。