严格地说,PERT技术是关键路径计划的细化,如果使用PERT图,它需要对每个事件估计三次,每次对应于满足估计日期的不同可能性。我觉得不值得为这样的精化产生额外的工作量,但为了方便,我把任何关键路径法都称为PERT图。
PERT的准备工作是PERT图使用中最有价值的部分。它包括整个网状结构的展开、任务之间依赖关系的识别、各个任务链的估计。这些都要求在项目早期进行非常专业的计划。第一份PERT图总是很恐怖的,不过人们总是不断地进行努力,运用才智制订下一份PERT图。
随着项目的推进,PERT图为前面那个泄气的借口,“其他的部分反正会落后”,提供了答案。它展示某人为了使自己的工作远离关键路径,需要超前多少,也建议了补偿其他部分失去的时间的方法。
地毯的下面
当一线经理发现自己的队伍出现了计划偏离时,他肯定不会马上赶到老板那里去汇报这个令人沮丧的消息。团队可以弥补进度偏差,他可以想出应对方法或者重新安排进度以解决问题,为什么要去麻烦老板呢?从这个角度来看,好像还不错。解决这类问题的确是一线经理的职责。老板已经有很多需要处理的真正的烦心事了,他不想被更多的问题打搅。因此,所有的污垢都被隐藏在地毯之下。
但是每个老板都需要两种信息:需要采取行动的计划方面的问题,用来进行分析的状态数据3。出于这个目的,他需要了解所有开发队伍的情况,但得到状态的真相是很困难的。
一线经理的利益和老板的利益是内在冲突的。一线经理担心如果汇报了问题,老板会采取行动,这些行动会取代经理的作用,降低自己的威信,搞乱了其他计划。所以,只要项
目经理认为自己可以独立解决问题,他就不会告诉老板。
有两种掀开毯子把污垢展现在老板面前的方法,它们必须都被采用。一种是减少角色冲突和鼓励状态共享,另一种是猛地拉开地毯。
减少角色的冲突。首先老板必须区别行动信息和状态信息。他必须规范自己,不对项目经理可以解决的问题做出反应,并且决不在检查状态报告的时候做安排。我曾经认识一个老板,他总是在状态报告的第一个段落结束之前,拿起电话发号施令。这样的反应肯定压制信息的完全公开。
不过,当项目经理了解到老板收到项目报告之后不会惊慌,或者不会越俎代庖时,他就逐渐会提交真实的评估结果。
如果老板把会见、评审、会议明显标记为状态检查(status-meeting)和问题-行动(problem-action)会议,并且相应控制自己的行为,这对整个过程会很有帮助。当然,事态发展到无法控制时,状态检查会议会演变成问题-行动会议。不过,至少每个人知道“当时游戏的分数是多少”,老板在接过“皮球”之前也会三思。
猛地拉开地毯。不论协作与否,拥有能了解状态真相的评审机制是必要的。PERT图以及频繁的里程碑是这种评审的基础。大型项目中,可能需要每周对某些部分进行评审,大约一个月左右进行整体评审。
有报告显示关键的文档是里程碑和实际的完成情况。图14.1是上述报告中的一段摘录。它显示了一些问题:手册(SLR)的批准时间有所冲突,其中一个的时间比独立产品测试(Alpha)的开始时间还要迟。这样一份报告将作为2月1号会议的议程,使得每个人都知道问题的所在,而产品构件经理应准备解释延迟的原因,什么时候结束,采取的步骤和需要的任何帮助——老板提供的,或者是其他小组间接提供的。
注:
SYSTEM/360 SUMMARY STATUS REPORT -SYSTEM/360总结状态报告
OS/360 LANGUAGE PROCESSORS + SERVICE PROGAMS-OS/360语言处理器 + 服务程序
AS OF FEBRAURAY 01.1965-1965年2月1号
APPOVEL-批准
COMPLETED-完成
PROJECT-项目
LOCATION-地点
COMMITMNT ANNOUNCE RELEASE-计划 发布
OBJECTIVE AVAILABLE APPROVED-目标制订 批准
SPECS AVAILABLE APPROVED-规格说明提交 批准
SRL AVAILABLE APPROVED-SRL提交 批准
ALPHA TEST ENTRY EXIT-ALPHA测试 进入 退出
COMP TEST START COMPLETE-单元测试 开始 结束
SYS TEST START COMPLETE-系统测试 开始 结束
BULLETIN AVAILABLE APPROVED-公告发布 批准
BETA TEST ENTRY EXIT-BETA测试 进入 退出
图14.1
Bell实验室的V. Vyssotsky添加了以下的观察意见:
我发现在里程碑报告中很容易记录“计划”和“估计”的日期。计划日期是项目经理的工作产物,代表了经协调后的项目整体工作计划,它是合理计划之前的判断。估计日期是最基层经理的工作产物,基层经理对所讨论的工作有着深刻的了解,估计日期代表了在现有资源和已得到了作为先决条件的必要输入(或得到了相应的承诺)的情况下,基层经理对实际实现日期的最佳判断。项目经理必须停止对这些日期的怀疑,而将重点放在使其更加精确上、以便得到没有偏见的估计,而不是那些合乎心意的乐观估计或者自我保护的保守估计。一旦它们在每个人的脑海中形成了清晰的印象,项目经理就可以预见到将来哪些地方如果他不采取任何措施,就会出现问题4。
PERT图的准备工作是老板和要向他进行汇报的经理们的职责。需要一个小组(一至三个人)来关注它的更新、修订和报告,这个小组可以看作是老板的延伸。对大型项目,这种计划和控制(Plan and Control)小组的价值是非常可贵的。小组的职权仅限于向产品线经理询问他们什么时候设定或更改里程碑,以及里程碑是否被达到。计划和控制小组处理所有的文字工作,因此产品线经理的负担将会减到最少——仅仅需要作出决策。
我们拥有一个富有热情的、有经验的、熟练的计划和控制小组。这个小组由A. M. Pietrasanta负责,他投入了大量创造天分来设计有效的、谦逊的控制方法。结果,我发现他的小组被广为尊重,而不仅仅是被容忍。对于这样一个本来就十分敏感的角色而言,这的
确是一个成功。
对计划和控制职能进行适度的技术人力投资是非常值得赞赏的。它对项目的贡献方式和直接开发软件产品有很大的不同。计划和控制小组作为监督人员,明白地指出了不易察觉的延迟,并强调关键的因素。他们是早期预警系统,防止项目以一次一天的方式落后一年。
另外一面(The other face)
不了解,就无法真正拥有。
- 歌德
- 克雷布
What we do not understand we do not possess.
- GOETHE
O give me commentators plain, Who with no deep researches vex the brain[jypan1]
- CRABBE
计算机程序是从人传递到机器的一些信息。为了将人的意图清晰地传达给不会说话的机器,程序采用了严格的语法和严谨的定义。
但是书面的计算机程序还有其他的呈现面貌:向用户诉说自己的“故事”。即使是完全开发给自己使用的程序,这种沟通仍然是必要的。因为记忆衰退的规律会使用户-作者失去对程序的了解,于是他不得不重拾自己劳动的各个细节。
公共应用程序的用户在时间和空间上都远离它们的作者,因此对这类程序,文档的重要性更是不言而喻!对软件编程产品来说,程序向用户所呈现的面貌和提供给机器识别的内容同样重要。
面对那些文档“简约”的程序,我们中的大多数人都不免曾经暗骂那些远在他方的匿名作者。因此,一些人试图向新人慢慢地灌输文档的重要性:旨在延长软件的生命期、克服惰性和进度的压力。但是,很多次尝试都失败了,我想很可能是由于我们使用了错误的方法。
Thomas J. Watson讲述了他年轻时在纽约北部,刚开始做收银机推销员的经历。他带着一马车的收银机,满怀热情地动身了。他工作得非常勤奋,但是连一台收银机也没有卖出去。他很沮丧地向经理汇报了情况,销售经理听了一会儿,说道:“帮我抬一些机器到马车上,收紧缰绳,出发!”他们成功了。在接下来的客户拜访过程中,经理身体力行地演示了如何出售收银机。事实证明,这个方法是可行的。
我曾经非常勤奋地给我的软件工程师们举办了多年关于文档必要性以及优秀文档所应具备特点方面的讲座,向他们讲述——甚至是热诚地向他们劝诫以上的观点。不过,这些都行不通。我想他们知道如何正确地编写文档,却缺乏工作的热情。后来,我尝试了向马车上搬一些收银机,以此演示如何完成这项工作。结果显示,这种方法的效果要好得多。所以,文章剩余部分将对那些说教之辞一笔带过,而把重点放在“如何做(才能产生一篇优秀的文档)上。
需要什么样的文档
不同用户需要不同级别的文档。某些用户仅仅偶尔使用程序,有些用户必须依赖程序,还有一些用户必须根据环境和目的的变动对程序进行修改。
使用程序。每个用户都需要一段对程序进行描述的文字。可是大多数文档只提供了很少的总结性内容,无法达到用户要求,就像是描绘了树木,形容了树叶,但却没有一副森林的图案。为了得到一份有用的文字描述,就必须放慢脚步,稳妥地进行。
1. 目的。主要的功能是什么?开发程序的原因是什么?
2. 环境。程序运行在什么样的机器、硬件配置和操作系统上?
3. 范围。输入的有效范围是什么?允许显示的合法范围是什么?
4. 实现功能和使用的算法。精确地阐述它做了什么。
5. 输入-输出格式。必须是确切和完整的。
6. 操作指令。包括控制台及输出内容中正常和异常结束的行为。
7. 选项。用户的功能选项有哪些?如何在选项之间进行挑选?
8. 运行时间。在指定的配置下,解决特定规模问题所需要的时间?
9. 精度和校验。期望结果的精确程度?如何进行精度的检测?
一般来说,三、四页纸常常就可以容纳以上所有的信息。不过往往需要特别注意的是表达的简洁和精确。由于它包含了和软件相关的基本决策,所以这份文档的绝大部分需要在程序编制之前书写。
验证程序。除了程序的使用方法,还必须附带一些程序正确运行的证明,即测试用例。
每一份发布的程序拷贝应该包括一些可以例行运行的小测试用例,为用户提供信心——他拥有了一份可信赖的拷贝,并且正确地安装到了机器上。
然后,需要得到更加全面的测试用例,在程序修改之后,进行常规运行。这些用例可以根据输入数据的范围划分成三个部分。
1. 针对遇到的大多数常规数据和程序主要功能进行测试的用例。它们是测试用例的主要组成部分。
2. 数量相对较少的合法数据测试用例,对输入数据范围边界进行检查,确保最大可能值、最小可能值和其他有效特殊数据可以正常工作。
3. 数量相对较少的非法数据测试用例,在边界外检查数据范围边界,确保无效的输入能有正确的数据诊断提示。
修改程序。调整程序或者修复程序需要更多的信息。显然,这要求了解全部的细节,并且这些细节已经记录在注释良好的列表中。和一般用户一样,修改者迫切需要一份清晰明了的概述,不过这一次是关于系统的内部结构。那么这份概述的组成部分是什么呢?
1. 流程图或子系统的结构图,对此以下有更详细的论述。
2. 对所用算法的完整描述,或者是对文档中类似描述的引用。
3. 对所有文件规划的解释。
4. 数据流的概要描述——从磁盘或者磁带中,获取数据或程序处理的序列——以及在每个处理过程完成的操作。
5. 初始设计中,对已预见修改的讨论;特性、功能回调的位置以及出口;原作者对可能会扩充的地方以及可能处理方案的一些意见。另外,对隐藏缺陷的观察也同样很有价值。
流程图
流程图是被吹捧得最过分的一种程序文档。事实上,很多程序甚至不需要流程图,很少有程序需要一页纸以上的流程图。
流程图显示了程序的流程判断结构,它仅仅是程序结构的一个方面。当流程图绘制在一张图上时,它能非常优雅地显示程序的判断流向,但当它被分成几张时,也就是说需要采用经过编号的出口和连接符来进行拼装时,整体结构的概观就严重地被破坏了。
因此,一页纸的流程图,成为表达程序结构、阶段或步骤的一种非常基本的图示。同样,它也非常容易绘制。图15.1展示了一个子程序流程图的图样。
图15.1:程序结构图(Courtesy of W. V. Wright)
当然,上述图纸既没有,也不需要遵循精心制订的ANSI流程图标准。所有图形元素如方框、连线、编号等,只需要能使这张详细的流程图可以理解就行了。
因此,逐一记录的详细流程图过时而且令人生厌,它只适合启蒙初学者的算法思维。
当Goldstine和Neumann1引入这种方法时,框图和框图中的内容作为一种高级别语言,将难以理解的机器语言组合成一连串可理解的步骤。如同早期Iverson所认识到的2,在系统化的高级语言中,分组已经完成,每一个方框相应地包含了一条语句(图15.2)。从而,方框本身变成了一件单调乏味的重复练习,可以去掉它们。这时,剩下的只有箭头。而连接相邻后续语句的箭头也是冗余的,可以擦掉它们。现在,留下的只有GO TO跳转。如果大家遵守良好的规则,使用块结构来消除GO TO语句,那么所有的箭头都消失了,尽管这些箭头能在很大程度上帮助理解。大家完全可以丢掉流程图,使用文字列表来表达这些内容。
现实中,流程图被鼓吹的程度远大于它们的实际作用。我从来没有看到过一个有经验的编程人员,在开始编写程序之前,会例行公事地绘制详尽的流程图。在一些要求流程图的组织中,流程图总是事后才补上。一些公司则很自豪地使用工具软件,从代码中生成这个“不可缺少的设计工具”。我认为这种普遍经验并不是令人尴尬和惋惜的对良好实践的偏离(似乎大家只能对它露出窘迫的微笑),相反,它是对技术的良好评判,向我们传授了一些流程图用途方面的知识。
耶稣门徒彼得谈到新的异教皈依者和犹太戒律时说道,“为什么让他们背负我们的祖先和我们自己都不能承担的重负呢?”(《使徒行传》 15:10 现代英文版本)。对于新的编程人员和陈旧的流程图方法,我持有相同的观点。
自文档化(self-documenting)的程序
数据处理的基本原理告诉我们,试图把信息放在不同的文件中,并努力维持它们之间的同步,是一种非常费力不讨好的事情。更合理的方法是:每个数据项包含两个文件都需要的所有信息,采用指定的键值来区别,并把它们组合到一个文件中。
不过,我们在程序文档编制的实践中却违反了我们自己的原则。典型的,我们试图维护一份机器可读的程序,以及一系列包含记叙性文字和流程图的文档。
结果和我们自己的认识相吻合。不同文件的数据保存带来了不良的后果。程序文档质量声名狼藉,文档的维护更是低劣:程序变动总是不能及时精确地反映在文档中。
我认为相应的解决方案是“合并文件”,即把文档整合到源代码。这对正确维护是直接有力的推动,保证编程用户能方便、即时地得到文档资料。这种程序被称为自文档化
(self-documenting)。
图15.2:流程图和对应程序的对比[节选自Thomas J. Cashman和Willian J. Keys(Harper & Row,1971)所著的“Data Processing and Computer Programming: A Modular Approach”中的图15-41、15-44]
现在看来,在程序中包括流程图显然是一种笨拙(但不是不可以)的做法。考虑到流程图方法的落后和高级语言的使用占统治地位,把程序和文档放在一起显然是很合理的。
把源程序作为文档介质强制推行了一些约束。另一方面,对于文档读者而言,一行一
行的源程序本身就可以再次利用,使新技术的使用成为可能。现在,已经到了为程序文档设计一套彻底的新方法的时候了。
文档是我们以及前人都不曾成功背负的重担。作为基本目标,我们必须试图把它的负担降到最小。
方法。第一个想法是借助那些出于语言的要求而必须存在的语句,来附加尽可能多的“文档”信息。因此,标签、声明语句、符号名称均可以作为工具,用来向读者表达尽可能多的意思。
第二个方法是尽可能地使用空格和一致的格式提高程序的可读性,表现从属和嵌套关系。
第三,以段落注释的形式,向程序中插入必要的记叙性文字。大多数文档一般都包括足够多的逐行注释,特别是那些满足公司呆板的“良好文档”规范的程序,通常就包含了很多注释。即使是这些程序,在段落注释方面也常常是不够的,而段落注释能提供总体把握和真正加深读者对整件事情的理解。
因为文档是通过程序结构、命名和格式来实现的,所有这些必须在书写代码时完成。不过,这也只是应该完成的时间。另外,由于自文档化的方法减少了很多附加工作,使这件工作遇到的障碍会更少。
一些技巧。图15.3是一段自文档化的PL/I程序3。圆圈中的数字不是程序的组成部分,而是用来帮助我们进行讨论。
图15.3:一段子文档化程序
1. 为每次运算使用单独的任务名称。维护一份日志,记录程序运行的目的、时间和结
果。如果名称由一个助记符(这里是QLT)和数字后缀(4)组成,那么后缀可以作为运算编号,把列表和日志联系在一起。这种技术要求为每次运算准备新的任务卡,不过这项工作可以采用“重复进行公共信息的批处理”来完成。
2. 使用包含版本号和能帮助记忆的程序名称。即,假设程序将会有很多版本。例子中使用的是1967年的最低一位数字。
3. 在过程(PROCEDURE)的注释中,包含记叙性的描述文字。
4. 尽可能为基本算法提供参考引用,通常它会指向更完备的处理方法。这样,既节省了空间,同时还允许那些有经验的读者能非常自信地略过这一段内容。
5. 显示和算法书籍中的传统算法的关系。
a) 更改 b) 定制细化 c) 重新表达
6. 声明所有的变量。采用助记符,并使用注释把DECLARE转化成完整的说明。注意,声明已经包含了名称和结构性描述,需要增加的仅仅是对目的的解释。通过这种方式,可以避免在不同的处理中重复名称和结构性的描述。
7. 用标签标记出初始化的位置。
8. 对程序语句进行分组和标记,以显示与设计文档中语句单元的一致性。
9. 利用缩进表现结构和分组。
10. 在程序列表中,手工添加逻辑箭头。它们对调试和变更非常有帮助。它们还可以补充在页面右边的空白处(注释区域),成为机器可读文字的一部分。
11. 使用行注释来解释任何不很清楚的事情。如果采用了上述技术,那么注释的长度和数量都将小于传统惯例。
12. 把多条语句放置在一行,或者把一条语句拆放在若干行,以吻合逻辑思维,表示和其他算法描述一致。
为什么不?这种方法的缺点在什么地方?很多曾经遇到的困难,已经随着技术的进步逐渐解决了。
最强烈的反对来自必须存储的源代码规模的增加。随着编程技术越来越向在线源代码存储的方向发展,这成为了一个主要的考虑因素。我发现自己编写的APL程序注释比PL/I程序要少,这是因为APL程序保存在磁盘上,而PL/I则以卡片的形式存储。
然而,与此同时文本编辑的访问和修改,也在朝在线存储的方向前进。就像前面讨论过的,程序和文字的混合使用减少了需要存储的字符总数。
对于文档化程序需要更多输入击键的争论,也有类似的答案。采用打字方式,每份草稿、每个字符需要至少一次击键。而自文档化程序的字符总数更少,每个字符需要的击键次数也更少,并且电子草稿不需要重复打印。
那么流程图和结构图的情况又如何呢?如果仅仅使用最高级别的结构图,那么另外使用一份文档的方法可能更安全一些,因为结构通常不会频繁变化。它理所当然也可以作为注释合并到文档中。这显然是一种聪明的作法。
以上讨论的用于文档和软件汇编的方法到底有多大的应用范围呢?我认为“自文档化”方法的基本思想可以得到大规模的应用。““自文档化”方法”对空间和格式要求更为严格,这一点的应用可能会受限;而命名和结构化声明显然可以利用起来,在这方面,宏可以起到很大的帮助;另外,段落注释的广泛使用在任何语言中都是一个很棒的实践。
自文档化方法激发了高级语言的使用,特别是用于在线系统的高级语言——无论是对批处理还是交互式,它都表现出最强的功效和应用的理由。如同我曾经提到的,上述语言和系统强有力地帮助了编程人员。因为是机器为人服务,而不是人为机器服务。因此从各个方面而言,无论是从经济上还是从以人为本的角度来说,它们的应用都是非常合情合理的。
没有银弹-软件工程中的根本和次要问题(No Silver Bullet – Essence and Accident in Software Engineering)
没有任何技术或管理上的进展,能够独立地许诺十年内使生产率、可靠性或简洁性获得数量级上的进步。
There is no single development, in either technology or management technique, which by itself promises even one order-of-magnitude improvement within a decade in productivity, in reliability, in simplicity.
摘要1
所有软件活动包括根本任务——打造由抽象软件实体构成的复杂概念结构,次要任务——使用编程语言表达这些抽象实体,在空间和时间限制内将它们映射成机器语言。软件生产率在近年内取得的巨大进步来自对后天障碍的突破,例如硬件的限制、笨拙的编程语言、机器时间的缺乏等等。这些障碍使次要任务实施起来异常艰难,相对必要任务而言,软件工程师在次要任务上花费了多少时间和精力?除非它占了所有工作的9/10,否则即使全部次要任务的时间缩减到零,也不会给生产率带来数量级上的提高。
因此,现在是关注软件任务中的必要活动的时候了,也就是那些和构造异常复杂的抽象概念结构有关的部分。我建议:
仔细地进行市场调研,避免开发已上市的产品。
在获取和制订软件需求时,将快速原型开发作为迭代计划的一部分。
有机地更新软件,随着系统的运行、使用和测试,逐渐添加越来越多的功能。
不断挑选和培养杰出的概念设计人员。
介绍
在所有恐怖民间传说的妖怪中,最可怕的是人狼,因为它们可以完全出乎意料地从熟悉的面孔变成可怕的怪物。为了对付人狼,我们在寻找可以消灭它们的银弹。
大家熟悉的软件项目具有一些人狼的特性(至少在非技术经理看来),常常看似简单明了的东西,却有可能变成一个落后进度、超出预算、存在大量缺陷的怪物。因此,我们听到了近乎绝望的寻求银弹的呼唤,寻求一种可以使软件成本像计算机硬件成本一样降低的尚方宝剑。
但是,我们看看近十年来的情况,没有银弹的踪迹。没有任何技术或管理上的进展,能够独立地许诺在生产率、可靠性或简洁性上取得数量级的提高。本章中,我们试图通过分析软件问题的本质和很多候选银弹的特征,来探索其原因。
不过,怀疑论者并不是悲观主义者。尽管我们没有看见令人惊异的突破,并认为这种银弹实际上是与软件的内在特性相悖,不过还是出现了一些令人振奋的革新。这些方法的规范化、持续地开拓、发展和传播确实是可以在将来使生产率产生数量级上的提高。虽然没有通天大道,但是路就在脚下。
解决管理灾难的第一步是将大块的“巨无霸理论”替换成“微生物理论”,它的每一步——希望的诞生,本身就是对一蹴而就型解决方案的冲击。它告诉工作者进步是逐步取得的,伴随着辛勤的劳动,对规范化过程应进行持续不懈的努力。由此,诞生了现在的软件工程。
是否一定那么困难呢?——根本困难
不仅仅是在目力所及的范围内,没有发现银弹,而且软件的特性本身也导致了不大可能有任何的发明创新——能够像计算机硬件工业中的微电子器件、晶体管、大规模集成一样——提高软件的生产率、可靠性和简洁程度。我们甚至不能期望每两年有一倍的增长。
首先,我们必须看到这样的畸形并不是由于软件发展得太慢,而是因为计算机硬件发展得太快。从人类文明开始,没有任何其他产业技术的性价比,能在30年之内取得6个数量级的提高,也没有任何一个产业可以在性能提高或者降低成本方面取得如此的进步。这些
进步来自计算机制造产业的转变,从装配工业转变成流水线工业。
其次,让我们通过观察预期的软件技术产业发展速度,来了解中间的困难。效仿亚里士多德,我将它们分成根本的——软件特性中固有的困难,次要的——出现在目前生产上的,但并非那些与生俱来的困难。
我们在下一章中讨论次要问题。首先,来关注内在、必要的问题。
一个相互牵制关联的概念结构,是软件实体必不可少的部分,它包括:数据集合、数据条目之间的关系、算法、功能调用等等。这些要素本身是抽象的,体现在相同的概念构架中,可以存在不同的表现形式。尽管如此,它仍然是内容丰富和高度精确的。
我认为软件开发中困难的部分是规格化、设计和测试这些概念上的结构,而不是对概念进行表达和对实现逼真程度进行验证。当然,我们还是会犯一些语法错误,但是和绝大多数系统中的概念错误相比,它们是微不足道的。
如果这是事实,那么软件开发总是非常困难的。天生就没有银弹。
让我们来考虑现代软件系统中这些无法规避的内在特性:复杂度、一致性、可变性和不可见性。
复杂度。规模上,软件实体可能比任何由人类创造的其他实体要复杂,因为没有任何两个软件部分是相同的(至少是在语句的级别)。如果有相同的情况,我们会把它们合并成供调用的子函数。在这个方面,软件系统与计算机、建筑或者汽车大不相同,后者往往存在着大量重复的部分。
数字计算机本身就比人类建造的大多数东西复杂。计算机拥有大量的状态,这使得构思、描述和测试都非常困难。软件系统的状态又比计算机系统状态多若干个数量级。
同样,软件实体的扩展也不仅仅是相同元素重复添加,而必须是不同元素实体的添加。大多数情况下,这些元素以非线性递增的方式交互,因此整个软件的复杂度以更大的非线性级数增长。
软件的复杂度是必要属性,不是次要因素。因此,抽掉复杂度的软件实体描述常常也去掉了一些本质属性。数学和物理学在过去三个世纪取得了巨大的进步,数学家和物理学家们建立模型以简化复杂的现象,从模型中抽取出各种特性,并通过试验来验证这些特性。这
些方法之所以可行——是因为模型中忽略的复杂度不是被研究现象的必要属性。当复杂度是本质特性时,这些方法就行不通了。
上述软件特有的复杂度问题造成了很多经典的软件产品开发问题。由于复杂度,团队成员之间的沟通非常困难,导致了产品瑕疵、成本超支和进度延迟;由于复杂度,列举和理解所有可能的状态十分困难,影响了产品的可靠性;由于函数的复杂度,函数调用变得困难,导致程序难以使用;由于结构性复杂度,程序难以在不产生副作用的情况下用新函数扩充;由于结构性复杂度,造成很多安全机制状态上的不可见性。
复杂度不仅仅导致技术上的困难,还引发了很多管理上的问题。它使全面理解问题变得困难,从而妨碍了概念上的完整性;它使所有离散出口难以寻找和控制;它引起了大量学习和理解上的负担,使开发慢慢演变成了一场灾难。
一致性。并不是只有软件工程师才面对复杂问题。物理学家甚至在非常“基础”的级别上,面对异常复杂的事物。不过,物理学家坚信必定存在着某种通用原理,或者在夸克中,或者在统一场论中。爱因斯坦曾不断地重申自然界一定存在着简化的解释,因为上帝不是专横武断或反复无常的。
软件工程师却无法从类似的信念中获得安慰,他必须控制的很多复杂度是随心所欲、毫无规则可言的,来自若干必须遵循的人为惯例和系统。它们随接口的不同而改变,随时间的推移而变化,而且,这些变化不是必需的,仅仅由于它们是不同的人——而非上帝——设计的结果。
某些情况下,因为是开发最新的软件,所以它必须遵循各种接口。另一些情况下,软件的开发目标就是兼容性。在上述的所有情况中,很多复杂性来自保持与其他接口的一致,对软件的任何再设计,都无法简化这些复杂特性。
可变性。软件实体经常会遭受到持续的变更压力。当然,建筑、汽车、计算机也是如此。不过,工业制造的产品在出厂之后不会经常地发生修改,它们会被后续模型所取代,或者必要更改会被整合到具有相同基本设计的后续产品系列。汽车的更改十分罕见,计算机的现场调整时有发生。然而,它们和软件的现场修改比起来,都要少很多。
其中部分的原因是因为系统中的软件包含了很多功能,而功能是最容易感受变更压力的部分。另外的原因是因为软件可以很容易地进行修改——它是纯粹思维活动的产物,可以
无限扩展。日常生活中,建筑有可能发生变化,但众所周知,建筑修改的成本很高,从而打消了那些想提出修改的人的念头。
所有成功的软件都会发生变更。现实工作中,经常发生两种情况。当人们发现软件很有用时,会在原有应用范围的边界,或者在超越边界的情况下使用软件。功能扩展的压力主要来自那些喜欢基本功能,又对软件提出了很多新用法的用户们。
其次,软件一定是在某种计算机硬件平台上开发,成功软件的生命期通常比当初的计算机硬件平台要长。即使不是更换计算机,则有可能是换新型号的磁盘、显示器或者打印机。软件必须与各种新生事物保持一致。
简言之,软件产品扎根于文化的母体中,如各种应用、用户、自然及社会规律、计算机硬件等等。后者持续不断地变化着,这些变化无情地强迫着软件随之变化。
不可见性。软件是不可见的和无法可视化的。例如,几何抽象是强大的工具。建筑平面图能帮助建筑师和客户一起评估空间布局、进出的运输流量和各个角度的视觉效果。这样,矛盾变得突出,忽略的地方变得明显。同样,机械制图、化学分子模型尽管是抽象模型,但都起了相同的作用。总之,都可以通过几何抽象来捕获物理存在的几何特性。
软件的客观存在不具有空间的形体特征。因此,没有已有的表达方式,就像陆地海洋有地图、硅片有膜片图、计算机有电路图一样。当我们试图用图形来描述软件结构时,我们发现它不仅仅包含一个,而是很多相互关联、重叠在一起的图形。这些图形可能描绘控制流程、数据流、依赖关系、时间序列、名字空间的相互关系等等。它们通常不是有较少层次的扁平结构。实际上,在上述结构上建立概念控制的一种方法是强制将关联分割,直到可以层次化一个或多个图形2。
除去软件结构上的限制和简化方面的进展,软件仍然保持着无法可视化的固有特性,从而剥夺了一些具有强大功能的概念工具的构造思路。这种缺憾不仅限制了个人的设计过程,也严重地阻碍了相互之间的交流。
以往解决次要困难的一些突破
如果回顾一下软件领域中取得的最富有成效的三次进步,我们会发现每一次都是解决了软件构建上的巨大困难,但是这些困难不是本质属性,也不是主要困难。同样,我们可以
对每一次进步进行外推,来了解它们的固有限制。
高级语言。勿庸置疑,软件生产率、可靠性和简洁性上最有力的突破是使用高级语言编程。大多数观察者相信开发生产率至少提高了五倍,同时可靠性、简洁性和理解程度也大为提高。
那么,高级语言取得了哪些进展呢?首先,它减轻了一些次要的软件复杂度。抽象程序包含了很多概念上的要素:操作、数据类型、流程和相互通讯,而具体的机器语言程序则关心位、寄存器、条件、分支、通道、磁盘等等。高级语言所达到的抽象程度包含了(抽象)程序所需要的要素,避免了更低级的元素,它消除了并不是程序所固有的整个级别的复杂度。
高级语言最可能实现的是提供所有编程人员在抽象程序中能想到的要素。可以肯定的是,我们思考数据结构、数据类型和操作的速度稳固提高,不过是以非常缓慢的速度。另外,程序开发方法越来越接近用户的复杂度。
然而,对于较少使用那些复杂深奥语言要素的用户,高级语言在某种程度上增加而不是减少了脑力劳动上的负担。
分时。大多数观察者相信分时提高了程序员的生产率和产品的质量,尽管它带来的进步不如高级语言。
分时解决了完全不同的困难。分时保证了及时性,从而使我们能维持对复杂程度的一个总体把握。批处理编程的较长周转时间意味着不可避免会遗忘一些细枝末节,如果我们停下编程,调用编译程序或者执行程序,思维上的中断使我们不得不重新进行思考,它在时间上的代价非常高昂。最严重的结果可能是失去对复杂系统的掌握。
较长的周转时间和机器语言的复杂度一样,是软件开发过程的次要困难,而不是本质困难。分时所起作用也非常有限。主要效果是缩短了系统的响应时间。随着它接近于零,到达人类可以辨识的基本能力——大概100毫秒时,所获得的好处就接近于无了。
统一编程环境。第一个集成开发环境——Unix和Interlisp现在已经得到了广泛应用,并且使生产率提高了5倍。为什么?
它们主要通过提供集成库、统一文件格式、管道和过滤器,解决了共同使用程序的次要困难。这样,概念性结构理论上的相互调用、提供输入和互相使用,在现实中可以非常容易地实现。
因为每个新工具可以通过标准格式在任何一个程序中应用,这种突破接着又激发整个工具库的开发。
由于这些成功,环境开发是当今软件工程研究的主要题目。我们将在下章中讨论期望达到的目标和限制。
银弹的希望
现在,让我们来讨论一下当今可能作为潜在银弹的最先进的技术进步。它们各自针对什么样的问题?它们是属于必要问题,或者依然是解决我们剩下的次要困难?它们是提供了创新,还是仅仅是增量改进?
Ada和其他高级编程语言。近来,最被吹捧的开发进展之一是编程语言Ada,一种80年代的高级语言。Ada实际上不仅仅反映了语言概念上的突破性进展,而且蕴涵了鼓励现代设计和模块化概念运用的重要特性。由于Ada采用的是抽象数据类型、层次结构的模块化理念,所有Ada理念可能比语言本身更加先进。Ada使用设计来承载需求,作为这一过程的自然产物,它可能过于丰富了。不过,这并不是致命的,因为它的词汇子集可以解决学习问题,硬件的进展能提供更高的MIPS(每秒百万指令集),减少编译的成本。软件系统结构化的先进理念实际上非常好地利用了MIPS上的进展。60年代,曾在内存和速度成本上广受谴责的操作系统,如今已被证明是一种能使用某些MIPS和廉价内存的非常优秀的系统。
然而,Ada仍然不是消灭软件生产率怪兽的银弹。毕竟,它只是另一种高级语言,这类语言出现最大的回报来自出现时的冲击,它通过使用更加抽象的语句来开发,降低了机器的次要复杂度。一旦这些难题被解决,就只剩下非常少的问题,解决剩余部分的获益必然也要少一些。
我预言在以后的十年中,当Ada的效率被大家评估认可时,它会带来相当大的变化,这并不是因为任何特别的语言特性,不是由于这些语言特性被合并在一起,也不是因为Ada开发环境会不断发展进步。Ada的最大贡献在于编程人员培训方式的转变,即对开发人员需要进行现代软件设计技术培训。