饭饭TXT > 学习管理 > 《人月神话》作者:[美]弗雷德里克·布鲁克斯【完结】 > 人月神话.TXT

第 6 页

作者:美-弗雷德里克·布鲁克斯 当前章节:15417 字 更新时间:2026-6-22 20:55

前进一步,后退一步

Lehman和Belady研究了大型操作系统的一系列发布版本的历史6。他们发现模块数量随版本号的增加呈线性增长,但是受到影响的模块以版本号指数的级别增长。所有修改都倾向于破坏系统的架构,增加了系统的混乱程度。用在修复原有设计上瑕疵的工作量越来越少,而早期维护活动本身的漏洞所引起修复工作越来越多。随着时间的推移,系统变得越来越无序,修复工作迟早会失去根基。每一步前进都伴随着一步后退。尽管理论上系统一直可用,但实际上,整个系统已经面目全非,无法再成为下一步进展的基础。而且,机器在变化,配置在变化,用户的需求在变化,所以现实系统不可能永远可用。崭新的、基于原有系统的重新设计是完全必要的。

通过对统计模型的研究,关于软件系统,Belady和Lehman得到了更具普遍意义、为所有经验支持的结论。正如Pascal. C. S. Lewis所敏锐指出的:

这正是历史的关键。使用卓越的能源——构建文明——成立杰出的机构,但是每次总会出现问题。一些致命的缺陷会将自私和残酷的人带到塔尖,接着一切开始滑落,回到到痛苦和堕落。实际上,机器失灵了。看上去,就好像是机器正常启动,跑了几步,然后垮掉了

7。

系统软件开发是减少混乱度(减少熵)的过程,所以它本身是处于亚稳态的。软件维护是提高混乱度(增加熵)的过程,即使是最熟练的软件维护工作,也只是放缓了系统退化到非稳态的进程。

干将莫邪(Sharp Tools)

巧匠因为他的工具而出名。

- 谚语

A good workman is known by his tools.

- PROVERB

就工具而言,即使是现在,很多软件项目仍然像一家五金店。每个骨干人员都仔细地保管自己工作生涯中搜集的一套工具集,这些工具成为个人技能的直观证明。正是如此,每个编程人员也保留着编辑器、排序、内存信息转储、磁盘实用程序等工具。

这种方法对软件项目来说是愚蠢的。首先,项目的关键问题是沟通,个性化的工具妨碍——而不是促进沟通。其次,当机器和语言发生变化时,技术也会随之变化,所有工具的生命周期是很短的。毫无疑问,开发和维护公共的通用编程工具的效率更高。

不过,仅有通用工具是不够的。专业需要和个人偏好同样需要很多专业工具。所以在前面关于软件开发队伍的讨论中,我建议为每个团队配备一名工具管理人员。这个角色管理所有通用工具,能指导他的客户-老板使用工具。同时,他还能编制老板需要的专业工具。

因此,项目经理应该制订一套策略,并为通用工具的开发分配资源。与此同时,他还必须意识到专业工具的需求,对这类工具不能吝啬人力和物力——这种企图的危害非常隐蔽。可能有人会觉得,将所有分散的人员集结起来,形成一个公共的工具小组,会有更高的效率。但实际上却不是这样。

项目经理必须考虑、计划、组织的工具到底有哪些呢?首先是计算机设施。它需要硬件和使用安排策略;它需要操作系统,提供服务的方式必须明了;它需要语言,语言的使用方针必须明确;然后是实用程序、调试辅助程序、测试用例生成工具和处理文档的字处理系统。接下面我们逐一讨论它们1。

目标机器

机器支持可以有效地划分成目标机器和辅助机器。目标机器是软件所服务的对象,程序必须在该机器上进行最后测试。辅助机器是那些在开发系统中提供服务的机器。如果是在为原有的机型开发操作系统,则该机器不仅充当目标机器的角色,同时也作为辅助机器。

目标机器的类型有哪些?团队开发的监督程序或其他系统核心软件当然需要它们自己的机器。目标机器系统会需要若干操作员和一两个系统编程人员,以保证机器上的标准支持是即时更新和实时可用的。

如果还需要其他的机器,那么将是一件很古怪的东西——运行速度不必非常快,但至少要若干兆字节的主存,百兆字节的在线硬盘和终端。字符型终端即可满足要求,但是它必须比15字符/每分的打字机速度要快。大容量内存可以进行进程覆盖(overlay)和功能测试之后的剪裁工作,从而极大地提高生产率。

另外,还需要配备调试机器或者软件。这样,在调试过程中,所有类型的程序参数可以被自动计数和测量。例如,内存使用模式是非常强大的诊断措施,能查出程序中不可思议的行为或者性能意外下降的原因。

计划安排。当目标机器刚刚被研制,或者当它的第一个操作系统被开发时,机器时间是非常匮乏的,时间的调度安排成了主要问题。目标机器时间需求具有特别的增长曲线。在OS/360开发中,我们有很好的System/360仿真器和其他的辅助设施,并根据以前的经验,我们计划出System/360的使用时间(小时数),向制造商提前预定了机器。不过,起初它们日复一日地处于空闲状态。突然有一天,所有16个系统全部上线,这时资源配给成了严重问题。实际使用情况如图12.1所示。每个人在同一时间,开始调试自己的第一个组件,然后团队大多数成员持续地进行某些调试工作。

我们集中了所有的机器和磁带库,并组建了一个富有经验的专业团队来操作它们。为了最大限度地利用S/360的时间,我们在任何系统空闲和可能的时间里,以批处理方式运行所有运算任务。我们尝试了每天运行四次(周转时间为两个半小时),而实际要求的周转时间为四小时。我们使用了一台带有终端的1401辅助机器来进行调度,跟踪成千上万的任务,监督时间周期。

Model40的每月使用小时数

图12.1:目标机器使用的增长曲线

但是整个开发队伍实在是过度运转了。在经过了几个月的缓慢周转、相互指责、极度痛苦之后,我们开始把机器时间分配成连续的块。例如,整个从事排序工作的15人小组,会得到系统4至6小时的使用时间块,由他们自己决定如何使用。即使没有安排,其他人也不能使用机器资源。

这种方式,是一种更好的分配和安排方法。尽管机器的利用程度可能会有些降低(常常不是这样),生产率却提高了。上述小组中的每个人,6小时中连续10次操作的生产率,比间隔3小时的10次操作要高许多,因为持续的精力集中能减少思考时间。在这样的冲刺之后,提出下一个时间块要求之前,小组通常需要一到两天的时间来从事书面文档工作。并且,通常3人左右的小组能卓有成效地安排和共享时间块。在调试新操作系统时,这似乎是一种使用目标机器的最好方法。

上述方法尽管没有在任何理论中被提及,在实际情况中却一直如此。另外,同天文工作者一样,系统调试总是夜班性质的工作。二十年前,当所有机房负责人在家中安睡时,我正工作在701上。三代机器过去了,技术完全改变了,操作系统出现了,然而大家喜好的工作方式没有改变。这种工作方式得以延续,是因为它的生产率最高。现在,人们已开始认识到它的生产力,并且敞开地接受这种富有成效的实践。

辅助机器和数据服务

仿真装置。如果目标机器是新产品,则需要一个目标机器的逻辑仿真装置。这样,在生产出新机器之前,就有辅助的调试平台可供使用。同样重要的是——即使在新机器出现之

后,仿真装置仍然可以提供可靠的调试平台。

可靠并不等于精确。在某些方面,仿真机器肯定无法精确地达到与新型机器一致的实现。但是至少在一段时间内,它的实现是稳定的,新硬件就不会。

现在,我们已经习惯于计算机硬件自始至终能正常工作。除非程序开发人员发现相同运算在运行时会产生不一致的结果,否则出错时,他都会被建议去检查自己代码中的错误,而不是去怀疑他的运行平台。

这样的经验,对于支持新型机器的编程工作来说,是不好的。实验室研制和试制的模型产品和早期硬件不会像定义的那样运行,不会稳定工作,甚至每天都不会一样。当一些缺陷被发现时,所有的机器拷贝,包括软件编程小组所使用的,都会发生修改。这种飘忽不定的开发基础实在是够糟的。而硬件失败,通常是间歇性的,导致情况更加恶劣。不确定性是所有情况中最糟糕的,因为它剥夺了开发人员查找bug的动力——可能根本就没有问题。所以,一套运行在稳定平台上的可靠仿真装置,提供了远大于我们所期望的功用。

编译器和汇编平台。出于同样的原因,编译器和汇编软件需要运行在可靠的辅助平台上,为目标机器编译目标代码。接着,可以在仿真器上立刻开始后续的调试。

高级语言的编程开发中,在目标机器上开始全面测试目标代码之前,编译器可以在辅助机器上完成很多目标代码的调试和测试工作。这为直接运行提供了支持,而不仅仅是稳定机器上的仿真结果。

程序库和管理。在OS/360开发中,一个非常成功的重要辅助机器应用是维护程序库。该系统由W. R. Crowley带领开发,连接两台7010机器,共享一个很大的磁盘数据库。7010同时还提供System/360汇编程序。所有经过测试或者正在测试的代码都保存在该库中,包括源代码和汇编装载模块。这个库实际上划分成不同访问规则下的子库。

首先,每个组或者编程人员分配了一个区域,用来存放他的程序拷贝、测试用例以及单元测试需要的测试辅助例程和数据。在这个开发库(playpen)中,不存在任何限制开发人员的规定。他可以自由处置自己的程序,他是它们的拥有者。

当开发人员准备将软件单元集成到更大的部分时,他向集成经理提交一份拷贝,后者将拷贝放置在系统集成子库中。此时,原作者不可以再改变代码,除非得到了集成经理的批准。当系统合并在一起时,集成经理开始进行所有的系统测试工作,识别和修补bug。

有时,系统的一个版本可能会被广泛应用,它被提升到当前版本子库。此时,这个拷贝是不可更改的,除非有重大缺陷。该版本可以用于所有新模块的集成和测试。7010上的一个程序目录对每个模块的每个版本进行跟踪,包括它的状态、用途和变更。

这有两个重要的理念。首先是受控,即程序的拷贝属于经理,他可以独立地授权程序的变更。其次是使发布的进展变得正式,以及开发库(playpen)与集成、发布的正式分离。

在我看来,这是OS/360工作中最优秀的成果之一。它实际上是管理技术的一部分,很多大型的项目都独立地发展了这些技术2,包括Bell试验室、ICL、剑桥大学等。它同样适用于文档,是一种不可缺少的技术。

编程工具。随着调试技术的出现,旧方法的使用减少了,但并没有消失。因此,还是需要内存转储、源文件编辑、快照转储、甚至跟踪等工具。

与之类似,一整套实用程序同样是必要的,用来实现磁带走带、拷贝磁盘、打印文件、更改目录等工作。如果一开始就任命了项目的工具操作和维护人员,那么这些工作可以一次完成,并且随时处在待命状态。

文档系统。在所有的工具中,最能节省劳动力的,可能是运行在可靠平台上的、计算机化的文本编辑系统。我们有一套使用非常方便的系统,由J. W. Franklin发明。没有它,OS/360手册的进度可能会远远落后,而且更加晦涩难懂。另外,对于6英尺的OS/360手册,很多人认为它表达的是一大堆口头垃圾,巨大容量带来了新的不理解问题——这种观点有一些道理。

对此,我通过两种途径作出了反应。首先,OS/360的文档规模是不可避免的,需要制订仔细的阅读计划。如果选择性地阅读,则可以忽略大部分内容和省下大量时间。人们必须把OS/360的文档看成是图书馆或者百科全书,而不是一系列强制阅读的文章。

第二,它比那些刻画了大多数编程系统特性的短篇文档更加可取。不过,我也承认,手册仍有某些需要大量改进的地方,经改进后文档篇幅会大大减少。事实上,某些部分(“概念和设施”)已经被很好地改写了。

性能仿真装置。最好有一个。正如我们将在下章讨论到的,彻底地开发一个。使用相同的自顶向下设计方法,来实现性能仿真器、逻辑仿真装置和产品。尽可能早地开始这项工作,仔细地听取“它们表达的意见”。

高级语言和交互式编程

在十年前的OS/360开发中,并没有使用现在最重要的两种系统编程工具。目前,它们也没有得到广泛应用,但是所有证据都证明它们的功效和适用。他们是(1)高级语言和(2)交互式编程。我确信只有懒散和惰性会妨碍它们的广泛应用,技术上的困难很快就不再成为借口。

高级语言。使用高级语言的主要原因是生产率和调试速度。我们在前面已讨论过生产率的问题(第8章)。其中,并没有提到大量的数字论据,但是所体现出来的是整体提升,而不仅仅是部分增加。

调试上的改进来自下列事实——存在更少的bug,而且更容易查找。bug更少的原因,是因为它避免在错误面前暴露所有级别的工作,这样不但会造成语法上的错误,还会产生语义上的问题,如不当使用寄存器等。编译器的诊断机制可以帮助找出这些类似的错误,更重要的是,它非常容易插入调试的快照。

就我而言,这些生产率和调试方面的优势是势不可挡的。我无法想象使用汇编语言能方便地开发出系统软件。

那么,上述工具的传统反对意见有哪些呢?这里有三点:它无法完成我想做的事情;目标代码过于庞大;目标代码运行速度过慢。

就功能而言,我相信反对不再存在。所有证据都显示了人们可以完成想做的事情,只是需要花费时间和精力找出如何做而已,这可能需要一些讨人嫌的技巧3,4。

就空间而言,新的优化编译器已非常令人满意,并且将持续地改进。

就速度而言,经优化编译器生成的代码,比绝大多数程序员手写代码的效率要高。而且,在前者被全面测试之后,可以将其中的百分之一至五替换成手写的代码,这往往能解决速度方面的问题5。

系统编程需要什么样的高级语言呢?现在可供合理选择的语言是PL/I6。它提供完整的功能集;它与操作系统环境相吻合;它有各种各样的编译器,一些是交互式的,一些速度很快,一些诊断性很好,另一些能产生优化程度很高的代码。我自己觉得使用APL来解决算法更快一些,然后,将它们翻译成某个系统环境下的PL/I语言。

交互式编程。MIT的Multics项目的成果之一,是它对软件编程系统开发的贡献。在那些系统编程所关注的方面,Multics(以及后续系统,IBM的TSS)和其他交互式计算机系统在概念上有很大的不同:多个级别上数据和程序的共享和保护,可延伸的库管理,以及协助终端用户共同开发的设施。我确信在某些应用上,批处理系统决不会被交互式系统所取代。但是,我认为Multics小组是交互式系统开发上最具有说服力的成功案例。

然而,目前还没有非常明显的证据来证明这些功能强大的工具的效力。正如人们所普遍认识的那样,调试是系统编程中很慢和较困难的部分,而漫长的调试周转时间是调试的祸根。就这一点而言,交互式编程的逻辑合理性是勿庸置疑的7。

另外,从很多采用这种方式了开发小型系统和系统某个部分的人那里,我们听到了很多好的证据。我唯一见到的关于大型编程系统开发方面的数字,来自Bell实验室John Harr的论文。它们如图12.2所示。这些数字分别反映了代码编写、汇编装配和程序调试的情况。第一个大部分是控制程序;其他三个则是语言解释、编辑等程序。Harr的数据表明了系统软件开发中,交互式编程的生产率至少是原来的两倍8。

程序

规模

批处理(B)或交互式(C)

指令/人年

ESS代码

800,000

B

500-1000

7094 ESS支持

120,000

B

2100-3400

360 ESS支持

32,000

C

8000

360 ESS支持

8,300

B

4000

图12.2:批处理和交互式编程生产率的对比

由于远程键盘终端无法用于内存转储的调试,大多数交互式工具的有效使用需要采用高级语言来进行开发。有了高级语言,可以很容易地修改代码和选择性地打印结果。实际上,它们组成了一对强大的工具。

整体部分(The Whole and the Parts)

我能召唤遥远的精灵。

那又怎么样,我也可以,谁都可以,问题是你真的召唤的时候,它们会来吗?

- 莎士比亚,《亨利四世》,第一部分

I can call spirits from the vasty deep.

Why, so can I, or so can any man; but will they come when you do call for them?

- SHAKESPEARE, KING HENRY IV, Part I

和古老的神话里一样,现代神话里也总有一些爱吹嘘的人:“我可以编写控制航空货运、拦截弹道导弹、管理银行账户、控制生产线的系统。”对这些人,回答很简单,“我也可以,任何人都可以,但是其他人成功了吗?”

如何开发一个可以运行的系统?如何测试系统?如何将经过测试的一系列构件集成到已测试过、可以依赖的系统?对这些问题,我们以前或多或少地提到了一些方法,现在就来更加系统地考虑一下。

剔除bug的设计

防范bug的定义。系统各个组成部分的开发者都会做出一些假设,而这些假设之间的不匹配,是大多数致命和难以察觉的bug的主要来源。第4、5、6章所讨论的获取概念完整性的途径,就是直接面对这些问题。简言之,产品的概念完整性在使它易于使用的同时,也使开发更容易进行以及bug更不容易产生。

上述方法所意味的详尽体系结构设计正是出于这个目的。Bell实验室安全监控系统项目的V.A.Vyssotsky提出,“关键的工作是产品定义。许许多多的失败完全源于那些产品未精确定义的地方。1”细致的功能定义、详细的规格说明、规范化的功能描述说明以及这些

方法的实施,大大减少了系统中必须查找的bug数量。

测试规格说明。在编写任何代码之前,规格说明必须提交给测试小组,以详细地检查说明的完整性和明确性。如同Vyssotsky所述,开发人员自己不会完成这项工作:“他们不会告诉你他们不懂。相反,他们乐于自己摸索出解决问题和澄清疑惑的办法。”

自顶向下的设计。在1971年的一篇论文中,Niklaus Wirth把一种被很多最优秀的编程人员所使用的设计流程2形式化。尽管他的理念是为了程序设计,同样也完全适用于复杂系统的软件开发设计。他将程序开发划分成体系结构设计、设计实现和物理编码实现,每个步骤可以使用自顶向下的方法很好地实现。

简言之,Wirth的流程将设计看成一系列精化步骤。开始是勾画出能得到主要结果的,但比较粗略的任务定义和大概的解决方案。然后,对该定义和方案进行细致的检查,以判断结果与期望之间的差距。同时,将上述步骤的解决方案,在更细的步骤中进行分解,每一项任务定义的精化变成了解决方案中算法的精化,后者还可能伴随着数据表达方式的精化。

在这个过程中,当识别出解决方案或者数据的模块时,对这些模块的进一步细化可以和其他的工作独立,而模块的大小程度决定了程序的适用性和可变化的程度。

Wirth主张在每个步骤中,尽可能使用级别较高的表达方法来表现概念和隐藏细节,除非有必要进行进一步的细化。

好的自顶向下设计从几个方面避免了bug。首先,清晰的结构和表达方式更容易对需求和模块功能进行精确的描述。其次,模块分割和模块独立性避免了系统级的bug。另外,细节的隐藏使结构上的缺陷更加容易识别。第四,设计在每个精化步骤的层次上是可以测试的,所以测试可以尽早开始,并且每个步骤的重点可以放在合适的级别上。

当遇到一些意想不到的问题时,按部就班的流程并不意味着步骤不能反过来,直到推翻顶层设计,重新开始整个过程。实际上,这种情况经常发生。至少,它让我们更加清楚在什么时候和为什么抛弃了某个臃肿的设计,并重新开始。一些糟糕的系统往往就是试图挽救一个基础很差的设计,而对它添加了很多表面装饰般的补丁。自顶向下的方法减少了这样的企图。

我确信在十年内,自顶向下进行设计将会是最重要的新型形式化软件开发方法。

结构化编程。另外一系列减少bug数量的新方法很大程度上来自Dijkstra3。Bohm和

Jacopini的为其提供了理论证明4。

基本上,该方法所设计程序的控制结构,仅包含语句形式的循环结构,例如DO WHILE,以及IF...THEN...ELSE的条件判断结构,而具体的条件部分在IF...THEN...ELSE后的花括号中描述。Bohm和Jacopini展示了这些结构在理论上是可以证明的。而Dijkstra认为另外一种方法,即通过GO TO不加限制的分支跳转,会产生导致自身逻辑错误的结构。

这种方法的基本理念非常优秀,但仍有人提出了一些反面的意见。一些附加的控制结构非常有效,例如,在多个条件下的多路分支(CASE、SWITCH语句),异常跳转等(GO TO ABNORMAL END)。此外,关于完全避免GO TO语句的说法显得有些教条主义,而且似乎有些吹毛求疵。

关键的地方和构建无bug程序的核心,是把系统的结构作为控制结构来考虑,而不是独立的跳转语句。这种思考方法是我们在程序设计发展史上向前迈出的一大步。

构件单元调试

程序调试过程在过去的二十年中有过很多反复,甚至在某些方面,它们又回到了出发的起点。整个调试过程有四个步骤,跟随这个过程来检验每个步骤各自的动机是一件很有趣的事情。

本机调试。早期的机器的输入和输出设备很差,延迟也很长。典型的情况是,机器采用纸带或者磁带的方式来读写,采用离线设备来完成磁带的准备和打印工作。这使得磁带输入/输出对于调试是不可忍受的。因此,在一次机器交互会话中会尽可能多地包含试验性操作。

在那种情况下,程序员仔细地设计他的调试过程——计划停止的地点,检验内存的位置,需要检查的东西以及如果没有预期结果时的对策。花费在编写调试程序上的时间,可能是程序编制时间的一半。

这个步骤的“重大罪过”是在没有把程序划分成测试段,并对执行终止位置进行计划的前提下,粗暴地按下“开始(START)”。

内存转储。本机调试非常有效。在两小时的交互过程中可能会发现一打问题,但是计算机的资源非常匮乏,成本很高。想象一下计算机时间的浪费,那实在是一件可怕的事情。

因此,当使用在线高速打印机时,测试技术发生了变化。某人持续地运行程序,直到某个检测失败,这时所有的内存都被转储。接着,他将开始艰苦的桌面工作,考虑每个内存位置的内容。桌面工作的时间和本机调试并没有太大的不同,但它的方式比以前更为含混,并且发生在测试执行之后。特定用户调试用的时间更长,因为测试依赖于批处理的周期。总之,整个过程的设计是为了减少计算机的使用时间,从而尽可能满足更多的用户。

快照。采用内存转储技术的机器往往配有2000~4000个字(word双字节),或者8K~16K字节的内存。但是,随着内存的规模不断增长,对整个内存都进行转储变得不大可能。因此,人们开发了有选择的转储、选择性跟踪和将快照插入程序的技术。OS/360 TESTRAN允许将快照插入程序,无需重新汇编和编译,它是快照技术方向的终极产品。

交互式调试。1959年,Codd和他的同事5以及Strachey6都发表了关于协助分时调试工作的论文,提出了一种兼有本机调试方式实时性和批处理调试高效使用率的方法。计算机将多个程序载入到内存中准备运行,被调试的程序和一个只能由程序控制的终端相关联,由监督调度程序控制调试过程。当终端前的编程人员停止程序,检查进展情况或者进行修改时,监督程序可以运行其他程序,从而保证了机器的使用率。

Codd的多道程序系统已经开发出来,但是它的重点是通过有效地利用输入/输出来提高吞吐量,并没有实现交互式的调试。Strachy的想法不断得到改进,终于在1963年由MIT的Corbato和他的同事在7090的实验性系统上实现7。这个开发结果导致了MULTICS、TSS和现在其他分时系统的出现。

在最初使用的本机调试方法和现在的交互式调试方法之间,用户可以感觉到的主要差异是工具性软件、调度监控程序和其它相关语言解释编译器的出现。而现在,已经可以用高级语言来编程和调试,高效的编辑工具使修改和快照更为容易。

交互式调试拥有和本机调试一样的操作实时性,但前者并没有象后者要求的那样,在调试过程中要预先进行计划。在某种程度上,像本机调试那样的预先计划显得并不是很必要,因为在调试人员停顿和思考时,计算机的时间并没有被浪费。

不过,Gold实验得到一个有趣的结果,这个结果显示在每次调试会话中,第一次交互取得的工作进展是后续交互的三倍8。这强烈地暗示着,由于缺乏对调试会话的计划,我们没有发掘交互式调试的潜力,原有本机调试技术中那段高效率的时间消失了。

我发现对良好终端系统的正确使用,往往要求每两小时的终端会话对应于两小时的桌面工作。一半时间用于上次会话的清理工作:更新调试日志,把更新后的程序列表加入到项目文件夹中,研究和解释调试中出现的奇怪现象。剩余一半时间用于准备:为下一次操作设计详细的测试,进行计划的变更和改进。如果没有这样的计划,则很难保持两个小时的高生产率;而没有事后的清理工作,则很难保证后续终端会话的系统化和持续推进。

测试用例。关于实际调试过程和测试用例的设计,Grunberger提出了特别好的对策9,在其他的文章中,也有较为简便的方法10,11。

系统集成调试

软件系统开发过程中出乎意料的困难部分是系统集成测试。前面我已经讨论了一些困难产生和困难不确定的原因。其中需要再次确认的两件事是:系统调试花费的时间会比预料的更长,需要一种完备系统化和可计划的方法来降低它的困难程度。下面来看看这样的方法所包括的内容12。

使用经过调试的构件单元。尽管并不是普遍的实际情况——不过通常的看法是——系统集成调试要求在每个部分都能正常运行之后开始。

实际工作中,存在着与上面看法不同的两种情况。一种是“合在一起尝试”的方法,这种方法似乎是基于这样的观点:除了构件单元上的bug之外,还存在系统bug(如接口),越早将各个部分合拢,系统bug出现得越早。另一种观念则没有这么复杂:使用系统的各个部分进行相互测试,避免了大量测试辅助平台的搭建工作。这两种情况显然都是合理的,但经验显示它们并不完全正确——使用完好的、经过调试的构件,能比搭建测试平台和进行全面的构件单元测试节省更多的时间。

更微妙的一种方法是“文档化的bug”。它申明构件单元所有的缺陷已经被发现,还没有被修复,但已经做好了系统调试的准备。在系统测试期间,依照该理论,测试人员知道这些缺陷造成的后果,从而可以忽略它们,将注意力集中在新出现的问题上。

但是所有这些良好的愿望只是试图为结果的偏离寻找一些合理理由。实际上,调试人员并不了解bug引起的所有后果;不过,如果系统比较简单,系统测试倒不会太困难。另外,对文档记录bug的修复工作本身会注入未知的问题,接下来的系统测试会令人困惑。

搭建充分的测试平台。这里所说的辅助测试平台,指的是供调试使用的所有程序和数据,它们不会整合到最终产品中。测试平台可能会有相当于测试对象一半的代码量,但这是合乎情理的。

一种测试辅助的形式是伪构件(dummy component),它仅仅由接口和可能的伪数据或者一些小的测试用例组成。例如,系统包含某种排序程序,但该程序还未完成,这时其他部分的测试可以通过伪构件来实现,该构件读入输入数据,对数据格式进行校验,输出格式良好、但没有实际意义的有序数据以供使用。

另一种形式是微缩文件(miniature file)。很常见的一类bug来自对磁带和磁盘文件格式的错误理解。所以,创建一个仅包含典型记录,但涵盖全部描述的小型文件是非常值得的。

微缩文件的特例是伪文件(dummy file),实际上并不常见。不过OS/360任务控制语言提供了这种功能,对于构件单元调试非常有用。

还有一种方式是辅助程序(auxiliary program)。用来测试数据发生器、特殊的打印输出、交叉引用表分析等,这些都是需要另外开发的专用辅助工具的例子13。

控制变更。对测试期间进行严密控制是硬件调试中一项令人印象深刻的技术,它同样适用于软件系统。

首先,必须有人负责。他必须控制和负责各个构件单元的变更或者版本之间的替换。

接着,就像前面所讨论的,必须存在系统的受控拷贝:一个是供构件单元测试使用的最终锁定版本;一个是测试版本的拷贝,用来进行缺陷的修复;以及一个安全版本,其他人员可以在该拷贝上工作,进行各自的程序开发工作,例如修复和扩展自己的模块和子系统等。

在System/360工程模型中,在一大堆常规的黄颜色电线中,常常可以不经意地看到紫色的电线束。在发现bug以后,我们会做两件事情:设计快速修复电路,并安装到系统,从而不会妨碍测试的继续进行。这些更改过的接线使用紫色电线,看上去就像伸着一个受了伤的大拇指。我们需要把更改记录到日志中,同时,还要准备一份正式的变更文档,并启动设计自动化流程。最后,在电路图或者黄色线路中会实现该设计的调整——更新相应的电路图和接线表,以及开发一个新的电路板。现在,物理模型和电路图重新吻合了,紫色的线束也就不再需要了。

软件开发也需要用到“紫色线束”的手法。对于最后成为产品的程序代码,它更迫切地需要进行严密控制和深层次的关注。上述技巧的关键因素是对变更和差异的记载,即在一个日志中记录所有的变更,而在源代码中显著标记快速补丁和正式修改之间的区别,正式修改是完备并经过测试的,而且需要文档化。

一次添加一个构件。这样做的好处同样是显而易见的,但是乐观主义和惰性常常诱使我们破坏这个规则。因为离散构件的添加需要调试伪程序和其他测试平台,有很多工作要做。毕竟,可能我们不需要这些额外工作?可能不会出现什么bug?

不!拒绝诱惑!这正是系统测试所关注的方面。我们必须假设系统中存在着许多错误,并需要计划一个有序的过程把它们找出来。

注意必须拥有完整的测试用例,在添加了新构件之后,用它们来测试子系统。因为那些原来可以在子系统上成功运行的用例,必须在现有系统上重新运行,对系统进行回归测试。

阶段(量子)化、定期变更。随着项目的推进,系统构件的开发者会不时出现在我们面前,带着他们工作的最新版本——更快、更卓越、更完整,或者公认bug更少的版本。将使用中的构件替换成新版本,仍然需要进行和构件添加一样的系统化测试流程。这个时候通常已经具备了更完整有效的测试用例,因此测试时间往往会减少很多。

项目中,其他开发团队会使用经过测试的最新集成系统,作为调试自己程序的平台。测试平台的修改,会阻碍他们的工作。当然,这是必须的。但是,变更必须被阶段化,并且定期发布。这样,每个用户拥有稳定的生产周期,其中穿插着测试平台的改变。这种方法比持续波动所造成的混乱无序要好一些。

Lehman和Belady出示了证据,阶段(量子)要么很大,间隔很宽;要么小而频繁14。根据他们的模型,小而频繁的阶段很容易变得不稳定,我的经验也同样证实了这一点——因此我决不会在实践中冒险采用后一种策略。

量子(阶段)化变更方法非常优美地容纳了紫色线束技术:直到下一次系统构件的定期发布之前,都一直使用快速补丁;而在当前的发布中,把已经通过测试并进行了文档化的修补措施整合到系统平台。

祸起萧墙(Hatching a Catastrophe)

带来坏消息的人不受欢迎。

- 索福克里斯

项目是怎样延迟了整整一年的时间?…一次一天。

None love the bearer of bad news.

- SOPHOCLES

How does a project get to be a year late? ... One day at a time.

当人们听到某个项目的进度发生了灾难性偏离时,可能会认为项目一定是遭受了一系列重大灾难。然而,通常灾祸来自白蚁的肆虐,而不是龙卷风的侵袭。同样,项目进度经常以一种难以察觉,但是残酷无情的方式慢慢落后。实际上,重大灾害是比较容易处理的,它往往和重大的压力、彻底的重组、新技术的出现有关,整个项目组通常可以应付自如。

但是一天一天的进度落后是难以识别、不容易防范和难以弥补的。昨天,某个关键人员生病了,无法召开某个会议。今天,由于雷击打坏了公司的供电变压器,所有机器无法启动。明天,因为工厂磁盘供货延迟了一周,磁盘例程的测试无法进行。下雪、应急任务、私人问题、同顾客的紧急会议、管理人员检查——这个列表可以不断地延长。每件事都只会将某项活动延迟半天或者一天,但是整个进度开始落后了,尽管每次只有一点点。

里程碑还是沉重的负担?

如何根据一个严格的进度表来控制项目?第一个步骤是制订进度表。进度表上的每一件事,被称为“里程碑”,它们都有一个日期。选择日期是一个估计技术上的问题,在前面已经讨论过,它在很大程度上依赖以往的经验。

里程碑的选择只有一个原则,那就是,里程碑必须是具体的、特定的、可度量的事件,能够进行清晰定义。以下是一些反面的例子,例如编码,在代码编写时间达到一半的时候就

已经“90%完成”了;调试在大多时候都是“99%完成”的;“计划完毕”是任何人只要愿意,就可以声明的事件1。

然而,具体的里程碑是百分之百的事件。“结构师和实现人员签字认可的规格说明”,“100%源代码编制完成,纸带打孔完成并输入到磁盘库”,“测试通过了所有的测试用例”。这些切实的里程碑澄清了那些划分得比较模糊的阶段——计划、编码、调试。

里程碑有明显边界和没有歧义,比它容易被老板核实更为重要。如果里程碑定义得非常明确,以致于无法自欺欺人时,很少有人会就里程碑的进展弄虚作假。但是如果里程碑很模糊,老板就常常会得到一份与实际情况不符的报告。毕竟,没有人愿意承受坏消息。这种做法只是为了起到缓和的作用,并没有任何蓄意的欺骗。

对于大型开发项目中的估计行为,政府的承包商做了两项有趣的研究。研究结果显示:

1. 如果在某项活动开始之前就着手估计,并且每两周进行一次仔细的修订。这样,随着开始时间的临近,无论最后情况会变得如何的糟糕,它都不会有太大的变化。

2. 活动期间,对时间长短的过高估计,会随着活动的进行持续下降。

3. 过低估计在活动中不会有太大的变化,一直到计划的结束日期之前大约三周左右。

好的里程碑对团队来说实际上是一项服务,可以用来向项目经理提出合理要求的一项服务,而不确切的里程碑是难以处理的负担。当里程碑没有正确反映损失的时间,并对人们形成误导,以致事态无法挽回的时候,它会彻底碾碎小组的士气。慢性进度偏离同样也是士气杀手。

“其他的部分反正会落后”

进度落后了一天,那又怎么样呢?谁会关心一天的滞后?我们可以跟上进度。何况,和我们有关的其他部分已经落后了。

棒球队队长知道,进取这种心理素质,是很多优秀队员和团队不可缺少的。它表现为“要求跑得更快”,“要求移动得更加迅速”,“更加努力尝试”。对软件开发队伍,进取同样是非常必要的。进取提供了缓冲和储备,使开发队伍能够处理常规的异常事件,可以预计和防止小的灾祸。而对任务进行计算和对工作量进行度量,会对进取超前会造成一些消极的影

响——这时,人们往往会比较乐观地放缓工作节奏。就这一点来说,它们是令人扫兴的事情。不过,如同我们看到的,必须关心每一天的滞后,它们是大灾祸的基本组成元素。

并不是每一天的滞后都等于灾难。尽管会如上文所述,事先估计会给工作进度的超前带来影响,但对活动的一些计算和考虑还是必要的。那么,如何判断哪些偏离是关键的呢?只有采用PERT或者关键路径技术才能判断。它显示谁需要什么样的东西,谁位于关键路径上,他的工作滞后会影响最终的完成日期。另外,它还指出一个任务在成为关键路径时,可以落后的时间。

目录
设置
设置
阅读主题
字体风格
雅黑 宋体 楷书 卡通
字体大小
适中 偏大 超大
保存设置
恢复默认
手机
手机阅读
扫码获取链接,使用浏览器打开
书架同步,随时随地,手机阅读
首 页 < 上一章 章节列表 下一章 > 尾 页