没有银弹

软件工程(software engineering)中的本质(essence)与偶然(accident)

Frederick P. Brooks, Jr. University of North Carolina at Chapel Hill

无论是在技术上,还是在管理方法上,都没有任何单一进展能够承诺在十年之内,使生产率、可靠性或简洁性提高一个数量级。

摘要[1]

所有软件构造都包含本质任务(essential tasks),即塑造构成抽象软件实体(abstract software entity)的复杂概念结构(conceptual structure);也包含偶然任务(accidental tasks),即用程序设计语言(programming languages)表示这些抽象实体,并在空间与速度约束下把它们映射到机器语言。过去软件生产率(software productivity)的大部分重大提升,来自移除了那些使偶然任务异常困难的人为障碍,例如严苛的硬件限制、笨拙的程序设计语言、机器时间不足。如今,软件工程师所做的工作中,还有多少仍然投入在偶然任务上,而不是本质任务上?除非它超过全部工作的 9/10,否则即使把所有偶然活动的时间压缩到零,也不会带来一个数量级的改进。

因此,看起来现在已经到了处理软件任务中本质部分的时候,也就是处理那些关乎塑造高度复杂抽象概念结构的部分。我建议:

  • 利用大众市场(mass market),避免自行构造可以买到的东西。
  • 把快速原型(rapid prototyping)作为有计划迭代的一部分,用于确立软件需求(software requirements)。
  • 以有机方式生长软件(grow software),在系统运行、使用和测试过程中逐步加入越来越多的功能。
  • 识别并培养新一代中卓越的概念设计者(conceptual designers)。

引言

在充满我们民间传说噩梦的所有怪物中,最令人恐惧的莫过于狼人,因为它们会出人意料地从熟悉之物变成恐怖之物。对于这类怪物,我们寻找能够神奇地使其安息的银弹。

常见的软件项目也有某些这样的特征,至少在非技术管理者看来如此:通常是无害而直接的,却可能变成错过进度、突破预算、产品有缺陷的怪物。于是我们听到对银弹的绝望呼喊,希望有某种东西能让软件成本像计算机硬件成本那样迅速下降。

但是,当我们把目光投向未来十年的地平线时,我们看不到银弹。无论是在技术上,还是在管理方法上,都没有任何单一进展本身能够承诺在生产率、可靠性或简洁性上带来哪怕一个数量级的提升。本章将尝试说明原因,考察软件问题的性质,也考察那些被提出的“子弹”的性质。

然而,怀疑并不等于悲观。虽然我们看不到惊人的突破,而且确实相信这类突破与软件的性质并不一致,但许多令人鼓舞的创新正在发生。以有纪律、持续一致的努力去发展、传播并利用它们,确实应当能带来一个数量级的改进。没有通往王道的捷径,但确实有一条道路。

疾病管理的第一步,是用病菌理论取代恶魔理论和体液理论。正是这一步,也就是希望的开端,本身粉碎了所有对魔法解法的希望。它告诉工作者,进步将逐步取得,需要付出巨大努力,并且必须持久不懈地关注清洁这门纪律。今天的软件工程也是如此。

它必须这么困难吗?本质困难

不仅眼下看不到银弹,软件的本质还使得未来也不太可能出现银弹,也就是不会有什么发明能像电子学、晶体管和大规模集成电路之于计算机硬件那样,作用于软件的生产率、可靠性和简洁性。我们不能指望每两年就看到翻倍的收益。

首先,我们必须看到,反常的并不是软件进步太慢,而是计算机硬件进步太快。自文明开始以来,没有其他技术能在 30 年内获得六个数量级的性价比提升。也没有其他技术能让人选择把这种收益用于提升性能,或用于降低成本。这些收益来自计算机制造从装配工业转变为过程工业。

其次,为了看清我们能期待软件技术以怎样的速度进步,让我们考察它的困难。依照亚里士多德,我把这些困难分成本质和偶然:本质是软件性质中固有的困难;偶然则是今天伴随其生产、但并非固有的困难。

偶然困难我将在下一节讨论。先让我们考虑本质。

软件实体的本质,是一组相互咬合的概念(interlocking concepts)构造:数据集、数据项之间的关系、算法(algorithms),以及函数调用(invocations of functions)。这一本质是抽象的,因为同一个概念构造可以有许多不同表示。尽管如此,它仍然高度精确且细节丰富。

我相信,构建软件的困难部分在于对这个概念构造的规格说明(specification)、设计和测试,而不是表示它的劳动,也不是测试这种表示是否忠实的劳动。当然,我们仍然会犯语法错误;但与多数系统中的概念错误(conceptual errors)相比,它们只是浮渣。

如果这是真的,构建软件就永远会很困难。这里本来就没有银弹。

让我们考察现代软件系统这一不可化约本质(irreducible essence)的固有属性:复杂性(complexity)、顺应性(conformity)、可变性和不可见性(invisibility)。

复杂性

按规模计,软件实体也许比任何其他人造物都更复杂,因为没有两个部分是相同的,至少在语句层次以上如此。如果它们相同,我们就会把这两个相似部分合并成一个子程序(subroutine),无论是开放的还是封闭的。在这方面,软件系统与计算机、建筑或汽车有根本不同,后者充满了重复元素。

数字计算机本身已经比人们构造的大多数东西更复杂;它们有大量状态。这使得构想、描述和测试它们都很困难。软件系统拥有的状态数量比计算机还要多几个数量级。

同样,软件实体的扩展并不只是把相同元素以更大规模重复;它必然会增加不同元素的数量。在多数情况下,这些元素以某种非线性方式(nonlinear fashion)相互作用,整体复杂度的增长远远超过线性。

软件复杂性是一种本质属性,而不是偶然属性。因此,对软件实体的描述如果抽掉了它的复杂性,往往也就抽掉了它的本质。三个世纪以来,数学和物理科学通过为复杂现象构造简化模型、从模型推导性质并用实验验证这些性质,取得了巨大进步。这之所以可行,是因为模型中忽略的复杂性并不是现象的本质属性。当复杂性本身就是本质时,这种方法就行不通。

开发软件产品的许多经典问题,都源于这种本质复杂性,以及复杂性随规模非线性增长这一事实。复杂性带来团队成员之间沟通的困难,进而导致产品缺陷、成本超支和进度延迟。复杂性带来枚举、更不用说理解程序所有可能状态的困难,由此产生不可靠性。功能的复杂性带来调用这些功能的困难,使程序难以使用。结构的复杂性带来把程序扩展到新功能而不产生副作用的困难。结构的复杂性还带来未被可视化的状态,而这些状态构成安全陷门。

不仅技术问题,管理问题也来自复杂性。这种复杂性使总体把握变得困难,从而阻碍概念完整性(conceptual integrity)。它使发现并控制所有零散尾巴变得困难。它造成巨大的学习和理解负担,使人员流动成为灾难。

顺应性

软件人员并不是唯一面对复杂性的人。物理学处理极其复杂的对象,即使在“基本”粒子层面也是如此。不过物理学家继续工作,是因为坚定相信存在可被发现的统一原理,无论是在夸克中,还是在统一场论中。爱因斯坦反复主张,自然一定有简化的解释,因为上帝不是反复无常或任意妄为的。

软件工程师没有这种信念可以慰藉。他必须掌握的许多复杂性是任意复杂性(arbitrary complexity),是由他的接口(interfaces)必须顺应的众多人类制度和系统毫无道理地强加的。这些接口与接口之间、时刻与时刻之间都不同,不是因为必然如此,而只是因为它们由不同的人设计,而不是由上帝设计。

在许多情况下,软件必须顺应,是因为它最近才来到现场。在其他情况下,它必须顺应,是因为人们认为它最容易顺应。但在所有情况下,大量复杂性都来自对其他接口的顺应;这种复杂性无法仅靠重新设计软件本身而被简化掉。

可变性(changeability)

软件实体持续承受变化压力。当然,建筑、汽车和计算机也是如此。但是制造物在制造后很少改变;它们会被后续型号取代,或者本质变化会被纳入同一基本设计的后续序列号副本中。汽车召回其实相当少见;计算机的现场变更也并不那么频繁。二者都远少于已部署软件的修改。

这部分是因为系统中的软件体现了其功能,而功能是最感受到变化压力的部分。另一部分原因是软件更容易改变,它是纯粹的思想材料,具有无限可塑性。建筑实际上也会改变,但改变的高成本为所有人所理解,因此会抑制变更者的一时兴起。

所有成功的软件都会被改变。有两个过程在起作用。当一个软件产品被发现有用时,人们会在原始领域的边缘,甚至超出原始领域的新场景中尝试它。扩展功能的压力主要来自喜欢其基本功能并为其发明新用途的用户。

其次,成功的软件也会存活得比它最初为之编写的机器载体更久。如果不是新计算机,至少也会有新磁盘、新显示器、新打印机出现;软件必须顺应这些新的机会载体。

简言之,软件产品嵌入在由应用、用户、法律和机器载体构成的文化矩阵中。所有这些都在持续变化,而它们的变化会不可阻挡地把变化强加给软件产品。

不可见性

软件是不可见且不可可视化的。几何抽象(geometric abstraction)是强有力的工具。建筑的平面图帮助建筑师和客户评估空间、交通流线和视野。矛盾会变得明显,遗漏能够被捕捉。机械部件的比例图和分子的棍棒模型虽然是抽象,也服务于同一目的。几何现实被捕获在几何抽象中。

软件的现实并不内在地嵌入空间。因此,它没有现成的几何表示,就像土地有地图、硅芯片有图表、计算机有连接示意图那样。一旦我们试图绘制软件结构图,就会发现它不是一个,而是多个一般有向图(directed graph)叠加在一起。这几个图可以分别表示控制流(flow of control)、数据流(flow of data)、依赖模式、时间顺序、名字空间关系(name-space relationships)。它们通常甚至不是平面的,更不用说层次化了。事实上,对这种结构建立概念控制的一种方式,就是强制切断连接,直到其中一个或多个图变成层次结构。[2]

尽管在限制和简化软件结构方面已有进展,软件仍然内在地不可可视化,因此剥夺了心智最强大的一些概念工具。这种缺失不仅妨碍一个头脑内部的设计过程,也严重阻碍头脑之间的沟通。

过去的突破解决了偶然困难

如果我们考察软件技术中迄今最有成果的三个步骤,就会发现每一个都攻击了构建软件时一个不同的主要困难,但这些都是偶然困难,而不是本质困难。我们也能看到,把每一种攻击继续外推时会遇到的自然极限。

高级语言(high-level languages)

对软件生产率、可靠性和简洁性最有力的一击,当然是逐步使用高级语言进行程序设计。多数观察者认为,这一发展至少带来了五倍的生产率提升,同时也带来了可靠性、简洁性和可理解性的相应收益。

高级语言完成了什么?它把程序从大量偶然复杂性中解放出来。抽象程序由概念构造组成:操作、数据类型(data types)、序列和通信。具体的机器程序则关心位、寄存器、条件、分支、通道、磁盘等等。只要高级语言体现了抽象程序中需要的构造,并避免所有更低层的构造,它就消除了一个完整的复杂性层级,而这一层级本来从未内在于程序之中。

高级语言最多能做的,是提供程序员在抽象程序中想象的所有构造。诚然,我们关于数据结构、数据类型和操作的思考成熟度在持续提高,但速度递减。语言发展越来越接近用户的成熟度。

而且,在某一点上,高级语言的精致化会成为负担,增加而不是减少那些很少使用深奥构造的用户的智力任务。

分时(time-sharing)

多数观察者认为,分时对程序员生产率和产品质量有重大改进,虽然不如高级语言带来的改进那样大。

分时攻击的是一种明显不同的困难。分时保持即时性,从而使我们能够维持对复杂性的总体把握。批处理程序设计的缓慢周转意味着,当我们停止编程并提交编译和执行后,必然会忘记细枝末节,如果不是连思路主线都忘掉的话。这种意识中断耗费时间,因为我们必须重新刷新记忆。最严重的影响很可能是对复杂系统中一切正在发生之事的把握衰退。

缓慢周转就像机器语言复杂性一样,是软件过程的偶然困难,而不是本质困难。分时贡献的极限也由此直接得出。其主要效果是缩短系统响应时间。当响应时间趋近于零时,到某一点它会低于人的可察觉阈值,约 100 毫秒。超过这一点,就不应再期待收益。

统一的程序设计环境(unified programming environments)

Unix 和 Interlisp 是最早广泛使用的集成程序设计环境,人们认为它们以整数倍改善了生产率。为什么?

它们通过提供集成库、统一文件格式以及管道和过滤器(pipes and filters),攻击了把程序组合使用时的偶然困难。因此,原则上总是可以相互调用、馈送和使用的概念结构,在实践中确实能够轻松做到。

这一突破反过来又刺激了完整工具台的发展,因为每个新工具都可以通过标准格式应用于任何程序。

由于这些成功,环境成为当今许多软件工程研究的主题。我们将在下一节考察它们的前景和局限。

对银弹的希望

现在让我们考虑那些最常被提出、被视为潜在银弹的技术发展。它们解决什么问题?这些问题是本质问题,还是我们偶然困难中的残余?它们提供的是革命性进展,还是渐进性进展?

Ada 和其他高级语言进展

最常被吹捧的近来发展之一,是 20 世纪 80 年代的通用高级语言 Ada。Ada 确实不仅反映了语言概念上的演进改进,也包含了鼓励现代设计和模块化(modularization)概念的特性。也许 Ada 哲学比 Ada 语言本身更是一种进步,因为它是模块化、抽象数据类型(abstract data types)和层次结构化的哲学。

Ada 或许过于丰富,这是需求叠加到其设计过程上的自然产物。这并不致命,因为子集工作词汇表可以解决学习问题,而硬件进步会给我们廉价的 MIPS 来支付编译成本。推进软件系统结构化,确实是我们用美元买到的更多 MIPS 的一个很好用途。操作系统在 20 世纪 60 年代曾因其内存和周期成本而受到猛烈谴责,但事实证明,它们是利用过去硬件浪潮带来的部分 MIPS 和廉价内存字节的一种极佳形式。

尽管如此,Ada 不会成为杀死软件生产率怪物的银弹。毕竟,它只是另一种高级语言,而这类语言最大的回报来自第一次转变,也就是从机器的偶然复杂性上升到对分步解法的更抽象陈述。那些偶然复杂性一旦被移除,剩余的偶然复杂性就更小,从移除它们中得到的回报必然更少。

我预测,十年之后,当 Ada 的有效性被评估时,人们会看到它带来了实质差异,但不是因为任何特定语言特性,事实上也不是因为所有特性合在一起。同样,新的 Ada 环境也不会被证明是这些改进的原因。Ada 最大的贡献将是,切换到 Ada 促成了对程序员进行现代软件设计技术培训。

面向对象程序设计(object-oriented programming)

许多研究这门技艺的人,对面向对象程序设计寄予的希望超过当日任何其他技术风尚。[3] 我也是其中之一。Dartmouth 的 Mark Sherman 指出,我们必须小心区分两个都归在这个名字下的独立思想:抽象数据类型和层次化类型(hierarchical types),也称为类。抽象数据类型的概念是,对象的类型应当由名称、一组合法值和一组合法操作来定义,而不是由其存储结构定义;存储结构应当被隐藏。例子包括 Ada 包(带私有类型)或 Modula 的模块。

层次化类型,例如 Simula-67 的类,允许定义通用接口,再通过提供下级类型进一步细化。两个概念是正交的:可以有不隐藏的层次结构,也可以有无层次结构的隐藏。二者都代表了构建软件技艺中的真实进步。

二者各自都从过程中移除了一种额外的偶然困难,使设计者能够表达其设计的本质,而不必表达大量不增加新信息内容的语法材料。对抽象类型和层次化类型来说,结果都是移除一种更高阶的偶然困难,并允许更高阶地表达设计。

尽管如此,这类进展最多也只能移除设计表达中的所有偶然困难。设计本身的复杂性是本质的;这类攻击对它完全没有改变。只有当今天程序设计语言中仍然残留的类型说明(type specification)这种不必要的灌木丛,本身负责了设计程序产品工作量的 9/10 时,面向对象程序设计才能带来一个数量级的收益。我对此表示怀疑。

人工智能(artificial intelligence)

许多人期待人工智能的进展提供革命性突破,从而在软件生产率和质量上带来数量级收益。[4] 我不这样认为。要看清原因,我们必须剖析“人工智能”的含义,然后看它如何适用。

Parnas 澄清了术语混乱:

今天常用的 AI 有两个相当不同的定义。AI-1:使用计算机解决以前只能通过运用人类智能才能解决的问题。AI-2:使用一组特定的程序设计技术,称为启发式或基于规则的程序设计(heuristic or rule-based programming)。在这种方法中,人类专家被研究,以确定他们在解决问题时使用了哪些启发式或经验法则……程序被设计成以人类似乎解决问题的方式来解决问题。

第一个定义的含义会滑动……某件事今天可以符合 AI-1 的定义,但一旦我们看清程序如何工作并理解了问题,就不再会把它看作 AI……不幸的是,我无法识别出一套该领域独有的技术……大多数工作都是问题特定的,需要某种抽象或创造力才能看出如何迁移它。[5]

我完全同意这一批评。用于语音识别的技术,似乎与用于图像识别的技术几乎没有共同之处,而二者又都不同于专家系统(expert systems)中使用的技术。例如,我很难看出图像识别会如何对程序设计实践产生任何可观差异。语音识别也是如此。构建软件的困难在于决定要说什么,而不是说出它。任何对表达的便利化最多只能带来边际收益。

专家系统技术,也就是 AI-2,值得单独讨论一节。

专家系统

人工智能技艺中最先进、也应用最广的部分,是构建专家系统的技术。许多软件科学家正努力把这项技术应用到软件构建环境中。[6] 其概念是什么,前景又如何?

专家系统是一个包含通用推理引擎(inference engine)和规则库(rule base)的程序,它被设计为接受输入数据和假设,通过规则库可导出的推理探索其逻辑后果,给出结论和建议,并能够通过追溯推理过程向用户解释其结果。除了纯确定性逻辑之外,推理引擎通常还可以处理模糊或概率性数据与规则。

与为同样问题得出同样解答的程序化算法相比,这类系统提供了一些明确优势:

  • 推理引擎技术以应用无关的方式开发,然后应用到许多用途上。人们可以为推理引擎投入多得多的努力。事实上,那项技术已经相当先进。
  • 应用特有材料中可变的部分以统一方式编码在规则库中,并提供工具来开发、修改、测试和记录规则库。这使应用本身的大量复杂性规整化。

Edward Feigenbaum 说,这类系统的力量并不来自越来越花哨的推理机制,而是来自越来越丰富、能更准确反映真实世界的知识库(knowledge bases)。我相信,这项技术提供的最重要进步,是把应用复杂性与程序本身分离开来。

这如何应用到软件任务?方式很多:建议接口规则、对测试策略给出建议、记住缺陷类型频率、提供优化提示,等等。

以一个想象中的测试顾问为例。在其最初级形式中,诊断专家系统很像飞行员的检查清单,本质上提供关于困难可能原因的建议。随着规则库发展,建议会变得更具体,对所报告故障症状作出更复杂的考虑。人们可以想象一个调试助手,一开始提供非常泛化的建议;但随着越来越多的系统结构被纳入规则库,它在生成的假设和推荐的测试上会变得越来越具体。这样的专家系统与传统专家系统最根本的不同,可能在于其规则库应当以与相应软件产品相同的方式层次化、模块化,使得当产品被模块化修改时,诊断规则库也能被模块化修改。

生成诊断规则所需的工作,无论如何都必须在为模块和系统生成测试用例集合时完成。如果这项工作以足够通用的方式进行,并有统一的规则结构和良好的推理引擎可用,它实际上可能减少生成启动测试用例的总劳动,同时也有助于终身维护和修改测试。以同样方式,我们可以假设存在其他顾问,可能有许多个,而且很可能是简单的,用于软件构造任务的其他部分。

要尽早实现对程序开发者有用的专家顾问,许多困难挡在前面。我们想象场景中的关键部分,是开发出从程序结构规格说明到自动或半自动生成诊断规则的便捷方法。更困难且更重要的是知识获取(knowledge acquisition)这一双重任务:找到表达清晰、能自我分析且知道自己为什么这样做的专家;并开发高效技术,提取他们所知道的东西并将其蒸馏成规则库。构建专家系统的本质前提,是拥有专家。

专家系统最有力的贡献,必然是把最优秀程序员的经验和积累智慧,提供给缺乏经验的程序员使用。这并不是小贡献。最佳软件工程实践与平均实践之间的差距非常大,也许比任何其他工程学科都更大。一个能传播良好实践的工具将是重要的。

“自动”程序设计

近 40 年来,人们一直在期待并撰写关于“自动程序设计(automatic programming)”的文章,即从问题规格说明(problem specifications)生成解决该问题的程序。今天有些人的写法仿佛他们期待这项技术提供下一次突破。[7]

Parnas 暗示,这个术语的使用是为了增添光彩,而不是为了语义内容,他断言:

简言之,自动程序设计一直是一个委婉说法,指的是使用比程序员当时可用语言更高层的语言进行程序设计。[8]

他的论证本质上是,在多数情况下,必须给出的并不是问题的规格说明,而是求解方法(solution method)的规格说明。

也能找到例外。构建生成器的技术非常强大,并且在排序程序中被常规地有利使用。一些用于积分微分方程的系统也允许直接给出问题规格说明。系统评估参数,从解法库中选择方法,并生成程序。

  • 这些应用具有非常有利的性质:
  • 问题很容易用相对较少的参数刻画。
  • 存在许多已知解法,可以提供备选库。
  • 广泛分析已经导出了显式规则,可根据问题参数选择求解技术。

很难看出这类技术如何泛化到普通软件系统的更广阔世界,在那里,具备这种整洁性质的案例只是例外。甚至很难想象这种泛化突破可能如何发生。

图形化程序设计(graphical programming)

软件工程中 Ph.D. 论文最喜欢的主题之一,是图形化或可视化程序设计(visual programming),也就是把计算机图形应用于软件设计。[9] 有时,这种方法的前景来自与 VLSI 芯片设计(VLSI chip design)的类比,因为计算机图形在芯片设计中发挥了如此有成效的作用。有时,这种方法的理由是把流程图(flowcharts)视为理想的程序设计媒介,并为构造流程图提供强大设施。

这类努力尚未产生任何令人信服的东西,更不用说令人兴奋的东西。我确信它们不会产生。

首先,正如我在别处所论证的,流程图是对软件结构非常糟糕的抽象。[10] 事实上,最好把它看作 Burks、von Neumann 和 Goldstine 为其拟议计算机提供急需的高级控制语言的尝试。在今天被细化成可怜的、多页的、带连接框的形式之后,流程图作为设计工具已经证明本质上无用;程序员是在写完程序之后,而不是之前,绘制描述这些程序的流程图。

其次,今天的屏幕以像素计太小,无法同时显示任何严肃详细软件图的范围和分辨率。今天工作站所谓的“桌面隐喻(desktop metaphor)”实际上是“飞机座位”隐喻(airplane-seat metaphor)。任何坐在经济舱两个魁梧乘客之间翻动满膝纸张的人,都会认识到其中差异:一次只能看见极少数东西。真正的桌面能提供二十来页的概览和随机访问。而且,当创造力强烈涌现时,不止一位程序员或作者会放弃桌面,转而使用更宽敞的地板。在我们的视域范围足以满足软件设计任务之前,硬件技术还必须有相当大的进步。

更根本的是,如我上文所论证,软件非常难以可视化。无论我们绘制控制流、变量作用域嵌套、变量交叉引用、数据流、层次化数据结构,还是其他任何东西,我们都只摸到了这头错综互锁的软件大象的一个维度。如果把许多相关视图生成的所有图叠加起来,又很难抽取任何全局概览。VLSI 类比从根本上是误导性的:芯片设计是一个分层的二维对象,其几何形状反映其本质。软件系统不是。

程序验证(program verification)

现代程序设计中的大量努力投入在测试和修复缺陷上。那么,是否可能通过在源头,也就是系统设计阶段消除错误,找到一枚银弹?是否可以通过遵循一种深刻不同的策略,即在把巨大努力投入实现和测试之前证明设计正确,来同时大幅提高生产率和产品可靠性?

我不相信我们会在这里找到魔法。程序验证是一个非常强大的概念,对于安全操作系统内核(secure operating system kernels)之类的东西会非常重要。然而,这项技术并不承诺节省劳动。验证工作量如此之大,以至于只有少数实质性程序曾经被验证。

程序验证并不意味着程序不会出错。这里也没有魔法。数学证明同样可能有缺陷。因此,虽然验证可能减少程序测试负担,却不能消除它。

更严重的是,即使完美的程序验证也只能确认程序满足其规格说明。软件任务最困难的部分是得到完整且一致的规格说明,而构建程序的大部分本质事实上正是对规格说明进行调试。

环境和工具

从对更好程序设计环境的爆炸性研究中,还能期待多少收益?人的本能反应是,高回报问题最先受到攻击,并且已经被解决:层次化文件系统、统一文件格式以获得统一程序接口,以及通用工具。语言特定的智能编辑器尚未在实践中广泛使用,但它们最多承诺让人免于语法错误和简单语义错误。

也许程序设计环境中尚待实现的最大收益,是使用集成数据库系统,跟踪无数必须由个体程序员准确回忆、并在单一系统协作者群体中保持最新的细节。

这项工作当然值得做,也当然会在生产率和可靠性上结出一些果实。但从其本质来看,从现在起回报必定是边际性的。

工作站

个人工作站的能力和内存容量必然且快速增长,这会给软件技艺带来怎样的收益?一个人能有效使用多少 MIPS?程序和文档的撰写与编辑已由今天的速度充分支持。编译可以获得提升,但机器速度提高 10 倍,也必然仍会让思考时间成为程序员一天中的主导活动。事实上,现在看起来已经如此。

更强大的工作站我们当然欢迎。但我们不能指望它们带来神奇增强。

对概念本质的有前景攻击

尽管没有任何技术突破承诺会带来我们在硬件领域如此熟悉的那种魔法结果,但现在确实有大量良好工作正在进行,也有稳定而不炫目的进步前景。

所有针对软件过程偶然性的技术攻击,根本上都受到生产率方程(productivity equation)的限制:

任务时间 = ∑(频率)i x (时间)i

如果如我所信,任务中的概念成分(conceptual components)现在占据了大部分时间,那么无论在那些仅仅是概念表达的任务成分上投入多少活动,都无法带来巨大的生产率收益。

因此,我们必须考虑那些处理软件问题本质的攻击,也就是处理这些复杂概念结构的形成。幸运的是,其中一些非常有前景。

买入还是构建

构造软件最激进的可能解法,就是根本不要构造它。

随着越来越多供应商为令人眼花缭乱的各种应用提供越来越多、更好的软件产品,这每天都变得更容易。当我们软件工程师还在为生产方法论辛苦工作时,个人计算机革命已经为软件创造了不是一个,而是许多个大众市场。每个报摊都售卖按机器类型分类的月刊,广告和评测几十种价格从几美元到几百美元不等的产品。更专业的来源则为工作站和其他 Unix 市场提供非常强大的产品。甚至软件工具和环境也可以买到现货。我曾在别处提出过单个模块的市场。

任何这类产品买起来都比重新构建便宜。即使成本为 100,000 美元,购买一件软件的成本也大约只相当于一个程序员年。而且交付是即时的!至少对于真正存在的产品,也就是开发者可以把潜在客户介绍给满意用户的产品来说,是即时的。此外,这类产品往往比自制软件有好得多的文档,维护也稍好一些。

我相信,大众市场的发展是软件工程中最深远的长期趋势。软件成本一直是开发成本,而不是复制成本。即使只在少数用户之间分摊这项成本,也会极大降低每个用户的成本。另一种看法是,使用一个软件系统的 n 份副本,实际上把其开发者的生产率乘以 n。这是对该学科和国家生产率的增强。

关键问题当然是适用性。我能用可得的现货包(off-the-shelf package)来完成我的任务吗?这里发生了一件令人惊讶的事情。在 20 世纪 50 年代和 60 年代,一项又一项研究显示,用户不会把现货包用于工资、库存控制、应收账款等场景。需求太专门,案例之间的变化太大。到了 20 世纪 80 年代,我们发现这类包需求很高且广泛使用。什么改变了?

并不真是包改变了。它们也许比从前稍微更通用、更可定制,但并没有多少。也并不真是应用改变了。如果说有什么变化,今天的商业和科学需求比 20 年前更加多样、更加复杂。

大的变化在于硬件/软件成本比(hardware/software cost ratio)。1960 年,一台 200 万美元机器的买家会觉得,他还负担得起额外 25 万美元购买一个定制工资程序,让它轻松而无扰地嵌入对计算机敌意很强的社会环境。今天 50,000 美元办公机器的买家不可能负担定制工资程序;所以他们调整自己的工资流程以适应可用包。计算机现在如此普遍,虽然还没有那么可爱,以至于这类适应被视为理所当然。

对于我关于软件包通用性多年来变化不大的论点,有一些显著例外:电子表格(electronic spreadsheets)和简单数据库系统。这些强大工具回头看如此显然,却又出现得如此之晚,适合无数用途,其中一些相当非正统。关于如何用电子表格处理意想不到任务的文章甚至书籍,如今大量涌现。大量过去会用 Cobol 或 Report Program Generator 编写为定制程序的应用,现在常规地由这些工具完成。

许多用户现在日复一日地在各种应用上操作自己的计算机,却从不写程序。事实上,这些用户中许多人不能为自己的机器写新程序,但他们仍然善于用它们解决新问题。

我相信,对于今天许多组织来说,最有力的单一软件生产率策略,是为一线不懂计算机的知识工作者配备个人计算机,以及良好的通用写作、绘图、文件和电子表格程序,然后放手让他们使用。同一策略若配以简单程序设计能力,也会适用于数百名实验室科学家。

需求细化(requirements refinement)和快速原型

构建软件系统中最困难的单一部分,是准确决定要构建什么。没有其他概念工作部分像确立详细技术需求这样困难,其中包括所有与人、机器以及其他软件系统的接口。如果做错,没有其他部分会如此严重地损害所得系统。也没有其他部分在后来更难纠正。

因此,软件构建者为客户承担的最重要职能,是对产品需求进行迭代式抽取(iterative extraction)和细化。事实是,客户不知道自己想要什么。他们通常不知道必须回答哪些问题,而且几乎从未以必须被规格化的细节程度思考过问题。即便是简单回答“让新软件系统像我们的旧手工信息处理系统那样工作”,实际上也过于简单。客户从不完全想要那个。复杂软件系统还都是会行动、会运动、会工作的东西。这种行动的动态很难想象。因此,在规划任何软件活动时,必须把客户和设计者之间作为系统定义一部分的大量迭代考虑进去。

我还要更进一步主张:对于客户来说,即使他们与软件工程师合作,也不可能在构建并试用所指定产品的某些版本之前,完整、精确且正确地指定现代软件产品的确切需求。

因此,当前最有前景的技术努力之一,也是攻击软件问题本质而非偶然性的一项努力,是开发方法和工具,把系统快速原型作为需求迭代规格说明的一部分。

原型软件系统(prototype software system)是这样一种系统:它模拟预期系统的重要接口并执行主要功能,但不一定受同样的硬件速度、大小或成本约束。原型通常执行应用的主线任务,但不试图处理例外、正确响应无效输入、干净地中止,等等。原型的目的,是让所指定的概念结构变成真实之物,使客户能够测试它的一致性和可用性。

今天许多软件采购程序建立在这样一种假设之上:人们可以预先指定一个令人满意的系统,为其构建招标,让它被建成并安装。我认为这一假设从根本上是错误的,许多软件采购问题都源于这个谬误。因此,如果没有根本性修订,也就是规定原型和产品的迭代开发与规格说明,这些问题就无法被修复。

增量开发(incremental development):生长软件,而不是构建软件

我仍然记得 1958 年第一次听到一位朋友谈论“构建”程序,而不是“编写”程序时受到的震动。那一瞬间,他拓宽了我对软件过程的整个看法。这个隐喻转变强有力且准确。今天我们理解软件构造与其他建造过程有多么相似,并且自由使用该隐喻的其他元素,例如规格说明、组件装配和脚手架。

建造隐喻(building metaphor)已经过了它的有用期。现在是再次改变的时候了。如果如我所信,我们今天构造的概念结构过于复杂,无法预先准确指定,也过于复杂,无法无缺陷地建成,那么我们必须采取一种根本不同的方法。

让我们转向自然,研究生命体中的复杂性,而不只是研究人的死作品。在那里,我们发现一些复杂性令我们敬畏的构造。仅大脑就复杂到无法映射,强大到无法模仿,多样而丰富,自我保护并自我更新。秘密在于,它是生长出来的,而不是建造出来的。

我们的软件系统也必须如此。几年前,Harlan Mills 提出,任何软件系统都应通过增量开发来生长。[11] 也就是说,系统首先应当被做成可以运行,即使它除了调用适当的一组虚拟子程序(dummy subprograms)之外什么有用的事情也不做。然后,它一点一点被充实,子程序依次被开发成动作,或调用下一层空桩(empty stubs)。

自从我开始在软件工程实验室课程中向项目构建者敦促这种技术以来,我看到了最显著的结果。过去十年里,没有什么像这种方法一样如此彻底地改变了我自己的实践及其有效性。这种方法要求自顶向下设计(top-down design),因为它是软件的自顶向下生长。它允许轻松回溯。它适合早期原型。每个新增功能,以及对更复杂数据或情境的每个新规定,都会从已有内容中有机生长出来。

士气效果令人惊讶。一旦有一个能运行的系统,即使很简单,热情也会跃升。当一个新的图形软件系统在屏幕上显示出第一幅图像时,即使只是一个矩形,努力也会加倍。在过程的每个阶段,人们总是拥有一个工作的系统。我发现,团队在四个月内能够生长出比他们能够建造出的实体复杂得多的实体。

大型项目也可以实现与我的小项目相同的收益。[12]

卓越设计者(great designers)

如何改进软件技艺的核心问题,一如既往地集中在人身上。

我们可以通过遵循良好实践而不是糟糕实践来获得良好设计。良好设计实践可以被教授。程序员属于人口中最聪明的一部分,因此他们能够学习良好实践。因此,美国的一项主要努力是传播良好的现代实践。新的课程、新的文献、新的组织,例如 Software Engineering Institute,都已经出现,目的是把我们的实践水平从差提升到好。这完全正确。

然而,我不相信我们能以同样方式迈出下一步。糟糕概念设计与良好概念设计之间的差异,可能在于设计方法(design method)是否健全;但良好设计与卓越设计之间的差异,肯定不在于此。卓越设计来自卓越设计者。软件构造是创造性过程。健全的方法论可以赋能并解放创造性头脑;它不能点燃或激发苦工。

这些差异并不小,它更像 Salieri 与 Mozart 的差异。一项又一项研究表明,最优秀的设计者所产生的结构更快、更小、更简单、更干净,并且用更少努力产生。卓越者与平均者之间的差异接近一个数量级。

稍作回顾即可发现,尽管许多优秀、有用的软件系统是由委员会设计、由多方项目构建的,那些激起狂热拥护者的软件系统却是一个或少数几个设计头脑,即卓越设计者的产物。考虑 Unix、APL、Pascal、Modula、Smalltalk interface,甚至 Fortran;并与 Cobol、PL/I、Algol、MVS/370 和 MS-DOS 对比(图 1)。

Unix Cobol
APL PL/I
Pascal Algol
Modula MVS/370
Smalltalk MS-DOS
Fortran

图 1 令人兴奋的产品

因此,虽然我强烈支持当前正在进行的技术转移(technology transfer)和课程开发努力,但我认为我们能够发起的最重要单一努力,是开发出培养卓越设计者的方法。

没有任何软件组织可以忽视这一挑战。好管理者虽然稀缺,但并不比好设计者更稀缺。卓越设计者和卓越管理者都非常罕见。多数组织花费相当多努力寻找和培养管理后备人才;我不知道有哪个组织花费同等努力寻找和培养那些产品技术卓越性最终所依赖的卓越设计者。

我的第一个建议是,每个软件组织都必须确定并宣布:卓越设计者对其成功的重要性与卓越管理者相同,并且可以期待他们得到类似的培育和奖励。不仅是薪水,认可的附属待遇也必须完全等价:办公室大小、陈设、个人技术设备、差旅经费、员工支持。

如何培养卓越设计者?篇幅不允许展开长篇讨论,但有些步骤是明显的:

  • 系统地尽早识别顶尖设计者。最优秀者往往不是最有经验者。
  • 指派一位职业导师,负责候选人的发展,并维护细致的职业档案。
  • 为每位候选人设计并维护一份职业发展计划(career development plan),其中包括精心选择的、与顶尖设计者一起工作的学徒期,若干次高级正规教育,短期课程,以及穿插其间的独立设计和技术领导任务。
  • 提供机会,让成长中的设计者相互交流并相互激发。

References

  1. Reproduced from: Frederick P. Brooks, The Mythical Man-Month, Anniversary edition with 4 new chapters, Addison-Wesley (1995), itself reprinted from the Proceedings of the IFIP Tenth World Computing Conference, H.-J. Kugler, ed., Elsevier Science B.V., Amsterdam, NL (1986) pp. 1069-76.
  2. Parnas, D.L., “Designing software for ease of extension and contraction,” IEEE Trans. on SE, 5, 2 (March, 1979), pp. 12-138.
  3. Booch, G., “Object-oriented design,” in Software Engineering with Ada. Menlo Park, Calif.: Benjamin Cummings, 1983.
  4. Mostow, J., ed., Special Issue on Artificial Intelligence and Software Engineering, IEEE Trans. on SE, 11, 11 (Nov. 1985).
  5. Parnas, D.L., “Software aspects of strategic defense systems,” Communications of the ACM, 28, 12 (Dec. 1985), pp. 1326-1335. Also in American Scientist, 73, 5 (Sept.-Oct. 1985), pp. 432-440.
  6. Balzer, R., “A 15-year perspective on automatic programming,” in Mostow, op. cit.
  7. Mostow, op. cit.
  8. Parnas, 1985, op. cit.
  9. Raeder, G., “A survey of current graphical programming techniques,” in R. B. Grafton and T. Ichikawa, eds., Special Issue on Visual Programming, Computer, 18, 8 (Aug. 1985), pp. 11-25.
  10. Brooks 1995, op. cit., chapter 15.
  11. Mills, H. D., “Top-down programming in large systems,” Debugging Techniques in Large Systems, R. Rustin, ed., Englewood Cliffs, N.J., Prentice-Hall, 1971.
  12. Boehm, B. W., “A spiral model of software development and enhancement,” Computer, 20, 5 (May 1985), pp. 43-57.

No Silver Bullet: Essence and Accidents of Software Engineering

1986 · Frederick P. Brooks, Jr.

lucida 翻译

Tags: 软件工程, 软件架构, 经典重读