代码大全(经典教材)


计算机实用软件技术系列丛书 软件开发人员必备工具书 Code Complete 代码大全 Steve McConnell 著 天奥 译 熊可宜 校 学苑出版社 1993 eVersion 1.0 缺陷报告: codecomplete@163.net 本书网站: www.delphidevelopers.com (京)新登字 151 号 内容摘要 本书从软件质量和编程思想等方面论述了软件构造问题,并详细论述了紧跟潮流的新技术、 高屋建瓴的观点、通用的概念,还含有丰富而典型的程序示例。本书中所论述的技术不仅填补 了初级与高级编程技术之间的空白,而且也为程序员们提供了一个有关编程技巧的信息来源。 本书适合经验丰富、自学成才的程序员阅读,也适合于那些几乎不懂什么编程技巧的新程 序员阅读. 欲购本书的用户,请直接与北大 8721 信箱联系。邮编:100080,电话:2562329。 版权声明 本书中文版版权由 Microsoft 授予北京希望电脑公司和学苑出版社独家出版、发行。未经 出版者书面许可,本书的任何部分不得以任何形式或任何手段复制或传播。 Microsoft,MS, Ms Dos 和 Windows 是微软公司注册商标。 计算实用软件技术系列丛书 软件开发人员必备工具书 代码大全 著者:Steve McConnell 翻译:天奥 审校:熊可宜 责任编辑:徐建军 出版发行:学苑出版社 邮政编码:100032 社址:北京市西城区成方街 33 号 印刷:兰空印刷厂 开本:787X1092 l/16 印张:33, 625 字数:777 千字 印数:1~10000 册 版次:1993 年 11 月第 1 版第 1 次 ISBN7—5077—0876—4/TP· 25 本册定价;66.00 一—————————————————————————————————— 学苑版图书 印、装错误可随时退换 前言 1 前 言 近年来,关于软件开发的研究,进展是非常迅速的,但是开发实践却并非如此。许多程序 仍然是错误百出,充斥着过时的技术,从而无法满足用户需要。软件工业界和学术界的研究者 们,基本上已经解决了七十年代和八十年代在编程中遇到的问题,并发展了相应的技术。但是 直到现在,这些技术中的大部分仍然没有在软件编程中广泛采用,其主要原因是这些研究成果 主要发表在高度专业性的学术刊物中,普通的程序员们无心顾及。Sridhar Raghavan 和 Donald Chand(1989)的研究表明,一项新技术从诞生到被工业界广泛采用大约需要 5 到 15 年的时间。 本书的目的就是希望能够缩短新技术推广周期,使广大的程序员们可以迅速地获得软件开发的 最新方法与手段。 本书所面向的对象 本书中所收集的研究和编程经验,将有助于你编写出高质量的软件,并且使得开发周期缩 短。通过阅读本书,你将会对自己过去所犯过的错误有更深刻的理解,并懂得今后如何避免它 们。同时,书中所收集的丰富的编程经验也将使你在控制大规模项目和按要求对软件进行修改 和维护时感到得心应手。下面是适合阅读本书的几类人: 经验丰富的程序员 本书适合于想要得到一本全面易用的软件设计指南的那些资深程序员们阅读。由于本书的 中心内容是广大程序员们所熟知的实现过程,因此,无论是受过正规训练而已经验丰富的程序 员,还是完全靠自学成长起来的程序员,都能容易读懂本书所论述的先进技术和方法。 自学成才的程序员 本书尤其适合于很少受过正式专业训练的程序员阅读。1988 年有 100,000 人加入了程序 员大军,但其中只有 40,000 人是从计算机专业毕业的本科生,其余则几乎全是靠自学成才的。 同时,还有不计其数的其他各行各业的人员需要在工作中自己动手编一些程序。无论你所受到 的正规计算机专业训练多或少,本书都将使你对有效的编程方法和技巧有更深刻的理解。 学生 本书不仅适于实践经验丰富但理论基础薄弱的自学者阅读,同时也适于那些理论基础较好 但几乎不懂得什么编程诀窍的毕业生们阅读。新程序员们的某些实践经验来自于经验丰富的同 事.但主要还是靠自己──吃一堑,长一智──获得的,这往往是一个艰苦而缓慢的过程。通 过本书,可以使你在短时期内获得大量的经验和技巧,从而脱颖而出,所以,不妨一试。 本书的主要特点 完备的软件创建参考 本书从质量和编程思想等方面论述了软件构造问题。几乎囊括 了生成子程序、数据的输入输出与控制结构、调试、代码调整策略与技术等各方面的细节。在 使用本书时不必逐页阅读每一个细节,只要在需要时查阅你所感兴趣的章节即可。请把本书作 前言 2 为手册而不是作为教科书来使用。 方便而实用的检查表 书中附有用于检查软件的结构设计、设计方法、模块和子程序等质 量的检查表,以供评估软件质量之用。同时,关于变量名、控制结构、方案布置、测试用例等 等检查表也将使你获益匪浅。 紧跟潮流的新技术 书中论述了许多目前最先进的技术,其中许多还只是刚刚投入应 用。由于本书取材于实践经验和最新研究成果两个方面,因此书中所提供的技术在相当长的时 间内都不会过时。 高屋建瓴的观点 阅读本书将使你跳出日常琐碎工作的圈子,对软件开发有一个总体上 的把握与认识。繁杂的日常工作往往使程序员们穷于应付而无暇阅读浩如烟海的书籍与资料, 本书丰富而翔实的第一手资料将弥补这一缺憾,使你对软件开发的策略作出正确决策而不致陷 入旷日持久的消耗战中。 通用的概念 无论你用的是 Pascal、C、C++、Ada、Basic、Fotran 还是 COBOL,都可 以从本书所论述的概念、方法和技巧中获得教益。 丰富而典型性的程序示例 书中含有大约 500 多个正反两方面的程序示例。之所以引入这 么多的示例,是因为笔者就是从各种例程中吸取了大部分的知识、经验与诀窍,因此笔者认为 最好的学习方法是多看例程。例程是用多种语言写成的,因为对于程序员来说,掌握多种语言 是其必不可少的基本素质之一。而且,只有掌握了不受语法规则限制的编程准则,才能真正有 效地提高你的编程效率和质量。 为了减轻由于使用多种语言所带来的额外负担,在例程中除非 确有必要,尽量避开了各个语言过于独特的部分。事实上,如果你真正注意每个例程所要说明 的问题的话,那么不必详细理解每个程序段,你也可以清楚地懂得程序的意义。同时,为了进 一步减轻读者的负担,对程序中有意义的部分作了标记。 本书的独特内容 本书关于创建活动的内容是从多个渠道获得的。有关创建活动的资料不仅分布得非常分 散,而且往往没有成文资料,事实上,卓有成效的优秀程序员们所使用的技术并不神秘,但由 于日常事务的繁重和工作任务的重压,程序员们很少有互相交流切磋的时间,因而,他们往往 缺乏有关编程技巧的有效信息来源。 本书中所论述的技术不仅填补了初级与高级编程课本之间的空白,而且也为程序员们提 供了一个有关编程技巧的信息来源。比如当你读过 C 语言初级教程之后,你可以再读 C 语言高 级教程,然后再去读 C 语言高级的高级教程,但读完这些书后,你还能再读什么书呢?你可以 再去读关于 PC、Macintosh 或 UNIX 等硬件或操作系统的书或者其它有关编程细节的书——因 为你如果不了解实现环境详情的话是无法充分有效地使用语言和程序的。但这只是讨论了编程 的一个方面,最有效的编程技术是那些不受实现环境及语言限制的技术。其它书往往忽略了这 一点,但这恰恰是本书的重点。 写作本书的目的 需要一本关于软件开发有效技术的书,是软件工程界所公认的。由计算机科学技术委员会 所发表的一份报告认为,提高程序质量和生产效率的最有效途径是出版一本关于软件开发有效 前言 3 技术的书,而且这本书应该以手册的形式来组织。 同时,计算机编程技术的发展史也证明急需一本这方面的书,本书正是出于这个目的 才出版的。 创建活动未受到应有的重视 在一段时期内,软件开发与编码被当作是一回事,但随着软件开发周期中的其它活动被认 识,这一领域内的主要努力全部集中到了项目管理、需求分析、设计和测试等方面,创建活动 成了被遗忘的角落。 与这种现象相对应的思想是认为创建活动是软件开发中无关紧要的部分。于是,刚入门的 程序员被派去进行创建工作,为那些已经由上一阶段设计好的子程序编码。工作几年之后,他 可能会被提升至需求分析或项目管理部门。于是,这位程序员也会自豪地感到他不必再去编码 了。 创建活动是非常重要的 创建活动被忽视的另一个原因是:研究者和程序员们错误地认为与其它开发活动相比,创 建活动是一相对来说比较机械的活动,没有什么值得改进的。没有什么比这种想法离事实更远 了。 在小规模项目中,创建活动约占工作量的 80%,在中型项目中也要占 50%的工作量,而发 生在创建活动中的错误则占总错误的 50%到 75%。一项会产生 50%到 75%错误的工作是有许 多待改进之处的。 一些人认为,虽然创建时的错误占到总错误的 50%到 75%,但修改它们的费用与分析、设 计错误相比要少得多。的确,创建时的错误修改费用与前期工作错误修改费用相比是要少一些, 但是绝对数并不少。 Gerald Weinbers 曾在 1983 年报道过三个错误,每个错误的修改费用都高 达数百万美元,而每个错误都是一行编码层次上的代码错误。因此,绝不能以修改费用相对少 为理由来忽视创建活动。 具有讽刺意味的是,被忽视的创建活动事实上是唯一任何规模项目都必不可少的活动。 需求可以进行猜想而不必分析;结构可以被省略而不必设计。系统测试也可以不进行。但是, 如果你想有一个程序的话,你就不得不进行创建活动。 本书的独特性 如果创建活动的重要性是非常明显的话,那么本书恐怕就没有出版的必要了。但事实上几 乎没有什么书详细论述了这一主题。只有 15 年前出版过一本类似内容的书,讲述的是 ALGOL、 PL/I、Ratfor 等早已过时的语言中的具体问题。其它偶尔也有几本这方面的书,但却是教授们 针对教学用的演示性项目而写的,没有涉及到真正的工程问题。有些则偏激地推崇新技术而不 恰当地贬低了一些非常实用的成熟技术。总之,就内容的新颖、翔实、丰富和实用来看,目前 似乎还没有与本书相匹敌的关于创建活动的书。 编 者 目 录 第一章 欢迎进入软件创建世界⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯1 1.1 什么是软件创建.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯1 1.2 软件创建的重要性.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯3 1.3 小结.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯4 第二章 利用隐喻对编程进行更深刻的理解⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯5 2.1 隐喻的重要性.. ⋯.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯5 2.2 如何使用软件隐喻.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯6 2.3 通常的软件隐喻.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯7 2.4 小结 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯11 第三章 软件创建的先决条件⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯12 3.1 先决条件重要性.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯12 3.2 问题定义先决条件 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯16 3.3 需求分析先决条件 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯16 3.4 结构设计先决条件 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯20 3.5 选择编程语言先决条件.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯26 3.6 编程约定.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯29 3.7 应花在先决条件上的时间.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯29 3.8 改变先决条件以适应你的项目.⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯30 3.9 小结. ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯30 第四章 建立子程序步骤 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯31 4.1 建立程序步骤概述. ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯31 4.2 程序设计语言(PDL) ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯31 4.3 设计子程序 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯33 4.4 子程序编码 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯37 4.5 检查子程序 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯42 4.6 小结 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯44 第五章 高质量子程序特点 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯45 5.1 生成子程序的原因 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯47 5.2 子程序名称恰当 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯51 5.3 强内聚性 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯52 5.4 松散耦合性 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯56 5.5 子程序长度 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯60 5.6 防错性编程 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯61 5.7 子程序参数 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯67 5.8 使用函数⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯71 5.9 宏子程序⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯72 5.10 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯74 第六章 模块化设计 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯75 6.1 模块化:内聚性与耦合性⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯75 6.2 信息隐蔽⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯77 6.3 建立模块的理由⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯84 6.4 任何语言中实现模块⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯85 6.5 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯90 第七章 高级结构设计 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯92 7.1 软件设计引论⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯92 7.2 结构化设计⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯95 7.3 面向对象⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯98 7.4 对目前流行设计方法的评论⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯102 7.5 往返设计⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯105 7.6 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯109 第八章 生成数据 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯111 8.1 数据识别⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯111 8.2 自建数据类型的原因⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯113 8.3 自建类型的准则⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯115 8.4 使变量说明更容易⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯115 8.5 初始化数据的准则⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯120 8.6 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯120 第九章 数据名称 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯121 9.1 选择名称⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯121 9.2 特定数据类型的命名⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯124 9.3 命名约定⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯128 9.4 非正式命名约定⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯129 9.5 匈牙利命名约定⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯132 9.6 短名称⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯136 9.7 要避免的名称⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯137 9.8 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯139 第十章 变量 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯141 10.1 作用域⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯141 10.2 持久性⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯143 10.3 赋值时间⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯144 10.4 数据结构与控制结构的关系⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯145 10.5 变量功能单一性⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯146 10.6 全局变量⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯148 10.7 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯153 第十一章 基本数据类型 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯154 11.1 常数⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯154 11.2 整型数⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯155 11.3 浮点数⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯157 11.4 字符和字符串⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯159 11.5 逻辑变量⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯161 11.6 枚举类型⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯162 11.7 命名常量⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯164 11.8 数组⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯166 11.9 指针⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯167 11.10 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯175 第十二章 复杂数据类型 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯176 12.1 记录与结构⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯176 12.2 表驱动方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯179 12.3 抽象数据类型(ADTs)⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯192 12.4 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯198 第十三章 顺序程序语句 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯199 13.1 必须有明确顺序的程序语句⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯199 13.2 与顺序无关的程序语句⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯201 13.3 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯207 第十四章 条件语句 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯208 14.1 if 语句 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯208 14.2 case 语句⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯213 14.3 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯216 第十五章 循环语句 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯217 15.1 选择循环类型⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯217 15.2 控制循环(Controlling The Loop)⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯222 15.3 编写循环的简单方法——从里到外⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯230 15.4 循环与数组的关系⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯232 15.5 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯233 第十六章 少见的控制结构 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯234 16.1 goto 语句⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯234 16.2 return 语句⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯243 16.3 递归调用⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯244 16.4 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯248 第十七章 常见的控制问题 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯249 17.1 布尔表达式⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯249 17.2 复合语句(块)⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯257 17.3 空语句⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯257 17.4 防止危险的深层嵌套⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯258 17.5 结构化编程的作用⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯264 17.6 用 goto 模拟结构化结构⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯267 17.7 控制结构和复杂性⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯269 17.8 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯271 第十八章 布局和风格 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯272 18.1 基本原则⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯272 18.2 布局技巧⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯279 18.3 布局风格⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯280 18.4 控制结构布局⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯285 18.5 单条语句布局⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯292 18.6 注释布局⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯301 18.7 子程序布局⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯303 18.8 文件、模块和程序布局⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯306 18.9 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯311 第十九章 文档 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯313 19.1 外部文档⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯313 19.2 编程风格作文档⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯314 19.3 注释还是不注释⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯316 19.4 有效注释的关键⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯318 19.5 注释方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯322 19.6 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯337 第二十章 编程工具 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯338 20.1 设计工具⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯338 20.2 源代码工具⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯339 20.3 执行代码工具⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯343 20.4 面向工具的环境⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯345 20.5 建立自己的编程工具⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯346 20.6 理想编程环境⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯347 20.7 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯350 第二十一章 项目大小如何影响创建 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯351 21.1 项目大小⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯351 21.2 项目大小时开发活动的影响⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯352 21.3 项目大小对错误的影响⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯356 21.4 项目大小对生产效率的影响⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯357 21.5 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯358 第二十二章 创建管理 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯359 22. l 使用好的代码⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯359 22.2 配置管理⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯361 22.3 评估创建计划⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯364 22.4 度量⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯369 22.5 将程序员视为普通人⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯370 22.6 如何对待上司⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯374 22.7 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯374 第二十三章 软件质量概述 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯375 23.1 软件质量特点⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯375 23.2 提高软件质量的方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯377 23.3 各种方法的效果⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯379 23.4 何时应作质量保证⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯381 23.5 软件质量的一般原则⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯381 23.6 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯382 第二十四章 评审 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯384 24.1 评审在软件质量保证中的地位⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯384 24.2 检查⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯386 24.3 其它评审方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯389 24.4 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯391 第二十五章 单元测试 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯393 25.1 单元测试在软件质量中的作用⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯393 25.2 单元测试的一般方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯395 25.3 测试技巧⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯396 25.4 典型错误⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯404 25.5 测试支持工具⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯408 25.6 提高测试质量⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯411 25.7 测试记录 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯412 25.8 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯412 第二十六章 调试 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯414 26.1 概述⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯414 26 2 找错⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯417 26.3 修改错误⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯423 26.4 调协心理因素⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯425 26.5 调试工具⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯427 26.6 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯430 第二十七章 系统集成 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯431 27.1 集成方法重要性⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯431 27.2 分段与递增集成⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯432 27.3 递增集成法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯434 27.4 改进的公布法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯439 27.5 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯445 第二十八章 代码调整策略 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯446 28.1 功能综述⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯446 28.2 代码调整介绍⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯448 28.3 低效率情况⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯454 28.4 代码调整方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯457 28.5 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯457 第二十九章 代码调试技术 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯459 29.1 循环⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯459 29.2 逻辑⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯460 29.3 数据转换⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯469 29.4 表达式⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯474 29.5 子程序⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯478 29.6 汇编语言再编码⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯484 29.7 调试技术快速参考⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯485 29.8 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯486 第三十章 软件优化 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯487 30.1 软件优化种类⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯487 30.2 软件优化指南⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯488 30.3 编写新程序⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯489 30.4 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯499 第三十一章 个人性格 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯501 31.1 个人性格是否和本书的主题无关⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯501 31.2 聪明和谦虚⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯502 31.3 好奇⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯503 31.4 诚实⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯504 31.5 交流和合作⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯506 31.6 创造力和合作⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯507 31.7 懒惰⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯507 31.8 不是你想象中那样起作用的性格⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯508 31.9 习惯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯508 31.10 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯510 第三十二章 软件开发方法的有关问题 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯511 32.1 克服复杂性⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯511 32.2 精选开发过程⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯513 32.3 首先为人编写程序,其次才是计算机⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯514 32.4 注重约定使用⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯515 32.5 根据问题范围编程⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯516 32.6 当心飞来之祸⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯517 32.7 重复⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯519 32.8 不要固执己见⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯520 32.9 小结⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯521 第三十三章 从何处获取更多的信息 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯522 33. l 软件领域的资料库⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯522 33.2 软件创建信息⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯523 33.3 创建之外的主题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯523 33.4 期刊⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯524 33.5 参加专业组织⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯525 第一章 欢迎进入软件创建世界 1 第一章 欢迎进入软件创建世界 目录 1.1 什么是软件创建(Software Construction) 1.2 软件创建的重要性 1.3 小结 相关章节 本书适合什么人阅读:见前言 阅读本书的益处:见前言 为什么要写这本书:见前言 大家都知道“Construction”这个词在一般情况下的意思是“建筑”。建筑工人盖房子、建 学校、造摩天大楼等时所进行的工作都是建筑。当你小的时候,你用积木进行“建筑工作”。因 此“Construction”指的是建造某个东西的过程。这个过程可能包括:计划、设计、检验等方面的 某些工作,但是,它主要是指在这其中的创造性工作。 1.1 什么是软件创建 开发计算机软件是一项非常复杂的工作,在过去的十五年中,研究者们指出了这项工作所 包括的主要方面,包括: · 问题定义 · 需求分析 · 实现计划 · 总体设计 · 详细设计 · 创建即实现 · 系统集成 · 单元测试 · 系统测试 · 校正性的维护 · 功能强化 如果你以前从事过一些不太正规的研制工作,你可能以为列出的这个表有些太详细了。而 如果你从事过一些正式的项目,你就会认为这个表非常准确。在正规性与随意性之间达到平衡 是非常困难的.这会在以后章节中讨论。 如果你是自学编程员或是主要从事非正规研制工作,你很可能还没有意识到这些在生产软 件中所需要的工作步骤。在潜意识中,你把这些工作统统称为编程。在非正式项目中,当你在 考虑设计软件时,你所想到的主要活动可能就是研究者们所指的“创建”工作。 关于“创建”的直觉概念是非常准确的,但它往往缺乏正确观点。把创建活动放到与其相 第一章 欢迎进入软件创建世界 2 关活动的背景中,有助于我们在适当重视其它非创建工作的同时,把主要精力放在正确的任务 上。图 1-1 中给出了创建活动在典型软件生存周期循环中的地位和包括的范围。 正如图中所指出的,创建活动主要指编码和调试过程,但也包括详细设计和测试中的某些 工作。假如这是本关于软件开发所有方面的书,它应该涉及到开发过程所有方面并给予同等重 视。但因为这是一本关于创建技术的手册,所以我们只重点论述创建活动及相关主题。如果把 这本书比喻成一只狗,那么它将用鼻子轻擦创建活动,尾巴扫过设计与测试,而同时向其它开 发活动汪汪叫。 创建活动有时被称作“实现”,它有时被叫作“编码和调试”,有时也称之为“编程”。“编 码”实在不是一个很好的叫法,因为它隐含着把已经设计好的程序机械地翻译成机器语言的过 程;创建则无此含义,它指的是在上述过程中的创造性和决策性活动,在本书中,将交替使用 “实现”、“编程”和“创建”。 图 l-l 软件生存周期中软件开发过程的平面图 在图 1-1 中,给出了软件开发过程的平面图示,而在图 1-2 中,则给了它的立体图示。 图 1-1 和图 l-2 是创建活动的总体图示,但是,什么是它的细节呢?下面是创建活动 中所包含的一些特定任务。 · 验证基础工作已经完成,可以进行创建工作 第一章 欢迎进入软件创建世界 3 · 设计和编写子程序与模块 · 创立数据类型并命名变量 · 选择控制结构并组织语句块 · 找出并修正错误 · 评审其它小组的细节设计和代码,同时接受其它小组评审 · 通过仔细地格式化和征集意见改进编码 · 对分别完成的软件单元进行综合 · 调整编码使其更小、更快 图 1-2 本书主要详细论述详细设计、编码、调试和单元测试(所占比例如图示) 要想更 些是创建活动呢?”。一般认 为, 1.2 软件创建的重要性 正如我们所知,改进软件质量、提高软件生产率是非常重要的。当今世界许多激动人心的 工程 同,创建活动在整个开发活动中所占 时间 件开发中处于枢纽地位。分析和设计是创建活动的基础工作,对系统进行测 试以 ikson 和 详尽地了解创建活动,请参阅目录中每一章的标题。 创建活动包括如此众多的工作,人们可能会禁不住要问:“哪 非创建活动包括:管理活动、需求分析、软件总体设计、用户交互界面设计、系统测试、 维护工作等。这其中每项工作都与创建工作一样,会直接影响到项目的最终成败(那些需要两 个人以上合作至少一星期项目的成败)。关于这其中每一项活动都有很不错的论著,在本书每一 章后都列出这些书的书名。 计划中,软件都被广泛地应用:太空飞行、航空、医学与生命保障科学、电影特技、金融 信息的快速处理、科学研究等,这仅是其中的几个例子。如果读者您也认为软件开发是重要的, 那么您就会问,为什么创建活动是重要的?原因如下: 创建活动是开发软件的重要组成部分。随项目规模不 为 30%~80%之间,在任何计划中占有如此大时间比例的活动必然会影响计划的成败,这 是不言而喻的。 创建活动在软 证实创建活动是正确的则是其后续工作,因而创建活动是软件开发的核心工作。 把主要精力集中于创建活动,可以极大地提高程序员的生产效率。由 Sackman、Er 第一章 欢迎进入软件创建世界 4 Grant 在 1968 年进行的实验表明,每个程序员的效率系数的变化范围为 10~20,这一结果随后 又被其它几个实验所证实。最优秀程序员与普通程序员的巨大差异表明,普通程序员提高效率 的潜力是非常大的。 创建活动的产品,源代码,往往是软件的唯一精确描述。在许多项目中,程序员可得到的 唯一文件便是代码本身。需求说明和设计文档可能会过时,但源代码却总是最新的。因此,源 代码必须具有最好的质量。一个软件成功与否的关键,就在于是否不断运用技术来改进源代码。 而这些技术恰恰是在创建阶段,才能得以最有效的应用。 创建活动是唯一一项必不可少的工作。理论上一个软件项目要经过精心的需求分析和总体 设计,然后再进行创建,接着对其进行彻底的、严格的系统测试。然而,实际工作中的软件项 目,往往越过前两个阶段而直接进行创建活动,最后,由于有太多的错误要修改,系统测试又 被弃之路旁。但是,不管一个项目的计划多么疏漏而又如何匆忙,创建活动都是必不可少的。 无论怎样精简,改进创建活动都是改进软件开发工作的方法。 l.3 小 结 · 创建活动是总体设计和系统测试之间承上启下的工作。 · 创建活动主要包括:详细设计、编码、调试和单元测试。 · 关于创建活动的其它称谓有:实现、编程等。 · 创建活动质量对软件质量有潜在影响。 · 在最后的分析中,对创建活动理解的好坏,决定了一个程序员素质的高低,这将在 本书其余部分论述。 第二章 利用隐喻对编程进行更深刻的理解 5 第二章 利用隐喻对编程进行更深刻的理解 目录 2.1 隐喻的重要性 2.2 如何使用软件隐喻(Software MetaPhors) 2.3 通常的软件隐喻 2.4 小结 相关章节 设计中的启发:“设计是一个启发过程”见 7.5 节 计算机科学的语言可能是所有科学领域中最丰富的。想象一下。你走进一间干净整洁、温 度严格控制在 68℉的房间,在这里,你将会找到病毒、蠕虫、臭虫、炸弹、崩溃、火焰、扭曲 的变形者、特洛伊木马和致命错误,在其它领域中,你会遇到这种情况吗? 这些形象的隐喻描述了特定的软件现象。同样形象的隐喻描述了更为广泛的现象,你可以 利用它们来加深你对软件开发的理解。 本书其余部分与本章关于隐喻的论述无关,如果你想了解实质问题可以跳过这一章。但你 要想对软件开发有更清楚的理解,请阅读这一章。 2.1 隐喻的重要性 重大发现往往是从类比中产生的。通过把一个你所陌生的事物与你所熟知的事物比较,你 会对它有进一步的认识,从而形成你对它的独到的深刻理解,这种隐喻方法被称之为“模型化”。 在科学发展史上,充满了利用类比而产生的发现。化学家 Kekle 梦见一条蛇咬住了自己的尾巴, 醒来后,他由此联想到苯的结构,提出了苯是环形分子的假说,这一假说在 1966 年被 Barbour 用实验所证实。 分子运动论是在“保龄球”模型上建立起来的。在这里,分子被假想为具有质量并且与保 龄球一样相互之间进行完全弹性碰撞的小球,并且在此基础上,又产生了许多有用的模型。 光的波动理论是在与声音类比的基础上产生的。光与声都具有振幅(亮度与音量),频率(颜 色与音调)和其它类似性质。这种类比是如此有效,以致于科学家们花费了大量时间来寻找像 空气传播声音一样传播光的物质——“以太”,但他们从来也没能找到。有时如此有效的类比这 次却导出了错误结果。 通常,模型的力量在于它能提供生动形象的概念而易被人整个接受。并提供特性、联系和 附加的疑问,有时模型会提出令人困惑的问题,这时往往是由于模型被误解了,那些建筑“以 太”的科学家们,就是因为误解了模型。 正如你所预料的,有些模型比其它的要好。好的模型要简单、与其它模型关联密切、能解释 第二章 利用隐喻对编程进行更深刻的理解 6 大部分实验事实和观测现象。 比如一个悬在铁链上来回晃动的大石头。在 Galileo 之前,Aristotelian 看到它时想到的是重 物必然要从高处落下来停在低处,他认为石头是在克服阻力下落,而当 Galileo 看到同一现象时, 他认为自己看到了一个单摆,他认为石头是在不断地重复同一运动。 这两个模型所提供的信息是截然不同的。Aristotelian 认为石头是在下落,因而他关心的是 石头的重量、升起的高度及停下所需的时间。而 Galileo 从单摆模型出发,他关心的是石头的重 量、铁链的半径、石头的角位移及石头每摆一次所需要的时间。Galileo 之所以能发现单摆定律, 就是因为他的模型与 Aristotelian 不同,从而导致他们提出了不同的问题。 隐喻对加深软件理解所做出的贡献,与它对其它领域所做出的贡献一样大。1973 年,在图 灵奖颁奖演说中,Charles Bachman 叙述了从地心说向日心说转移的过程。Ptolemy 的地心说统 治了近 1400 年。直到 1543 年,Copernicus 提出了日心说,这一思想模型的转变导致了一系列 新星的发现,把月亮定义为卫星而不是行星,也改变了人类对自身在宇宙中地位的理解。 Bachman把天文学中从地心说向日心说的转变,与 70 年代前期在计算机编程中的变化作了 个对比。在当时,数据处理正从以计算机为中心向以数据库为中心进行转变。Bachman 指出, 在旧的处理模式中,数据被当成是一个连续流过计算机的卡片流(以计算机为中心);而在新的 模式中,数据好比是一个水池,而计算机则偶尔涉足其中(以数据库为中心)。 今天,很难想象谁会认为太阳绕着地球转;也同样难以想象谁会把数据当成流过计算机的 卡片流。在这两个例子中,旧的理论一旦被抛弃,很难想象有谁会再把它捡起来。具有讽刺意 味的是,旧理论的相信者认为新理论荒唐可笑,就像我们今天看旧理论一样。 当日心说出现之后,地心说便成了那些相信它的天文学家的阻碍。同样,计算机中心模式 也已经成了那些相信它的计算机科学家的阻碍,因为我们现在已经有了数据库中心模式。 如果一旦看了新的模型,我们便说:“哦,当然正确的模型更有用,其余的都是错误的”, 那只会降低模型的作用。因为这太偏激了。科学史并不是由一系列从“错误”模型到“正确” 模型开关组成的,而是逐渐由“坏的”模型变为“较好”的模型,从包含面较窄到包含面较宽, 从覆盖领域较少到覆盖领域较多。 事实上,很多被较好模型替代的旧模型仍然在发挥作用。例如,工程师们仍然在用牛顿力 学进行工程计算,虽然它已经被相对论力学所取代。 软件科学是一门比其它学科年轻得多的学科,还很不成熟,远未形成一套标准的模型。所 以,现在拥有的是大量相互矛盾的模型。这其中有些很好,有些则很差。因此,对这些模型理 解得好坏,便决定了你对软件开发理解的好坏。 2.2 如何使用软件隐喻 软件隐喻更像是一束搜索灯光,而不是一张地图,它并不会告诉你到哪里去寻找答案;它 只给你以启发,教你如何寻找答案,而不是像数学算法一样硬性规定出到哪里找出答案。 一个公式是一套完整建立的、进行某一些任务的规则。它的结果是可以预测的、确定的, 并不取决于运气。公式会告诉你直接从 A 点走到 B 点,中间不准绕路,不准随意顺便访问 C、 D、E 或 F 点,也不准停下来闻一下玫瑰花香或者喝杯咖啡什么的,一切必须按规定来。 启发是一种帮助你寻求答案的技术。它的结果往往和运气有关,因为它只告诉你如何去 第二章 利用隐喻对编程进行更深刻的理解 7 找,而并未告诉你应该找到些什么。它不会告诉你怎样直接从点 A 到点 B.甚至很可能它根本 就不知道点 A 和点 B 在哪里。事实上,可以认为启发是一个穿着小丑儿外套的公式。它往往不 可预测,更富有趣味,不会保证一定会发生或不会发生什么。 比如,开车去某人家的公式是这样的:沿 167 号公路向南到 Sumner,从 Bonney 湖出口向 山上开 2.4 英里,借助加油站的灯光向左拐,在第一个右转弯处向右转,再拐入通向褐色房子 的公路,寻找的门牌号是北大街 714 号。 以下则是一个如何找到我们房屋的启发:找到我们寄给你的最后一封信,开车到回信地址 所说的小镇,到了镇上后随便问哪个人我们住哪儿,别担心,镇上的人都认识我们。如果你谁 也遇不到的话,就打电话找我们。 公式和启发之间的区别是微妙的,这两个例子或许会说明一些问题。从本书的角度来看, 它们之间的主要区别是:它们与答案之间的直接程度。公式给予直接指令;而启发则告诉你该 怎样找到这些指令,或者至少告诉你到哪里寻找它们。 如果有一套指令告诉你该如何解决程序中的问题,这当然会使编程变得很容易,而且结果 也可以预测了。但是编程科学目前还没有那样发达,也许永远也不会。编程中最富于挑战性的 问题便是将问题概念化,编程中许多错误往往都是概念性错误,因为每个程序在概念上都是独 特的,所以创立一套可以指导每一个问题的规则是非常困难,甚至是不可能的。这样,从总体 上知道该如何解决问题,便几乎和知道某一特定问题的答案一样重要了。 你是怎样使用软件隐喻的呢?应该用它来帮助你获得关于编程过程的内在理解,利用它们 来帮助你考虑编程活动,想象解决问题的更好办法。你不要一看到某一行代码就说这与这一章 所使用的某个隐喻相矛盾。随着时间推移,在编程过程当中使用隐喻的程序员肯定比不使用这 一方法的人编写代码更快更好。 2.3 通常的软件隐喻 随着软件的发展,隐喻越来越多,已经到了使人迷惑的地步,Fred Brooks 说写软件就像耕 种、猎狼或者在一个沥青矿坑中淹死一只恐龙。Paul Heekel 说这就像电影《白雪公主与七个小 矮人》。David Gries 说这是科学,Donald Knuth 则说这是门艺术,Watts Hamphrey 则说这是一个 过程,Peter Freeman 说这是个系统,Harlan Mills 认为这就像解数学题、做外科手术、或者是宰 一条狗,Mark Spinrad 和 Curt Abraham 说这更像是开发西部、在冰水中洗澡或者围着营火吃豆 子。 2.3.l 软件书写:写代码(Writing Code) 开发软件最原始的隐喻出自“写代码”一词。这个写的隐喻说明开发一个程序就像随便写 封信,你准备好纸、笔和墨水,坐下从头写到尾就算完成了。这不需要任何正式计划,你只是 把你要说的都写出来。 许多想法都源于写隐喻。Jon Beitle 说,你应该准备好一杯白兰地,一支上等雪茄,与你喜 欢的猎狗一同坐在火边,像一个优秀小说家一样享受一次“自由编程”。Brian 和 Kernighan 把 写隐喻风格的书称为《风格要素》(《The Elements of Style》)之后,把他们编程风格的书称作《编 程风格要素》(《The Elements of Programming Style》),程序员们则经常谈论程序的“可读性”。 第二章 利用隐喻对编程进行更深刻的理解 8 在一些小问题中,写代码隐喻可以充分描述它们。但是对于其余的问题,它就力不从心了, 它不可能全面彻底地描述软件开发过程。写往往是一种个人活动,而软件开发往往需要许多人 分担各种不同的责任。当你写完一封信时,你把它装进信封并把它寄出去后,你就再也不能改 变它的内容了,无论从哪个角度说,这项工作都已经完成了。软件的内容是很容易改变的却很 难彻底完成。几乎有 50%的软件开发工作量是在软件最初发行之后才进行的(Lientz和 Swanson, 1980)。编写软件,主要工作量集中在初始阶段。在软件创建中,把精力集中于初始阶段往往不 如在初始工作完成后,再集中精力进行代码的重新调整工作。简而言之,写隐喻往往把软件工 作表示成是一项过于简单而刻板的工作。 不幸的是,写隐喻已经通过我们这个星球上最流行的软件书——Fred Brooks 的《The Mythical Man Month》而变得永存了。Brooks 说,“扔掉一个计划,又有什么呢?”这使得我们 联想到一大堆被扔进废纸篓的手稿。当你写封家常信问候你叔叔时,准备扔掉一封信是可能的, 这也可能是 Brooks1975 年写那本书时,当时软件工程的水平。 但是,到了九十年代,再把写隐喻解释为准备扔掉一封信时,恐怕是不合时宜的。现在, 开发一个主要系统的投资已经相当于建一幢十层办公楼或造一艘远洋客轮的费用了。我们应该 在第一次调试时就完成它,或者在它们成本最低时试几次运气,其它几个隐喻较好地解决了说 明达到这一目的的方法问题。 2.3.2 软件播种:生成系统(Growing a System) 与刻板的写隐喻相反,一些软件开发者认为你应该把创建软件当作播种或培植庄稼。你设 计一小部分,编码一小部分,测试一小部分,然后在某个时候把它加到系统上,通过小步走, 你减小了每次可能遇到的错误。 有时,一项先进的技术可能是通过拙劣的隐喻来表达的。在这种情况下,应努力保留这项 技术并换一个隐喻来表达它。在这里增量技术是先进的,但是种庄稼的比喻则是十分拙劣的。 一次干一点儿的想法可能和植物生长有某种类似之处,但是耕种类比实在太牵强,而且也 令人感到陌生,因而也就很快被后面的隐喻所取代了。很难把耕种隐喻推广到每次做一点儿这 一简单想法之外。如果你来用耕种隐喻,你就会发现自己在谈论给系统计划施肥,减少详细设 计,通过有效地田间管理提高编码产量,最后收获编码。你也会谈论进行轮作,用种小麦代替 大麦,让土地休息一年以提高土壤中的养分。 软件种植隐喻的弱点是你对于软件开发失去了直接控制。你在春天播种代码,最后在秋天 收获一大堆代码。 2.3.3 软件珍珠培植法:系统积累(System Accretion) 有时候,人们在谈论种植软件而事实上他们指的是软件积累。这两个隐喻是密切联系的, 但是软件积累更深刻一些。“积累”这个词,含有通过外加或吸收,缓慢生长的意思,就像河蚌 逐渐分泌酸钙形成珍珠一样。在地质学上,水中悬浮物逐渐沉积形成陆地的过程也与此相似。 这并不是说你要从水中悬浮物里沉积出代码来;这只意味着你应该学会每次向你的系统中 加一点儿东西。另外一个与积累密切相联的词是增量。增量设计、构造、测试是软件开发的最 强有力工具之一。“增量”一词在设计者心目中还远未达到“结构化”或“面向对象设计”等的 地位,所以迄今为止也没有一本关于这方面的论述,这实在是令人遗憾的,因为这种书中所收 集的技术将具有极大的潜力。 第二章 利用隐喻对编程进行更深刻的理解 9 在增量开发中,你首先设计系统可以运行的最简单版本。它甚至可以不接受实际数据输入, 或者对数据进行处理。它也可以不产生输出,只需要成为一个坚实的骨架结构,以便能承受将 要在它之上发展的真实系统。它可以调用任何一个实现预定功能而设立的伪子程序。就像河蚌 刚开始产生珍珠的核——一粒沙子。 当你搭好骨架后,逐渐地往上添加肌肉和皮肤。你把每一个伪子程序变成真正的子程序。 此时你不必再假设产生结果了,你可以随意访问一个代码来产生结果。也不必使其假设接收输 入,你可以用同样的方法让它接收输入。你每次加入一点儿代码直到你最终完成它。 这种方法的发展是令人印象非常深刻的。Fred Brooks,在 1975 年时还认为:“应做好建造 一个扔掉一个的准备”,在 1987 年时,却说在过去的岁月里,还没有一样东西像增量概念这样 如此深刻地改变了他自己的实践或效率。 增量隐喻的力量在于:作为一个隐喻,它并没有过分作出许诺,它不像耕种隐喻那样容易 被错误延伸。河蚌育珍珠的联想对理解增量发展法或积累法有很大帮助。 2.3.4 软件创建:建造软件(building software) “建造”一词的想象比“写”或者“种植’软件的想象更为贴切,它与“增量”软件的想 法是基本一致的。建造隐喻暗示了许多诸如计划、准备、执行等工作阶段。如果你仔细研究这 个隐喻,你还会发现它还暗示着其它许多东西。 建造一个四英尺高的塔需要一双稳健的手、一个平台和十个完好的啤酒罐。而建造一个四 百英尺高的塔却决不仅仅是需要一千个啤酒罐就够了,它还需要一种完全不同的计划和创建方 法。 如果你想建一个简单的建筑物,比如说一个狗舍,你买来了木板和钉子,到下午的时候, 你已经给你的爱犬造好了一幢新房子,假设你忘了修一个门,不过这没关系,你可以补救一下 或推倒一节重新开始。你所浪费的不过是一个下午的时间罢了。这与小型软件的发展失败非常 类似。如果你有 25 行代码设计错了。那你重新再来一遍好了,你不会因此浪费许多的。 然而如果你是在造一幢房子,那修建的过程就要复杂些了,而拙劣设计的后果也严重得多。 首先,你必须决定造一幢什么样的房子,这就像软件开发中的问题定义。然后,你与建筑师必 须搞出一个你们都同意的总体方案,这和软件的总体设计是一样的。接着,你又画出细节蓝图 并找来一位承包商,这相当于软件中的详细设计。下面的工作是选好房址、打地基、建造起房 屋的框架、建好墙壁并加上屋顶、用千斤锤检查墙壁是否垂直,这同软件创建基本差不多。当 房屋的绝大部分工作已经完成时,你请来园艺师和装修师,以便使你的房间和空地得到最好的 利用,这可以与软件优化相类似。在整个过程中,会有各种监督人员来检查房址、地基、框架、 供电系统和其它东西,这也可以与软件开发中的评审和鉴定相类似。 较大的规模和复杂性往往意味着可以产生较大的成果。在修房子的时候,材料可能比较贵, 但更大的花费是劳动力。拆掉一面墙并把它移到六英尺之外是很昂贵的,但并不是因为你浪费 了许多钉子,而是因为你需要付出劳动。你应该尽可能精心设计,以避免那些本可避免的错误, 以降低成本。在开发软件过程中,材料更便宜,然而劳动力成本却更高。改变一个报告的格式, 可能与移走一幢房子里的墙壁一样昂贵,因为二者成本的主要部分都是劳动力。 第二章 利用隐喻对编程进行更深刻的理解 10 这两个活动之间还有什么类似之处呢?在建房子中,你不会去建造那些你可以现成买来的 东西,比如洗衣机、烘干机,电冰箱、吸尘器等,除非你是个机械迷。同时,你也会去购买已 经做好的地毯、门、窗和浴室用品,而不是自己动手建。如果你正在建造一个软件,你也会这 样做。你会推广使用高级语言的特点,而不是去编写操作系统一级的代码。你也会利用已经存 在的显示控制和数据库处理系统,利用已经通过的子程序。如果样样都自己动手是很不明智的。 如果你想修建一幢陈设一流的别墅,情况就不同了,你可能定做全套家具,因为希望洗碗 机、冰箱等与你的家具协调一致,同时你还会定做别具风格的门和窗户。这种定制化的方式与 一流软件开发也是非常类似的。为了这一目的,你可能创建精度更高、速度更快的科学公式。 你也会设计自己的显示控制、数据库处理系统和自己的子程序,以使整个软件给人以一气呵成, 天衣无缝的感觉。 当然这两种建造方法也要付出代价,工作的每一步都要依据事先制定好的计划进行。如果 软件开发工作的顺序有误,那么这个软件将是难以编码、难以测试和难以调试的。这可能会使 整个计划延误甚至失败,因为每个人从事的工作都非常复杂,把它们综合到一起后会使人无所 适从。 如果你在盖办公楼时工作做得不好,那么在楼内办公的人便可能面临危险。同样,如果你 在创建医药、航空电子、空中交通管制、加工控制等软件时工作做得不好,后果也可能是灾难 性的。危及别人生命是劣质软件的最可怕后果,但并不是它的唯一危害。如果公司的股东们因 为你编写了错误软件而赔钱,那也是令人遗憾的。无论如何,无辜的人们没有义务为你的工作 失误而付出代价。 对于软件作修改与建造建筑物也有类似之处。如果你要移走的那面墙壁还要支撑其它东西 而不仅仅是隔开两个房间,那么你要付出的成本将会更高。同样,对软件做结构性的修改也将 比增加或减少外设特征付出更高昂的代价。 最后,建筑类比对于超大型软件也是同样适用的。一幢超大型建筑物存在错误的后果将是 灾难性的,整个工程可能不得不返工。建筑师们在制定和审查计划时是非常仔细的,他们往往 留出安全裕度,多用 10%的材料来加强结构总比一幢大楼坍塌要好得多,同时还必须仔细注意 工时计划,在修建帝国大厦时,每辆卡车的每次卸货时间都留出了十五分钟的裕度。因为如果 有一辆卡车不能在指定时间到达指定的位置,整个计划就有可能被延误。 同样,对于超大型软件来说,计划工作需要比一般的大型软件在更高的层次上进行。1977 年,Capers Jones 估计说,对于一个拥有 750,000 行代码的系统来说,可能需要多达 600 页的 功能定义文件。对于一个人来说,不要说理解这种规模全部的设计,就是读完它也是非常困难 的。安全系数对于这种项目是必须的,制定该系统的工时计划尤为重要。当我们在建造与帝国 大厦同等经济规模的软件时,我们也需要同等严密的计划。而我们现在才刚刚开始考虑这种规 模项目的计划技术。 这两者之间的相似还可以推广到其它方面,这就是为什么建筑物创建隐喻是如此强有力的 原因。许多常用的软件词汇来源于建筑学,如:软件体系结构、搭结构架、构造、分割代码、 插入子程序等等。 2.3.5 实用软件技术:智能工具箱(The Intellectual Toolbox) 在过去的十几年中,优秀的软件开发人员们积累了几十条关于开发软件的技术和技巧,有 第二章 利用隐喻对编程进行更深刻的理解 11 些像咒语般灵验,这些技术不是规则,它们是分析工具。一个优秀的工匠知道用什么样的工 具干哪一样工作,而且知道该如何使用它们。程序员也是如此,关于编程你理解得越深入, 你的工具箱里的工具也就越多,何时何地该如何运用它们的知识也就越多。 把方法和技巧当作工具是很有益处的,因为这样可以使我们对其有一个正确的态度。不 要把最新的“面向对象设计技术”当作上帝赐予的法宝,它不过是一件在某些场合下有用, 而在某些场合下又无用的技术。如果你拥有的唯一工具就是一把锤子,那么你就会把整个世 界都当作一个钉子。好在没有人会花 500 美元一天的费用来雇佣一个仅告诉你去买一把可以 解决一切问题的锤子的研究小组,也没有人建议你丢掉你的改锥、手钻和电烙铁。 在软件开发中,常常会有人告诉你用一种方法来代替另外一种方法。这实在不幸,如果 你仅仅采用一种方法,那你就会把整个世界都当成那个工具的作用对象。你会失去用更适合 的方法解决问题的机会。工具箱隐喻有助于我们保留一切方法、技巧、技术等,并在适当的 时候使用它们。 2.3.6 复合隐喻(Combing Metaphors) 因为隐喻更像是一种启发,而不是公式,所以,它们并不是互相排斥的。你可以同时使 用增量隐喻和建筑隐喻。如果你愿意的话,你也可以采用“写”隐喻,或者把写隐喻与耕种 隐喻一起使用。只要能激发你的思想,你尽可以采用一切你认为合适的隐喻。 使用隐喻是一项模糊的事情。你不得不把它们外推到可以从中受到启发的外延中。如果 你把它过分外推或者推广到了错误方向,它很可能使你误入歧途。就像是再好的工具也有可 能被误用一样,你也可能错误使用隐喻。但是,它们的作用将无可置疑地使其成为你的智能 工具箱中的一件有力工具。 2.4 小 结 隐喻仅仅是启发,而不是公式,因此,它们更倾向于比较随便,无拘无束。 · 隐喻通过把软件开发与你所熟知的事情联系在一起,从而使你对其有更深刻的理解。 · 一些隐喻要好于其它隐喻。 · 把软件创建与建造建筑物类比,表明开发软件前要精心准备,并表明了大规模项目与小 规模项目之间的差别。 · 认为软件开发实践是智能工具箱中的工具进一步表明,每个程序员都有许多自己的工 具,没有任何一种工具是万能的。为每件工作选择合适的工具,是成为一个优秀程序员 的首要素质之一。 第三章 软件创建的先决条件 12 第三章 软件创建的先决条件 目录 3.1 先决条件重要性 3.2 问题定义先决条件 3.3 需求分析先决条件 3.4 结构设计先决条件 3.5 选择编程语言先决条件 3.6 编程约定 3.7 应花在先决条件上的时间 3.8 改变先决条件以适应你的项目 3.9 小结 相关章节 · 不同规模程序的不同条件:见第 21 章 · 管理创建:见第 22 章 · 设计:见第 7 章 在开始修造一幢房屋之前,建筑工人会评审蓝图,确认所有用料已经备齐,并检查房子的 地基。建筑工人为修建摩天大楼和修建狗舍所做的准备工作是截然不同的。但不管是什么样的 项目,准备工作总是和需要相适应的,并且应在工程正式开始前做完。 本章主要论述在软件创建之前所要做的准备工作,对于建筑业来说,项目的成败往往在开 工前就已经决定了。如果基础打得不好,或者项目计划进行得不充分,你所能做的最多也就是 防止计划失败,根本谈不上做好。如果你想做一件精美的首饰,那么就得用钻石作原料。如果 你用的是砖头,那你所能得到的最好结果不过是块漂亮的砖头而已。 虽然本章讲的是软件创建基础工作,但并没有直接论述创建工作。如果你觉得不耐烦,或 是你对软件工程生存期循环已经很熟悉了,那么请跳过本章而直接进入下一章。 3.1 先决条件重要性 优秀程序员的一个突出特点是他们采用高质量的过程来创建软件。这种过程在计划的开始、 中间和末尾都强调高质量。 如果你只在一个计划即将结束时强调质量,那你注重的只是测试。当某些人一谈起软件质 量时,他们首先想到的便是测试。然而,事实上测试只是全部质量控制策略的一部分。而且并 不是最重要的部分。测试既不能消除在正确方向上的错误工作,也不能消除在错误方向上的正 确工作的错误,这种错误必须在测试开始之前就清除掉,甚至在创建工作开始之前就要努力清 除掉它们。 第三章 软件创建的先决条件 13 如果你在一个计划的中间强调质量,那么你强调的是创建活动,这一活动是本书论述的中 心。 如果在一个计划的开始强调质量,这意味着你计划并要求设计一种高质量的产品。假设你 在过程开始时要求设计的是一种菲亚特汽车,你尽可以用你所喜欢的各种手段测试它,但是无 论你怎样测试,它也决不会变成一辆罗尔斯——罗伊斯牌汽车。或许你所得到的是一辆最好的 菲亚特汽车,但如果你想要的是罗尔斯——罗伊斯车,你就不得不从计划开始时就提出要求。 在软件开发中,当你进行诸如问题定义、规定解决办法等等计划工作时,你所进行的就是这样 的工作。 由于创建工作处在一个计划的中间,所以,当你开始创建工作时,早期的工作已经奠定了 项目成败的基础。在创建工作中,至少你应该知道自己的处境如何,当你发现失败的乌云从地 平线上升起时,赶快返回第一阶段。本章其余部分主要讲述准备工作已经作好了。 3.1.l 造成准备不足的原因 你也许会认为所有的职业程序员都懂得准备工作的重要性,并且在开始正式工作之前确认 所有的先决条件都已得到满足。不幸的是,事实并非如此。 一些程序员并不作准备工作,因为他们抵制不了立刻开始进行编码工作的渴望。如果你就 是这种程序员,那我对你有两条忠告。第一,阅读一下下一部分工作的内容提示,或许你会从 中发现一些你没想到的问题。第二,要注意自己的问题。只要创建过几个大的程序,你就会明 白强调准备工作的必要性。不要忘记自己的经验教训。 程序员不重视准备工作的另一个原因是管理人员往往不理解那些在创建先决条件上花费时 间的程序员。 Ed Yourdon 和 Tom DeMarco 等人强调准备工作已经有十五年了。在这期间,他 们不时地敲响警钟,或许有一天,管理人员们最终会明白软件开发不仅仅是编写代码。 八十年代后期,我曾经在一项军用项目的某一部门中工作。当项目进行到需求分析阶段时, 负责这个计划的一位将军前来视察。我们告诉了他目前所处的阶段,并主要谈论了文件编写工 作,而这位将军却坚持要看一下代码,我们告诉他目前还没有代码,而他却走进一间正有一百 多人工作的房间,转了一圈,企图找到谁在编码。由于未能如愿以偿,他变得有些气急败坏, 这位身材高大的将军指着自己身边的工程师喊道:“他在干什么?他一定是在写代码。”事实上, 这位软件工程师正在进行文档格式编排的工作,由于这位将军想得到代码,认为那看起来像代 码并且想让工程师编码,所以我们不得不骗他说这位工程师写的确实是代码。 这可以称为 WISCA 或 WIMP 现象,即:为什么 Sam 没有正在写代码?或 Mary 为什么没 正在编程? 如果你正在从事的项目经理像那个将军一样,命令你立刻开始编码,说声“是,长官”是 很容易的。但这是一个坏的反应,你应该还有几个替代办法。 第一,你应该平静地拒绝按照错误顺序工作。如果你与老板的关系很正常的话,那么这太 好了。 第二,你可以假装正在编码而事实上没有。把一个旧的程序清单放到桌角上,然后埋头从 事你的需求和构想文件编写工作,不管你的老板同不同意。这样你可以把工作做得更快更好。 从你老板的观点来看,这个忽视是一个福音。 第三,你可以用技术项目的开发方式来教育一下老板。这是一个好办法因为这可以增加这 第三章 软件创建的先决条件 14 个世界上开明老板的数量。在下一部分,我们将给出更多在创建活动前做好准备工作的理由。 最后,你可以另找一份工作。优秀的程序员是非常短缺的。可以找到更好的工作,干吗非 要呆在一个很不开明的程序店里徒损生命呢? 3.1.2 在进行创建工作之前必须做准备工作的论据 假设你已经登上了问题定义的山峰,与负责需求分析的人并肩走了一英里,在结构设计之 泉中,洗净了你沾满灰尘的衣服,并且沐浴在已经作好准备的纯洁之水中。那么你就会知道在 实现一个系统之前,你应该清楚需要一个系统干什么和需要怎样去干。 作为一个工程技术人员,教育你周围的人,让他们懂得技术项目的开发过程,也是你工作 的一部分。本书的这一部分可以帮你对付那些还不懂得技术项目开发过程的老板和管理人员。 它是关于进行构造设计和问题定义设计权利的延伸论据。在你进行编码、测试和调试之前,学 会这些论据,并且和你的老板推心置腹地谈谈技术项目的开发过程。 求助于逻辑推理 进行有效程序设计的关键之一就是认识到准备工作是非常重要的。在进行一项大的项目 之前,事先做好计划是明智的。项目越大,需要的计划工作量也越大,从管理人员的角度来看, 计划是指确定一个项目所需要的时间、人力、物力和财力。从技术人员的观点来看,计划是指 弄清楚你想要干什么,以免做出错误的工作而徒耗精力与钱财。有时候你自己并不十分清楚自 己想要的到底是什么?起码刚开始是这样。这时,就会比清楚知道用户需求的人要付出更多努 力,但是,这总比做出一件错误的东西,然后把它扔掉,再从头开始的成本要低得多。 建造一个系统之前,弄清楚怎样开始和如何建造它也是非常重要的,你当然不希望在完全 没有必要的情况下,浪费时间与钱财去钻死胡同而白白增加成本。 求助于类比 创建一个软件系统与其它需要耗费人力与财力的工程是一样的。如果你要造一幢房子,在 开始砌第一块砖之前,你必须事先画好建筑图与蓝图。在你开始浇铸水泥之前,你必须让人评 审你的蓝图并获得通过,在软件开发中事先做计划也与此类似。 在你把圣诞树立起来后,你才会开始装饰它,在没有修好烟囱之前你也不会点燃炉火的, 同样,也没有人会打算在油箱空空的情况下踏上旅程,在软件开发中,你也必须按照正确的顺 序来进行。 程序员处于软件开发食物链的最后一环。结构设计吃掉需求分析;详细设计者以结构设计 者为食,而他自己又成为编码者的食物。 比较软件食物链和真正的食物链,我们会发现如下事实,在一个正常的生态系统中,海鸥 以沙丁鱼为食,沙丁鱼吃鲜鱼,鲜鱼吃水虱,其结果会形成一个正常的食物链。在编程工作中, 如果软件食物链的每一级都可以吃到健康的食物,其结果是由一群快乐的程序员写出的正确代 码。 在一个被污染了的环境中,水虱在受到核沾染的水中游泳,鲫鱼体内积聚了滴滴涕,而沙 丁鱼生活的水域又遭受了石油污染,那么,不幸的海鸥由于处在食物链的最后一环,因此,它 吃的不仅仅是沙丁鱼体内的石油,还有鲜鱼体内的滴滴涕和水虱体内的核废料。在程序设计中, 第三章 软件创建的先决条件 15 如果需求定义遭受了污染,那么这又会影响结构设计,而这将最终影响创建活动。这将导 致程序员们脾气暴躁而营养不良,同时生产出遭受严重污染而充满缺陷的软件。 求助于数据 过去十五年的研究证明,一次完成是最好的选择,不必要的修改是非常昂贵的。 TKW的数据表明,在项目的初期阶段进行设计更改,比如在需求定义和结构设计阶段进行 更改,与在项目的后期,即创建和维护阶段进行更改相比较,其成本要低 50 到 100 倍(Boehm 和 Pappecio,1988)。 对 IBM 的研究也表明了同样结果。在设计开始阶段,如详细设计、编码或单元测试阶段就 消除错误,其成本要比在后期即系统测试和功能强化阶段低 10 到 100 倍(Fagan,1976)。 通常的准则是,一旦引入错误,就尽早发现和消除它。错误在软件食物链中存留的时间越 长,它的危害也就传播得越远。因为需求分析是我们做的第一项工作,因此这时引入的错误在 系统中存留时间最长,危害最大。在软件开发初期引入的错误往往比后来引入的错误传播的面 更广,这也使得早期错误会极大地提高成本。 由 Robert Dunn 总结的表 3-1,给出了由于错误引入和发现时间不同,而产生修复它们所 要耗费的相对成本差异。 表 3-1 错误引入时间 错误发现时间 需求分析 细节设计 编码 需求分析 1 — — 细节设计 2 1 — 波动测试 5 2 1 结构测试 15 5 2 功能测试 25 10 5 表 3-1 的数据表明,在需求分析阶段引入的错误,如果马上发现并消除所耗费的成本是 1000 美元的话,那么如果到了功能测试阶段才发现和消除,耗费的成本则会高达 25000 美元。这说 明我们应该尽早地发现并消除错误。 如果你的老板不相信这些数据,那你可以告诉他,立刻开始编码的程序员往往要比那些先 作计划、而后才编码的程序员花费更长的时间,由 NASA 计算机科学公司和马里兰大学联合建 立的软件工程实验室的研究表明,过分地使用计算机(进行编辑、编译、链接、测试等)往往 与低生产率紧密相联。而在计算机旁花费较少时间的程序员,往往更快地完成工作。这是由于 频繁使用计算机的程序员在进行编码和测试之前,花在计划和设计上的时间较少。 老板的意愿测试 当你认为老板已经理解了在开始创建工作之前进行准备工作的重要性,那么请进行下面 的测验以证实这一点。 下面这些说法哪些是正确的? · 我们最好马上就开始编码因为我们将会有许多测试工作要做。 第三章 软件创建的先决条件 16 · 我们没有安排许多时间进行测试,因为我们不会发现很多错误。 · 我们已经在计划和设计上花费了这么多精力,我想我们的编码和测试时不会有什么 大问题了。 以上这些都是正确的。 在本章的其余部分我们将论述如何确定先决条件是否已经得到满足。 3.2 问题定义先决条件 在进行创建工作之前你要满足的第一个先决条件,便是必须弄清楚你想要解决的问题是 什么。由于本书的中心内容是创建活动,因此我们不打算在这里论述如何进行问题定义。我们 只想告诉读者如何确认问题定义是否完成,这个定义的质量如何,是否足以作为创建活动的基 础。 问题定义只描述要解决的问题是什么,根本不涉及解决方法。它应该是一个简短的说明, 听起来像一个问题。比如“我们无法跟上指令系统”听起来像一个问题,也是一个好的问题定 义。而“我们需要优化数据入口系统以便跟上指令系统”则是一个糟糕的问题定义,它听起来 不像是个问题而更像是个解决方案。 问题定义的工作是在需求分析之前进行,后者是对问题的更为详尽的分析。 问题定义应该从用户的观点出发,使用用户的语言进行定义。一般来说,它不应该使用计 算机技术术语进行定义。因为最好的解决办法可能并不是一个计算机程序。比如说,你需要一 份关于年度利润的报告,而你已经拥有了一套能产生季度利润的计算机报表系统,如果你的思 路仅仅局限于计算机,那你可能会让人再写一个产生年度利润报告的程序加到这个系统中。为 达到这个目的,你不得不雇用一个程序员编写并调试出一段相应的程序。可是,要是你的思路 开阔一些的话,让你的秘书用计算器把四个季度的利润加到一起,问题不就解决了吗? 当然,如果问题是关于计算机本身时,就是个例外了。比如,计算机的编译速度太慢或者 编程工具的问题太多,那我们只能用技术术语来说明问题了。问题定义错误的后果是你可能浪 费许多时间精力去解决了一个错误问题。这种惩罚往往是双重的,因为真正的问题并没有解决。 3.3 需求分析先决条件 需求详细描述了一个软件系统需要解决的问题,这是找到问题答案的第一步。这项活动也 被称作“需求分析”、“需求定义”等。 3.3.1 为什么要有正式的需求 明确的需求是很重要的,因为: 明确的需求可以保证是由用户而不是程序员决定系统的功能。如果需求是很清楚的,那么 用户可以对其进行评定,并确认自己是否同意。如果需求不很清楚,那么程序员在编程过程中 就不得不自己决定系统功能,明确的需求防止对用户需求进行猜测。 明确的需求也可以避免引起争议。在开始编程之前,系统的范围已经明确确定了。如果在 编程过程中,两个程序员对系统干什么有争议,那么只要查阅一下写好的需求分析,问题就解 第三章 软件创建的先决条件 17 决了。 注意需求定义,也可以使得在开发工作开始之后,对系统作的改动最小、如果你在编码时 发现某几行有误,那么改掉这几行就是了。而如果在编码阶段发现需求有误,那么你很可能不 得不改变所有的代码以适应新的需求。 一些设计不得不被丢掉,是因为按它们的要求写好的代码不具备兼容性。新设计可能要花 费很长的时间,被一同扔掉的还有受到需求变更影响的代码和测试用例,即使未受影响的代码 部分也不得不进行重新测试,以确认其他地方的变动没有引入新的错误。 IBM、GTE、TRW 的数据表明.修正在总体结构阶段发现的需求错误,将比当时就发现并 修正的成本要高出 5 倍,如果是在编码阶段,要高出 10 倍,在单元或系统测试阶段,高 20 倍, 在验收测试阶段,高 50 倍,而在维护阶段,竟要比原来高出多达 100 倍!在较小规模的计划中, 在维护阶段修正错误的放大因子可能是 20 而不是 100,因为这时管理费用较低。但无论如何没 有人愿意从自己的收益中拿出这笔钱来。 充分进行需求分析是一个项目成功的关键,很可能比使用有效的创建技术还重要。关于如 何进行需求分析有许多好的论著。因此,我们不打算在随后的几部分中探讨如何进行需求分析。 我们只想告诉你如何确定需求分析已经完成,如何最充分地利用需求分析。 3.3.2 稳定需求的神话 稳定的需求可以说是软件开发的法宝。有了稳定的需求,软件开发工作可能从结构设计到 详细设计到编码,都平稳、顺利的进行。这简直是造就了软件开发的天堂。你可以预测开支, 不必担心最终会冒出一个让你多花 100 倍钱的错误来。 用户一旦接受了写好的需求文件,便再也不会提出更改需求,这简直是太好了。然而事实 上,在实际项目中,用户在代码写出来之前,往往并不能确切可靠地描述出他想要的到底是什 么,这倒并不是说用户是一种低级生物。正如随着工作的进行,你对其理解越来越深刻一样, 用户对自己想要的东西,也是随着项目的进行而越来越清楚的,这也是需求变动的主要原因。 -个从不变更需求的计划,事实上是一个对用户的需求不予理睬的计划。 典型的变动有多少呢?根据 IBM 的调查,对于一个典型的有一百万字的需求分析,大约 25%的内容在开发过程中要进行变动。 或许你认为卡迪拉克小汽车是空前绝后的,帝国大厦将万古永存,如果真是这样的话,那 你就相信你的项目需求永远不会更好了。如果不是这样,那么或许我们可以采取一些措施,使 得由于需求变更所造成的冲击最小。 3.3.3 在创建阶段如何对付需求变化 以下是在创建阶段,为应付需求变化而应该采取的措施。 用本部分后面的检查表来评估你的需求分析质量 如果你的需求分析不是很好,那么,停止继续工作,重新返回到需求分析阶段。当然,这 样会使人觉得你已经落后了。但是,如果你在开车从芝加哥到洛杉矶的途中,发现自己到了纽 约市郊,那么停下车来看一下地图是浪费时间吗?当然不是。因此,如果你发现方向不对,赶 紧停下来检查你的方向。 第三章 软件创建的先决条件 18 让每个人都知道由于变化需求所付出的代价 雇员们往往由于自己有了新的设计想法而激动不已。在这种兴奋驱使之下,他们往往会热 血沸腾,得意忘形。什么讨论需求的会议,什么签约仪式、什么需求文件,统统都会被他们扔 在一边。对付这种人最简单办法就是对他说:“喂,先生,你的想法不错,但是由于它不在需求 文件之中,我想先做一个变动后的进度和成本估计,然后我们再决定是立刻就采用这个想法还 是以后再说”。“时间进度”和“成本”这两个词往往比咖啡和泼冷水更管用,这样说,往往会 把许多“立刻采用”变成“最好采用”。 如果你的组织机构还没有认识到需求分析的重要性,那么就请引述本章前面“进行创建活 动前满足先决条件的安全和必要论据”一节的内容,告诉他们,在需求阶段变更设计是成本最 低的办法。 建立一套更改控制过程 如果雇员们坚持更改的热情高涨,则可以考虑建立一个审查这种更改建议的正式委员会。 用户改变主意,意识到他们的软件需要更强的功能是非常正常的。但如果他们频繁地改变主意 以至于你无法跟上他们的速度,那就不正常了。这时如果拥有一套控制更改的正式过程,那将 使大家都会感到宽慰。你感到宽慰是因为现在你只在特定的时候处理变动问题。顾客也感到宽 慰是因为有专门机构处理他们的意见,会使他们感到自己倍受重视。 用开发的方法来容纳变动 一些开发方法可以极大地扩展你应付变更需求的能力。原型化开发的方法可能帮助你在 全力以赴投入工作以前,首先了解系统的需求。渐进开发的方法是指按阶段公布系统。每次你 只做一点儿,从用户那里得到一些反馈后,你再做一些调整的改动,然后再增加一些内容。这 种方法的关键是使用短周期开发方法,以便你对顾客的需求变更迅速作出反应。 放弃项目 如果需求特别稀奇古怪或者反复无常,上面那些办法全都不起作用,那就放弃这个项目。 即使你并不能真正地砍掉这个项目,你也可以考虑一下这样做会怎么样。考虑在你砍掉这个项 目之前,事情会发展到什么地步。假如在某一情况下,的确可以把这个项目扔进垃圾箱,那么 还可以考虑一下有或没有这个项目会造成什么区别。 3.3.4 检查表 需求 这个需求检查表包含一系列关于你的项目需求的自测题。本书并没有论及如何提出一份 好的需求文件,这个检查表也同样没有。但用这个检查表,你可以检验一下在创建工作时,你 的工作基础是否牢固可靠。 并不是表中所列出的每一个问题都适用于你的项目。如果你正在从事一个非正式项目,你 会发现根本不需要考虑这个问题,你也会在其中发现一些需要考虑但并不需要回答的问题。但 如果你正在从事一个大型的正式项目,我们建议你最好还是仔细考虑每一个问题。 第三章 软件创建的先决条件 19 需求内容 · 系统的所有输入都定义了吗?包括它们的来源、精度、取值范围和频率? · 系统所有的输出都定义了吗?包括它们的目标、精度、取值范围、频率和格式? · 所有的报告格式都定义了吗? · 所有的硬件与软件接口都定义了吗? · 所有的通信界面都定义了吗?包括握手、错误检查以及通信约定? · 是否从用户的观点出发,定义了所有必要操作的反应时间? · 是否定义了时间问题,如处理时间、数据传输率以及系统吞吐能力? · 是否对用户所要求完成的任务都作出了规定? · 每项任务所需用到和产生的数据都规定了吗? · 规定保密级别了吗? · 规定可靠性了吗?包括软件出错的后果、在出错时要保护的至关重要的信息、以及错误 测试和恢复策略。 · 规定所需最大内存了吗? · 所需最大存储容量规定了吗? · 对系统的维护性是否作出了规定?包括系统对运行环境、精度、性能以其与其它软件的 接口等方面变化的适应能力规定了吗? · 是否规定了相互冲突的设计之间的折衷原则,例如,在坚固性与准确性之间如何进行折 衷? · 是否制定了系统成败的标准? 关于需求的完善性 · 在开发开始前暂时得不到的信息是什么?是否规定了不够完善的区域? · 需求定义是否已经完善到了可以成为软件标准的地步? · 需求中是否有哪一部分令你感到不安?有没有根本不可能实现,而仅仅为了取悦老板和 用户才加进来的内容? 关于需求的质量 · 需求是否是用用户的语言制定的?用户也这样认为吗? · 需求中是否每一条之间都尽量避免冲突? · 需求中是否注意了避免规定设计工作? · 需求在详细程度方面是否保持了一致性;有没有应该更详细些的需求?有没有应该更 简略些的? · 需求是否明确得可以分为一些独立的可执行部分,而每一部分又都很明了? · 是否每一条都与问题和答案相关?是否每一条都可以追溯到产生它的环境中? · 是否每一条需求都可以作为测试依据?是否可以针对每一条进行独立测试以确定是否满 足需求? · 是否对可能的改动作出了规定?包括每一改动的可能性? 关于需求定义的进一步阅读 以下是一些给出了如何进行需求定义的书: DeMarco, Tom 《 Structured Analysis and Systems Specification:Tools and Techniques 》 第三章 软件创建的先决条件 20 Englewood Cliffs,N.J:Prentice Hall,1979,这是关于需求定义的经典著作。 Yourdon,Edward 《Modern Structured Analysis》 New York:Yourdon Press,1989,这本 新书论述了许多进行需求定义的文字和图表工具。 Hatley,Derek J 和 Imtiz A. Pirbhai 《Strategies for Real-Time system Specification》Newyork : Dorset house,1988。这是一本替代 DeMarco 或 Yourdon 书的最佳选择。它重点论述了实时系统, 并把 DeDarco 和 Yourdon 提出的图表法扩展到了实时系统中。 Shlaer,sally 和 Stephen Mellor《Object Oritented System Analysis-Modeling the World in Data》Englen wood Cliffs,N.J: Prentice Hall,1988。本书讨论了面向对象设计中的需求分析。 IEEE Std 830-1984(Guide for Software Requirements Specifications) in IEEE 1991。这份 文献是 IEEE 为编制软件开发需求定义制订的指导性论述。它描述了需求定义中应该包括的内 容并给出了几个例子。 Gibson,Elizabeth《objects-Born and Bred》Byte,1990 10:245-54。这篇文章是关于面向 对象需求分析的入门书。 3.4 结构设计先决条件 软件结构设计是较高级意义上的软件设计,它是支持详细设计的框架。结构也被称为“系 统结构”、“设计”、“高水平设计”或者“顶层设计”。一般说来,结构体系往往在一个被称为“结 构定义”或者“顶层设计”的单一文件中进行描述。 由于本书是关于创建活动的,因此这部分也没有讲述如何开发软件结构。本部分的中心是 如何确定一个现存结构质量。因为结构设计比需求定义更接近于创建活动,因此对于结构设计 的描述要比需求定义详尽得多。 为什么要把结构设计当成先决条件呢?因为结构设计的质量决定了系统概念上的完整性, 而这又会决定系统的最终质量。好的结构设计可能使创建工作变得很容易,而坏的结构设计则 使创建活动几乎无法进行。 在创建活动中,对结构设计进行变动也是很昂贵的。一般来说,在创建阶段修复结构设计 错误要比修复需求错误耗时少,但比修正编码错误耗时多得多。从这个意义上来说,结构变动 与变动需求差不多,所以,无论是出于修正错误还是提高性能的动机,如果要进行结构变动的 话,那么越早越好。 3.4.1 典型的结构要素 有许多要素是一个好的系统结构所共有的。如果你是一个人在独自开发一个系统,那么你 的结构设计工作,或者说顶层设计工作,将与你的详细设计工作重叠。在这种倩况下,至少你 应该考虑每一个结构要素。如果你正在从事一项由别人进行结构设计的系统工作,你应该不费 什么劲儿就能找到其中的重要部分。下面是一些在两种情况下都需要考虑的要素。 程序的组织形式 一个系统结构首先需要一个总体上的概括性描述。如果没有的话,从成千个细节与几十个 独立模块中勾画出一幅完整的图画将是一件十分困难的事情。如果这个程序仅仅是一个由十二 第三章 软件创建的先决条件 21 块积木组成的小房子,那么或许连你那两岁的儿子也会认为这很容易。然而,对于一个由十二 个模块组成的软件系统,事情恐怕就困难得多了。因为你很难把它们组合到一起,而如果不能 把它们组合到一起,你就不会理解自己所开发的这一个模块对系统有什么贡献。 在结构设计中,你应该能找出最终组织形式的几种方案,并且应该知道为什么选中了现在 这种组织形式。如果开发模块在系统中不被重视,会使人产生挫折感。通过描述这些组织形式 的替代方案,我们就可以从结构设计中找出选择目前方案的原因,并已知道每一个模块的功能 都仔细考虑过了。回顾设计实践发现,设计理由对于维护性来说,与设计本身是同样重要的 (Rombach 1990) 在结构设计中,应该在程序中定义主要模块。在这里,“模块”并不是指子程序。在结构 设计中通常不考虑建立模块一级的子程序。一个模块是一个能完成某一高级功能的子程序的组 合,例如,对输出结果进行格式化,解释命令,从文件中读取数据等。在需求定义中列出的每 一项功能,都应该有至少一个模块覆盖这项功能。如果一项功能由两个或更多的模块覆盖,那 么它们之间应该是互补的而不是相互冲突。 每一个模块作什么应该明确定义。一个模块应该只完成一项任务而且圆满完成。对于与它 相作用的其它模块情况,你知道得越少越好。通过尽可能地降低模块之间的了解程度,就可能 把设计信息都集中在一个模块中。 每个模块之间的交界面也应该明确定义。结构设计应该规定可以直接调用哪些模块,哪些 模块它不能调用。同时,结构设计也应该定义模块传送和从其它模块接收的数据。 变动策略 创建一个软件系统,对于程序员和用户来说,都是一个逐渐学习的过程,因此在这个过程 中作出变动是不可避免的。变动产生的原因可能是由于反复无常的数据结构,也可能是由于文 件格式和系统功能改变,新的性能等而引起的。这些变动有时是为了增加新的能力以便强化功 能,也有时是版本增加而引起的。所以结构设计所面临的主要挑战便是增强系统的灵活性,以 便容纳这类变动。 结构设计应该清晰地描述系统应付变动的策略。结构设计应该表明:设计中已经考虑到了 可能的功能增强变动,而且,应该使最可能的变动同时也是最容易实现的变动。比如,假设最 可能的变动是输入或者输出格式、用户界面的方式或者处理需求,那么结构设计就应表明已经 预先考虑到了这些变动,而且,其中每一个单一的变动,只会涉及到数量有限的几个模块。在 结构设计中应付变动的手段可能是非常简单的,比如在数据文件中加入版本号,保留一部分区 域以备将来使用,或是设计一些可以添加内容的文件。 结构设计中应该说明用于延缓变动的策略。比如,结构设计中可能规定应使用表驱动技术 而不是手工编码技术。它还可能规定表所使用的文件应该保存在一个外部文件中,而不是编码 在程序中,这样,可以不必重新编译就可以对程序作出调整。 购买而不是建造的决定 创建一个软件的最彻底的办法并不是创建——而是去购买一个软件,你可以购买数据库管 理系统、屏幕生成程序、报告生成程序和图形环境。在苹果公司 Macintosh 或者微软公司 Windows 环境下编程的一个主要优点是你可以自动获得许多功能;图形程序,对话框管理程序, 第三章 软件创建的先决条件 22 键盘输入与处理程序,可以自动与任何打印机或者显示器工作的代码,等等。 如果计划中要求使用已有的程序,那它就该指出如何使这些重新被使用的软件适应新的需 求,而且它应该证明这个软件可以通过改动来满足新的需求。 Barry Boehm 在 1984 年指出:从长远观点来看,重新使用旧软件是提高生产率的首要因素。 购买代码可以降低计划、详细设计、测试和调试的工作量。 Caper Jones 在 1986 年报告如果购 买的代码从 0 上升到 50%,那么生产率可以提高一倍。 主要的数据结构 结构设计应该给出使用的主要文件、表和数据结构。同时,还应给出考虑的替代方案并评 审作出的选择。在《Software Maintenance Guidebook》一书中,Glass 和 Noiseux 认为数据结构 对系统维护有举足轻重的影响,因而,它应该在经过全盘考虑之后,才能选定(1981 年)。如 果某一应用需要维护一个用户识别表,而结构设计又选中了顺序存取表来实现,那它就该解释 为什么顺序存取表要好于随机存取表、堆栈和哈希表。在创建阶段,这些信息可以使你对结构 设计有一个比较深刻的理解。在维护阶段,这些信息也是非常宝贵的。如果没有它们,你就会 有一种看一部不带字幕的外国电影的感觉。 不应该允许一个以上的模块访问数据结构,除非是通过访问子程序,以使得这种访问是抽 象的而且是可控的。这将在 6.2 “信息隐蔽”部分中详细论述。 如果一个程序使用了数据库,那么结构中应该规定这个数据库的组织形式和内容。 最后,应该遵循数据守恒定律:每一个进入的数据都应该出去,或者与其它数据一道出去, 如果它不出去,那它就没有必要进来。 关键算法 如果结构设计依赖于某一特定算法,那它应该描述或指出这一算法。同主要数据结构一样, 结构设计中也应该指出考虑过的算法方案,并指出选中最终方案的原因。比如,如果系统的主 要部分是排序,而结构设计中又指定了排序方式是堆排序,那它就要说明为什么采用堆排序的 方法,以及未采用快速排序或插入排序的理由。如果是在对数据作出某种假定的基础上才选中 堆排序的,那就该给出这个假定。 主要对象 在面向对象的系统中,结构中应指出要实现的主要对象,它应该规定每一个对象的责任并 指出每个对象之间是如何相互作用的。其中应包括对于排序层次、状态转换和对象一致性的描 述。 结构中还应该指出考虑的其它对象,以及选择这种组织形式的原因。 通用功能 除了特定程序的特定功能,绝大多数程序中都需要几种在软件结构中占有一席之地的通用 功能。 用户界面。有时用户界面在需求定义阶段便已经规定了。如果没有的话,那就应该在结构 设计中作出规定。结构中应该定义命令结构,输入格式和菜单。用户界面的精心结构设计,往 第三章 软件创建的先决条件 23 往是一个深受欢迎的软件与被人弃之不用的软件间的主要不同之处。 这部分结构应该是模块化的,这样,当用新的界面代替旧的时,就不致影响到处理和输出 部分。比如,这部分结构应该使得用批处理接口替代交互式界面的工作非常容易。这种能力是 很有用的,特别是在单元测试和子系统测试阶段。 用户界面设计本身就值得写一部专著,但本书并未涉及这一内容。 输入/输出。输入/输出是结构中另一个应引起重视的部分。结构中应规定采用向前看、 向后看还是当前规则的查询方式。同时,还应该指出在哪个层次上检查输入/输出错误,是在 区域层次、记录层次还是在文件层次上。 内存管理。内存管理是结构设计中应该处理的另一个重要部分,结构中应该对正常和极端 情况下所需要的内存作出估计。例如,如果你正在写数据表,那么结构就应估计其中每一个单 元所需的内存。它还应估计正常表格和最大表格所需要的内存。在简单情形下,这种估计应表 明内存在某项功能的实现环境中是正常的。在复杂情况下,可能不得不建立自己的内存管理系 统,如果是这样,那么内存管理程序的设计应和系统其它部分一样,需要认真对待。 字符串存储。在交互式系统中,字符串存储也应在结构设计阶段予以重视。在这种系统中, 往往包含了大量的提示、帮助信息和状态显示。应该估计被字符串所占用的内存。如果程序是 商用的,那么,结构中应该考虑到典型的字符串问题,包括字符串的压缩,不必修改代码即可 保持字符串,以及保证在译成外文时对代码的影响将是最小的。结构设计可以决定字符串的使 用方法,是编码在程序中,还是把它保存在数据结构中。是需要时通过存取子程序调用,还是 把它存在一个源文件中,结构设计应该指明采用哪种方法及其原因。 错误处理 错误处理已成为当代计算机科学中最棘手的问题,没有谁能担负起频繁应付它的负担。有 人估计,程序中有 90%的代码是为了应付例外的错误处理或者内务处理而编写的,就是说仅有 10%的代码才是处理正常情况的。既然有如此多的代码是用于错误处理,那么在结构中阐明处 理错误的策略就是十分必要的了。以下是些需要考虑的问题: · 错误处理是纠正还是仅仅测试错误?如果是纠正错误,程序可以尝试从错误状态下恢 复。如果仅仅是测试,那么程序可以继续运行,就像什么也没有发生一样,或者直接 退出运行。但无论在哪种情况下,都应该提醒用户发现了错误。 · 错误测试是主动的还是被动的?系统可以积极地预防错误,如通过检验用户的输入是 否合法,当然也可以消极地在无法回避它们时才做出反应。例如,用户的一系列输入 产生了溢出,你可以清除,也可以滤除信息。同样,无论哪种方案,都要提醒用户。 · 程序是怎样对付错误的?一旦测试出错误,程序可以立刻抛弃产生错误的数据,也可 以把它当作错误而进入错误处理状态,还可以等到全部处理完毕后再通知用户数据有 误。 · 处理错误信息的约定是什么呢?如果结构设计中没有规定某种策略。那么用户界面在 程序的不同部分就会像迷宫中的通道一样忽东忽西,让人摸不着头脑。为避免出现这 类问题,结构设计中应建立一套处理错误信息的约定。 · 在程序中,应该在哪一个层次上处理错误呢?你可以在发现的地方立即处理,也可以 把它交给一个错误处理子程序去处理,或者交给更高层次的子程序处理。 第三章 软件创建的先决条件 24 · 每一个模块检验输入数据合法性的责任级别有多高?是每一模块仅检验它自己的数据, 还是由一级模块来检验整个系统的数据?是否每个层次上的模块都可以假定输入其中 的数据是合法的? 坚固性 (Robustness) 坚固性是指在发现错误后,一个系统继续运行的能力。在结构设计中需要从几个方面表述 坚固性。 裕度设计(over-engineering)。在结构设计中应该明确表述所要求的系统裕度有多大。 结构设计中规定的裕度往往比需求定义中规定的要大。一个原因是由于系统是由许多部分组成 的,这会降低其总体坚固性。在软件链条中,其强度不是由最薄弱的一环决定的,而是由所有 薄弱环节的乘积决定的。 清楚地表述所要求的裕度级是非常重要的,这是因为程序员出于职业素养,会不自觉地在 程序中留出裕度。通过清楚地规定裕度级,可以避免某一部分裕度过大,而另一部分又过小的 现象发生。 断言(assertions)。结构中还应该规定断言的使用程度。断言是指一段放在代码中,当代 码运行时可以使其自检的可执行语句。如果断言显示出正确信息,那么表明一切都正常运行。 如果显示出错误信息,那么表明它在程序中发现了错误。比如,系统假定用户信息文件永远不 会超过 5000 记录行,那么程序中可能会包含一段说明这个假定的断言。只要这个文件不超过 5000,那么断言就保持沉默,而一旦断言发现此文件超过了 5000 个记录行,那它就会声称已发 现了一个错误。 为了在程序中加入断言,你必须知道在设计系统时所做的假设,这也是在结构设计中应阐 明采用假设的原因之一。 容错性(fault tolerance)。结构设计应指明所期望的容错性类型,容错性是指通过测试 错误、修正错误或在不能修复时容错等一系列方法,来提高系统可靠性的技术。 例如,一个可以采用如下办法来容忍求算术平方根时的错误。 · 系统可以返回并重新开始。如果发现结构有误,系统可以返回到正常的部分并重新开 始。 · 当发现错误时,系统可以用辅助代码来代替基本代码。如果第一个结果看起来是错的, 系统将使用另一个备用求平方根子程序重新计算一遍。 · 系统可以采取投票算法。可以用三种不同的方法算平方根,每一个子程序求一个平方 根,由系统作出比较。根据系统所采用的容错种类,最终结果可能是三者的平均,其 中的中间值就是占优势的那一个值。 · 系统可以用一个假想值来代替错误的结果,以避免对程序其余部分的不良影响。 其它的容错方式包括:在测试出错误后,只让系统部分运行或者系统功能降级,关闭自己 或者自动重新开始等,这些例子是非常简单的。容错性是一个非常诱人而又复杂的学科,但它 也不在本书讨论之列。 性能 如果考虑到性能,那么在性能需求中应该考虑性能目标。性能目标包括速度和内存使用。 第三章 软件创建的先决条件 25 结构设计要对这些目标作出估计,并解释为什么这些目标是可以达到的。如果某个域可能 有达不到目标的危险,或者,如果某个域要求使用特定的算法或者数据结构来达到某一目标, 在结构设计中也应指出这点。结构设计还应该规定每一个模块或目标的时间和存储空间预算。 通用的结构设计质量准则 一个好的结构设计特征包括;对于系统中模块的讨论,每个模块中隐含的信息,选用和不 选用某方案的原因。 这个结构应该是一个近乎完美的整体概念。关于软件工程的最权威的著作<>,其中心思想便是认为概念完整性是最重要的(Brooks,1975)。一个好的结构 设计应满足这一条,当看到这个结构设计时,应该为其解决方案的自然和简单而折服。而不会 有把问题和答案生拼硬凑到一起的感觉。 你或许知道在开发过程中变动结构设计的途径。每一次变动都应与总体和设计概念相符。 不能使最终完成的结构设计看起来像是一个穿着由各种碎布拼凑起来的百家衣的乞丐。 结构的目标应该清楚地说明。一个以可变性为首要目标的结构设计可能与一个以性能为首 要目标的结构设计差之千里,虽然二者的功能可能是完全一样的。 结构中作出每一个决定的动机都要阐明清楚。要当心“我们过去一直是这么干的”的理由。 有这样一个故事,会给我们启迪。Beth 想按照她丈夫的家传方法做一道红烧牛肉。她的丈夫 Abdul 告诉她,要先把牛肉放在盐和调料中腌一下,再剁掉肉的两边,把中间部分放进锅里, 盖上盖儿焖一下就可以了。Beth 问:“为什么要剁掉肉的两边?”Abdul 说:“我不知道,我总 是这样做的,我们问一下妈妈吧”。便打电话问妈妈、Abdul 的妈妈则说是他的外祖母告诉她的。 于是电话打到了 Abdul 的外祖母那儿,她的外祖母奇怪地说:“我也不知道你们为什么那样做, 我那样做不过是因为肉块太大,放不进锅里”。 好的软件结构往往是机器和语言相互独立。当然,我们不能忽略系统的实现环境。然而, 通过尽量减少对实现环境的依赖性,你可以避免过分地结构化系统,并且使你可以在创建阶段 把工作做得更好。但如果程序专门是为某一机型或语言设计的,那么本条不适合。 结构设计应该恰好在过分定义和定义不足的分界线上。结构中不应该有任何部分受到了它 不应受的重视。设计者不能以牺牲某一部分为代价来重视另一部分。 最后,结构中不应该有任何部分让你感到不舒服。它不应该含有任何仅仅为取悦老板而加 上去的部分。你是最终实现它的人,如果你根本读不懂它,又谈何实现呢? 3.4.2 检查表 结构设计 一个好的结构设计应该阐明所有问题。这个表并不是用于指导结构设计的,而只是想提供 一种方法,通过它,你可以估计处于软件食物链顶层的程序员可以从食物中获得多少营养。它 可以作为建立自己的检查表的起点。同需求定义检查表的使用一样,如果你正在从事一个非正 式的项目,那么其中有些条款是不必考虑的。但如果你正在开发一个较大的系统,那绝大部分 内容都是非常有用的。 · 软件的总体组织形式是否清晰明了?包括对于结构设计的总体评论与描述。 · 模块定义是否清楚?包括它们的功能及其与其它模块的接口。 第三章 软件创建的先决条件 26 · 需求定义中所提出的所有功能,是否有恰当数量的模块覆盖? · 结构设计是否考虑了可能的更改? · 是否包括了必要的购买? · 是否阐明了如何改进重新启用的代码来满足现在的结构设计需求? · 是否描述并验证了所有主要的数据结构? · 主要数据结构是否隐含在存取子程序中? · 规定数据库组织形式和其它内容了吗? · 是否说明并验证所有关键算法? · 是否说明验证所有主要目标? · 说明处理用户输入的策略了吗? · 说明并验证处理输入/输出的策略了吗? · 是否定义了用户界面的关键方面? · 用户界面是否进行了模块化,以使对它所作的改动不会影响程序其它部分? · 是否描述并验证了内存使用估算和内存管理? · 是否对每一模块给出了存储空间和速度限制? · 是否说明了字符串处理策略?是否提供了对字符串占用空间的估计? · 所提供的错误处理策略是不是一致的? · 是否对错误信息进行了成套化管理以提供一个整洁的用户界面? · 是否指定了坚固性级别? · 有没有哪一部分结构设计被过分定义或缺少定义了?它是否明确说明了? · 是否明确提出了系统目标? · 整个结构在概念上是否是一致的? · 机器和使用实现的语言是否顶层设计依赖? · 给出做出每个重要决定的动机了吗? · 你作为系统实现者的程序员,对结构设计满意吗? 3.5 选择编程语言先决条件 实现系统的语言对你来说是有重大意义的,因为从创建工作开始到结束你都要沉浸其中。 研究表明,程序语言选择可以通过几方面影响生产率和编码质量。 当程序员使用自己所熟悉的语言时,其工作效率要比使用陌生的语言高得多。TRW 公司的 数据表明,两个水平和经验相当的程序员如果一个用一种他已用了三年的语言编程,而另一个 则用一种他所陌生的语言编程,那么前者的效率要比后者高 30%。IBM 的调查表明,一个在某 种语言上经验丰富的程序员,其效率要比在这种语言上没什么经验的程序员高三倍(Walston 和 Felix 1977)。 使用高级语言编程,其效率和质量要比使用低级语言高得多。Pascal 和 Ada 语言的效率、 可靠性、简单性和可懂性是低级语言,如汇编和机器语言的 5 倍(Brooks 1987)。由于不必每 次都为机器正确地执行了指令而欢呼,你当然可以节省许多时间。同时,高级语言的表达能力 第三章 软件创建的先决条件 27 比低级语言要高,这样,它的每一行代码就可以表达更多的内容。表 3-2 给出了在代码量相同 的情况下,高级语言所表达的原指令与低级语言的比值(以汇编语言为代表)。 表 3-2 高级语言指令与低级语言指令比 语言 比值 汇编语言 1:l Ada l:4.5 Quick/Turbo Basic l:5 C 1:2.5 Fotran 1:3 Pascal l:3.5 IBM公司的数据从另一个方面指出了语言特性是如何影响效率的,用解释语言工作的程序 员往往比用编译语言工作的程序员的效率更高(Jones 1986)。许多种语言都有解释和编译两种 形式(如多种版本的 c 语言),你可以用高效率的解释形式,然后再把它们转换成更容易执行的 编译形式。 一些语言比其它语言更擅长解释编程思想。你可以把自然语言(如英语)和程序语言(如 Pascal 和汇编语言)进行对比。在自然语言中,语言学家 Sapir 和 Whorf 提出的假想指出,在一 种语言的表达能力和其所能思考的问题之间存在着联系,你思考某一问题的能力取决于你所懂 得的关于这一问题的词汇。如果你不懂那些词汇,那你也就不能表达那些思想,你甚至根本无 法形成那些思想。 程序员也可能同样受到他所懂得的程序语言限制。在某种程序语言方面你所懂得的词汇, 当然会决定你如何表达你的编程想法,还很可能决定你将表达什么样的思想。 程序语言影响程序员的思想方法。一个典型的故事是这样说的:“我们正用 Pascal 语言开发 一个新的系统,而我们的程序员们却并不熟悉 Pascal 语言,他们都是搞 Fortran 语言出身的。结 果他们写出的是用 Pascal 编译的代码,但是他们真正使用的却是变形的 Fotran 语言。他们用 Fortran 的不好的特性(goto 语句和全局数据)歪曲了 Pascal 语言,而同时又把 Pascal 丰富的控 制和数据结构弃之不用”。这种现象在整个软件业都有报道(Hanson 1984,Yourdon 1986)。 3.5.1 语言描述 某些语言的发展史同其通用功能一样令人感兴趣。以下是关于一些在本书中所举的例程中所 出现的语言的描述。 Ada 语言 是一种在 Pascal 语言基础上发展的通用高级语言,它是在国防部的要求和资助下发展起来 的,特别适用于实时和嵌入式系统。Ada 强调数据抽象和信息隐蔽,迫使你区分模块的公共和 局部部分。 把这种语言命名为“Ada”是为了纪念数学家 Ada lovelace,她被公认为世界上的第一个程 序员,从 1986 年起,北约组织和国防部的所有关键任务嵌入式系统都采用 Ada 语言。 第三章 软件创建的先决条件 28 汇编语言 汇编语言,是一种低级语言,每一条语句都与一条机器指令相对应。由于语句使用特定的 机器指令,所以汇编语言是针对特定处理器的,比如 Intel 80x86 或者 Motorala 680x0。汇编是 第二代计算机语言,除非是执行速度或代码空间的需要,绝大多数程序员都避免使用它。 Basic 语言 Basic 是由 Dartmouth 学院的 John Kemeny 和 Thormas Kurtz 开发的一种高级语言。由字首 组成的 BASIC 的意思是初学者的全功能符号指令代码(Beginner’ s All-Purpos Symbolic Instruction Code),Basic 主要用于教学生们编程。由于 IBM-PC 机包含了它而使其在微机中风 行一时,Basic 原来是一种解释性语言,现在则解释性和编译性两种形式都有。 C 语言 C是一种中级通用语言,本来是和 UNIX 操作系统相关的。C 有某些高级语言的特点,例 如,结构化数据、结构化控制流、对于机器的独立性、丰富的操作指令等。它也被称作“可移 植的汇编语言”,因为它广泛地使用了指针和地址,具有某些低级组成部分,如位操作,而且是 弱类型的。 C 是在七十年代由贝尔实验室 Dennis Ritchie 开发的。C 本来是为 DEC PDP-11 设计的, 它的操作系统、C 编译器和 UNIX 应用程序都是用 C 编写的。1988 年,ANSI 公布了 C 的编码 标准,这成了微机和工作站编程的通用标准。 C++语言 C++,是一种面向对象的语言,与 C 相似,由贝尔实验室的 Bjarne Stroustrup 于 1980 年开 发,除了与 C 兼容之外,C++提供了多态性和函数重载功能,同时,它还提供了比 C 更坚固的 类型检查功能。 Fortran 语言 Fortran 是一种高级语言,引入变量和高级循环的概念。Fortran 代表 Formula Translation, 即公式翻译的意思。 Fortran 最初是在五十年代由 Jim Bckus 开发,并且做过几次重大修订.包 括 1977 所发表的 Fotran-77,其中增加了块结构化的 IF-THEN-ELSE 语句和字符串操作。 Fortran-90 增加由用户定义的数据类型、指针、模块和丰富的数组操作。在写本书的时候(1992 年末)。Fortran 标准是如此引发争议,以致绝大多数语言商都没能最终完成它。本书中所引用 的是 Fortran-77 标准。Fortran 语言主要在科学和工程计算中使用。 Pascal 语言 Pascal 是为了教学目的而开发的高级语言。其主要特征是严格的类型、结构化控制创建和 结构化数据类型。它是在六十年代末由 Niklaus Wirth 开发,到了 1984 年,由于 Borland 国际 公司引入了微机使用的低成本编译程序,Pascal 就流行起来了。 第三章 软件创建的先决条件 29 3.5.2 语言选择快速参考表 表 3-3 给出了关于不同语言适用范围的简略参考。它也可以帮你选择应该进一步了解的语 言。但是,不要试图用它来代替对你某一特定计划进行语言选择时的详细评估。以下的排序是 很粗略的,因此阅读时应仔细辨别,因为很可能会有许多例外。 表 3-3 适于不同种类程序的最差和最好语言 程序类型 最好语言 最差语言 结构化数据 Ada、 C++、 Pascal 汇编、Basic 快速而杂乱的项目 Basic Pascal、Ada、 汇编 快速执行 汇编、C 解释性语言如 Basic 数学计算 Fortran Pascal 易于维护的程序 Pascal 、Ada C 、Fortran 动态内存使用 Pascal、C Basic 在有限内存环境下运行 Basic、汇编、C Fortran 实时程序 Ada、汇编、C Basic 、Fortran 串操作 Basic 、Pascal C 3.6 编程约定 在高质量软件中,你可以发现结构设计的概念完整性与较低层次实现之间的密切联系。这 种联系必须与指导它的结构设计保持一致,而且,这种一致应该是内在的。这就是实现时在给 变量和子程序命名、进行格式约定和注释约定时的指导方针。 在复杂的软件中,结构设计指导方针对程序进行结构性平衡,而实现指导方式则在较低层 次上实现程序的和谐统一,使得每一个子程序都成为总体设计的一个可以信赖的组成部分。任 何一个大的软件系统都需要结构控制,以便把编程语言的细节统一到一起。大型系统的完美之 处便是它的每一个细节都体现了它的结构设计风格。如果没有一个统一约束,那么你的软件只 能是一个由各种风格不同的子程序拼凑到一起的拼盘而已。 即使你有一个关于一幅画的美妙总体构思,但如果其中一部分是用古典手法的,另一部分 是印象派的,其余则是超现实主义风格的,那么,再美妙的构思又有什么用呢?不论其中每一 部分是如何密切联系主题的,这幅画的概念完整性都将荡然无存。同样,程序也需要较低层次 上的完整性。 在创建工作开始之前,一定要写明你将要采用的编程约定、约定说明一定要写得非常详尽, 使得在编程过程中无法对其进行改动。本书提供了许多非常详细的约定。 3.7 应花在先决条件上的时间 用于问题定义、需求分析和软件结构设计的时间,随项目需要的不同而不同。一般来说, 一个运行良好的项目通常把 20~30%的时间用于先决条件。这 20~30%的时间中不包括进行详 细设计的时间,因为它是创建活动的一部分。 第三章 软件创建的先决条件 30 如果你正从事一个正式项目,而需求又是不稳定的,那么,你将不得不与需求分析员一道 解决需求定义问题,拿出你的一部分时间与需求分析员讨论,并给需求分析员一定时间以便让 他重新征求用户意见,可以使需求定义更适合项目需要。 如果你从事的是一个非正式项目,而需求是不稳定的,应该给需求分析留出足够的时间, 以免反复无常的需求定义影响你的创建工作。 如果需求对于任何项目——不管是正式还是非正式的,都是不稳定的,那你就该亲自从事 需求分析工作。当完成需求分析后,再估计从事项目其余部分所需要的时间。这是一个很明智 的办法,因为在你知道自己将作些什么之前,你是不可能知道需要多长时间来完成它的。打个 比方,假设你是一个建筑承包商,你的顾客问:“这项工程要花多少钱?”你则问他要干些什么, 而他却接着说:“我不能告诉你,我只想知道工程要花多少钱?”这时你最好对他说声谢谢,然 后吹着口哨回家吧。 在建筑中,在知道要建什么之前,就进行工程预算显然是荒谬的。在设计师完成草图之前, 老板是不会问要用多少水泥、钉子和木材的。但人们对于软件开发的理解往往不是如此清楚的, 所以你的老板可能一时还弄不明白为什么要把需求分析当作一个单独的项目,这时你就需要作 出解释。 3.8 改变先决条件以适应你的项目 先决条件随项目规模和正式性不同而变化。本章指出了大规模和小型项目之间先决条件的 判别,可以根据项目的特点对先决条件作出合适的调整。要想详细了解大项目与小项目之间的 不同,请参看第 21 章“程序规模是如何影响创建活动的”。 3.9 小 结 · 如果想开发一个高质量的软件,必须自始至终重视质量问题。在开始阶段强调质量往 往比在最后强调质量更为有效。 · 程序员的份内工作之一便是向老板和同事宣传软件的开发过程,包括在编程开始前从 事先决条件准备工作的重要性。 · 如果问题定义工作做得不好,那么在创建阶段,所解决的问题可能并不是用户真正要 解决的问题。 · 如果需求分析工作做得不好,很可能因此而漏掉要解决问题中的重要细节。在创建工 作后更改需求,要比在需求分析阶段进行更改的成本高 20 到 100 倍。所以,在开始编 程前一定要确认需求定义工作一切正常。 · 在编程前规定好约定,在创建工作结束后再改变代码来满足约定几乎是不可能的。 · 在创建活动开始之前如果无法完成准备工作,可以尝试在不太稳固的基础上进行创建 活动。 第四章 建立子程序的步骤 31 第四章 建立子程序的步骤 目录 4.1 建立程序步骤概述 4.2 程序设计语言(PDL) 4.3 设计子程序 4.4 子程序编码 4.5 检查子程序 4.6 小结 相关章节 高质量程序的特点:见第 5 章 高层次设计;见第 7 章 注释方式:见第 19 章 创建工作先决条件:见第 3 章 本章详细讲述了在建立一个子程序时的典型步骤。虽然从广义上讲,你可以把本书所有的 描述都当作是在讲如何建立程序,但本章把这些步骤放在同一背景下讲述。本章的中心内容是 如何编写小规模的程序,以及编写对各种规模项目都十分关键的程序的特定步骤。本章也描述 了从程序设计语言(PDL)到编码的转换过程,几乎没有哪些程序员充分利用了这一过程所带来 的方便,这部分论述会给大家以启迪。 4.1 建立程序步骤概述 在建立程序过程中引入的许多低层次细节问题,并不需要按某一特点顺序来进行,但是一 些主要活动——设计程序、检查程序、子程序编码、检查代码,则应该按图 4-1 的顺序来进行。 4.2 程序设计语言(PDL) PDL(程序设计语言)是由 Came,Father 和 Gordon 共同开发的,在 1975 年发表之后.曾 作过重大修改。因为 PDL 是在模仿英语,所以认为任何像是英语的 PDL,都可以正确表达思想 是很自然的。但是,事实上 PDL 之间的好坏是有判别的。下面是有效使用 PDL 的一些方针: · 用模拟英语的语句来精确描述每一个特定操作。 · 避免使用最终程序语言的语句。PDL 使你比在代码稍高级的层次上进行设计工作。 当使用程序语言进行创建时,就又回到了低层次上,从而得不到由于在高层次上 进行设计的好处,而且会受到不必要的程序语言语法规则的限制。 · 在设计意向这一层次上写 PDL。说明方法的意义,而不是描述如何用目标语言实现。 第四章 建立子程序的步骤 32 图 4-1 创建子程序过程中主要活动顺序示意图 · 在足够低的层次上写出 PDL,它几乎可以自动生成代码。如果 PDL 写得太简略,可能 会在编码过程中忽略问题细节。应该精确地使用 PDL 以方便编码。 当 PDL 写好之后,就可以根据它来编码,而 PDL 则成为程序语言的注释。这可以省去大量 的注释工作。如果 PDL 遵循了这一指导方针,那么注释将是非常完备而且富有意义的。 以下则是一个几乎违背了上述所有原则的错误使用 PDL 的例子: Increment resource number by l allocate a dlg struct using malloc if malloc() returns NULL then return l invoke OSrsrc _init to initialize a resource for the operation system * hRstcPtr=resource number return 0 这个 PDL 的意图是什么?由于它写得很糟糕,因此很难说清楚。之所以称之为一个错误使 用 PDL 的典型,是为它使用了像*hRstcPtr 这种特定的 c 语言指针标志和 malloc( )这个特定的语 言函数,即它采用了代码语句。这段 PDL 的中心是如何写代码,而不是说明设计意义。不管子 程序返回 1 还是返回 0,这段 PDL 都引入了代码细节。如果从是否变为一个好的注释的观点来 看这段 PDL,你就会发现它毫无意义。 以下是对同一个操作的设计,使用的是大大改进了的 PDL: Keep track of current number of resource in use If another resource is available Allocate a dialog box structure If a dialog box structure could be allocated Note that one more resource is in use Initialize the resource Store the resource number at the location provided by the caller Endif Endif 第四章 建立子程序的步骤 33 Reture TRUE if a new resource was created; else return FALSE 这段 PDL 要好于前一个。因为它完全是用自然语言写成的,没有使用任何目标程序语言语 句。在第一段 PDL 中,它只能用 C 语言来实现,而第二段却并没有限制所使用的语言。同时, 第二段 PDL 也是在意图层次上写成的。第二段 PDL 的意图是什么?其意图理解起来比前一个要 容易多了。 尽管第二段 PDL 是完全用自然语言写成的,但它却是非常详细和精确的,很容易作为用程 序语言编码的基础。如果把这段 PDL 转为注释段,那它则可以非常明了地解释代码的意图。 以下是你使用这种风格的 PDL 可以获得的益处: · PDL 可以使评审工作变得更容易。不必检查源代码就可以评审详细设计。它可以使详 细评审变得很容易,并且减少了评审代码本身的工作。 · PDL 可以帮助实现逐步细化的思想。 从结构设计工作开始,再把结构设计细化为 PDL, 最后把 PDL 细化为源代码。这种逐步细化的方法,可以在每次细化之前都检查设计, 从而可以在每个层次上都可以发现当前层次的错误,从而避免影响下一层次的工作。 · PDL 使得变动工作变得很容易。几行 PDL 改起来要比一整页代码容易得多。你是愿意 在蓝图上改一条线还是在房屋中拆掉一堵墙?在软件开发中差异可能不是这样明显, 但是,在产品最容易改动的阶段进行修改,这条原则是相同的。项目成功的关键就是 在投资最少时找出错误,以降低改错成本。而在 PDL 阶段的投资就比进行完编码、测 试、调试的阶段要低得多,所以尽早发现错误是很明智的。 · PDL 极大地减少了注释工作量。在典型的编码流程中,先写好代码,然后再加注释。 而在 PDL 到代码的编码流程中,PDL 本身就是注释,而我们知道,从代码到注释的花 费要比从注释到代码高得多。 · PDL 比其它形式的设计文件容易维护。如果使用其它方式,设计与编码是分隔的,假 如其中一个有变化,那么两者就毫不相关了。在从 PDL 到代码的流程中,PDL 语句则 是代码的注释,只要直接维护注释,那么关于设计的 PDL 文件就是精确的。 作为一种详细设计工具,PDL 是无可比拟的。程序员们往往愿意用 PDL 而不愿使用缺陷表。 事实上程序员们愿意使用缺陷表以外的任何工具,调查表明,程序员们愿意使用 PDL,是因为 它很容易用程序语言实现,而且 PDL 可以帮助发现详细设计中的缺陷,并且 PDL 也很容易写 成文件,改动也很方便,PDL 并不是详细设计的唯一工具,但是 PDL 和 PDL 到代码流程的确 是有用的工具。不妨试一下。在随后的几部分中,我们将告诉你如何使用它们。 4.3 设计子程序 创建一个子程序的第一步是设计。假设想设计一个根据错误代码输出错误信息的子程序, 并且把这个子程序称为 RecordErrorMessge(),以下是关于 RecordErrorMessage()的需求定义: RecordErrorMessage()的输入变量是非法代码,输出是与这个非法代码相对应的错误信息, 它负责处理非法代码。如果程序运算方式是交互式,那么这个错误信息就打印给用户。如果运 行方式是批处理式的,那么这个信息就送入一个信息文件。在输出信息后,这个子程序应该能 返回到一种状态,指出程序是否成功。 第四章 建立子程序的步骤 34 在本章的其余部分,用这个子程序作为一个实际例子。这一部分的其余内容将论述如何设 计这个子程序,设计这个子程序所需要进行的活动见图 4-2。 检查先决条件。在进行与子程序有关的任何工作之前,首先检查是否定义了这个子程序的 工作任务,这项任务是否和整个结构设计融为一体?通过检查确定是否这个子程序被调用了? 至少,在项目的需求定义中就涉及到它。 定义这个子程序将要解决的问题。应该足够详尽地规定它需要解决的问题,以便于创建。 如果结构设计是非常详尽的,那么这项工作可能已经完成了,结构设计应该至少指出以下这些 问题: · 这个子程序将要隐含的信息。 · 这个子程序的输入。 · 这个子程序的输出,包括受到影响的全局变量。 · 这个子程序将如何处理错误? PDL PDL 图 4-2 设计程序中的所有实现步骤 下面是在 RecordErrorMessage()这个子程序中,上述考虑是如何得以阐明的。这个子程序隐 含了如下两个事实;错误信息与现存的处理方式(交互式或者批处理),子程序的输入是非法代 码,要求两种输出方式:第一是错误信息;第二是 RecordErrorMessass()子程序返回到调用它的 程序时的状态。 问题说明之后,并没有直接给出解决方案。假设以这个例子来说,程序约定是在发现错误 时立即报告。在这种情况下,这个子程序必须报告它所发现的每一个错误,假定其它错误都已 经报告过了。根据要求,这时子程序应把状态变量设置为失败。 给子程序命名。给子程序命名似乎是小事一桩,但好的子程序名字往往是一个高质量软件 的标志之一,而且,命名并不是件容易的事情。一般来说,子程序应该有一清楚的、不容易引 起异义的名字。如果在给程序找一个好名字时感到困难,这往往意味着对程序的功能还不十分 清楚。一个模棱两可的名字就像是一个在进行竞选辩论的政治家,似乎他在说着什么,可是当 第四章 建立子程序的步骤 35 你仔细听时,又分辨不出他的话到底有什么意义、应尽量将名字起得清楚。如果产生一个模棱 两可名字的原因是模棱两可的结构设计,那么就应注意这个危险信号,这时应返回去改进结构 设计。 在这个例子中,RecordErrorMessage()的含义是很清楚的,因此是个好名字。 决定如何测试子程序。在编写子程序时,最好能同时考虑如何测试。这对进行单元测试工 作是很有益处的。 在这个例子中,输入很简单,就是错误代码。因此,可以计划用全部有效错误代码和一系 列无效代码来进行测试。 考虑效率。根据所处的情形,你可以用一种或两种方式来说明效率。 在第一种情形下,程序的绝大部分,性能并不是主要的,在这种情况下,应该把子程序作 成高度模块化而且具有很强的可读性,以便在今后需要时很容易对其作出改进。如果其模块化 程度很高,就可以在需要时,用更好的算法或者汇编语言编写的子程序来代替速度较慢的程序 而不致影响程序其它部分。 在第二种情形下,在大部分程序中,性能都是很重要的,这时,结构设计应该对子程序的 运行速度和允许使用的内存作出规定,只要按照速度和空间指标设计子程序就可以了。如果速 度和空间只有一方面是主要的,则可以牺牲一方面来满足另一方面的要求。在初始创建阶段, 对子程序作出足够调整以使它满足速度和空间要求是合理的。 除了以上指明的情况以外,不必浪费精力去考虑个别子程序的执行效率。优化的收益主要 来自高层次设计,而不是个别子程序、只有在高层次设计某方面有缺陷时,才需要进行微观优 化,而这点只在程序全部完成时才会知道。除非必要,不要浪费时间进行增量改进。 研究算法和数据结构。同时提高编码质量和效率的最有效办法是重新使用好的代码。在 学术文章中,已经有许多种算法被发明、讨论、检验和改进过。因此,与其花费时间去发明一 种别人已经为之写过博士学位论文的东西,倒不如花几分钟测览一个算法论著,看有多少种算 法可供选择。如果想使用某种已有的算法,切记要对其做出改进以适应你的程序语言。 编写 PDL。在做完上述工作之后,编写的时间可能已经不多了。本步骤的主要目的是,建 立一种可以在实际编写子程序时提供思想指导的文件。 在主要工作步骤完成之后,可以在高层次 PDL 水平上编写子程序。可以使用编辑程序或者 整体环境来编写 PDL,很快,这些 PDL 就将成为用程序语言编码的基础。 编写工作应该从抽象到具体。一个子程序最抽象的部分便是最开始的注释部分,这部分将 说明要求子程序作什么;因此,首先应该写一个关于编写子程序目的的精确说明。编写这个说 明也将帮你更清楚地理解这个子程序。如果在编写这部分说明时感到困难,那说明需要对这个 子程序在整个软件中的地位和作用作出更深刻的理解。总之,如果感到编写抽象说明时有困难, 应该想到可能是某一环节出了问题。以下是一个精确说明子程序作用的例子: This routine outputs an error message based on an error code supplied by the calling routine. The way it outputs the message depends on the current processing state,which it retrieves on its own.It returns a variable indicating success or failure. 写完抽象说明后,再编写关于这个子程序的高层次 PDL。下面就是一个关于前述例子的高 层次 PDL: 第四章 建立子程序的步骤 36 This routine outputs an error message based on an error code supplied by the calling routine.The way it outputs the message depends on the current processing state, which it retrieves on its own. It returns a variable indicating success or failure. set the default status look up the message based on the error code if the error code is valid determine the processing method if doing interactive processing print the error message interactively and declare success else doing batch processing if the batch message file opens properly log the error message to the batch file, close the file,and declare success else the message code is not valid notify the user that an interal error has been detected 应该注意的是这个 PDL 是在一个相当高的层次上写成的。它使用的不是程序语言,而是用 自然语言来精确表达设计思想的。 考虑数据。可以在编写过程中的几个不同地方设计数据。在这个例子中,数据非常简单, 因而数据操作并不是程序的主要部分。如果数据操作是程序的主要部分,那么在考虑程序的逻 辑结构之前,考虑主要数据是必要的。如果在进行子程序的逻辑设计时,已经有了关键数据结 构的定义,那将是大有裨益的。 检查 PDL。写 好 PDL 并设计完数据之后,应该花一点时间来检查一下 PDL。返回来看着所写 的 PDL,考虑一下应该怎样向别人说明。 请别人帮助看一下或听一下你的说明。也许你认为请别人看一个只有 11 行的 PDL 是很 愚蠢的,但你会对这样作的结果感到惊奇。PDL 使假设和高层次错误比程序语言代码容易被发 现。人们往往更愿意检查一个只有几行的 PDL,而不愿去检查一个有 35 行的 C 或 Pascal 子程 序。 要确认对子程序做什么和将怎样做已经有了清楚透彻的了解。如果在 PDL 这一层次上对这 点还没有概念上的了解,那么在编码阶段了解它的机会还有多少呢?如果连你都理解不了它的 话,又有谁会理解呢? 逐步细化。在开始编码之前,要尽可能多使用 PDL 尝试一些想法。一旦开始编码,就会对 所写的代码产生爱惜之情,这时,要再想把它扔掉重新开始是非常困难的。 通常的思想是逐步细化用 PDL 写成的子程序,直到可以在每行 PDL 语句下面添加一行代码 而成为子程序为止,并把原来的 PDL 当作注释文件,或许最初的 PDL 的某些部分过于简略,需 要进一步说明,那么切记一定要对其作出进一步说明。如果不能确认如何对某一部分编码,那 么就继续细化 PDL,直到可以确认为止。要不断地细化 PDL 并对其作出进一步说明,直到你看 到这样作是在浪费时间时,再开始实际的编码工作。 第四章 建立子程序的步骤 37 4.4 子程序编码 设计好子程序之后,就要开始实现。可以按照标准步骤实现,也可以根据需要作出改动。 图 4-3 给出了实现一个子程序时的步骤。 图 4-3 实现子程序的步骤 书写子程序说明。 编写子程序的接口语句——编写过程或函数说明,应采用所需要的语言, 无论 Pascal、C 或 Fortran 都可以,只要符合需要。把原来的抽象说明用程序语言实现,把它放 在原来写好的 PDL 位置之上。以下是前述子程序的接口语句和抽象说明,它是用 Pascal 写成的: procedure RecordErrorMessage ( ErrorCode:ErrorCode_t ; var Status:Status_t ); { This routine outputs an error message based on an error code Supplied by the calling routine. The way it outputs the message depends on the current processing state, which it retrieves on its own. It returns a variable indicating success or failure. } 这些是接口语句 已转化成 Pascal 风格 注释的标题注释 set the default staus look up the message based on the error code 第四章 建立子程序的步骤 38 if the error code is valid determine the processing method if doing interactive processing print the error message interactively and declare success else doing batch processing if the batch message file opens properly log the error message to the batch file, close the file, and declare success else the message code is not valid notify the user that an internal error has been detected 这时是指出接口假设的好时机。在本例中,接口变量 ErrorCode 和 Status 是简明的,并且 根据其特定用途排序,不含任何隐蔽信息。 把 PDL 转变成高层次注释。利用 Pascal 中的 Begin 和 End,或者 C 中的“{”和“}”,可 以把 PDL 变成注释,以下是把前述的 PDL 变成了 Pascal 语言: procedure RecordErrorMessage ( Errorcode:ErrorCode_t; var Status:Status_t ); { This routine outputs an error message based on an error code Supplied by the calling routine. The way it outputs the message depends on the current processing state, which it retrieves on its own. It returns a variable indicating success or failure.} begin { set the default status } { look up the message based on the error code } { if the error code is valid} { determine the processing method } { if doing interactive processing} { print the error message interactively and declare success} { else doing batch processing } { if the batch message file opens properly} {log the error message to the batch file, close the file. and declare success} {else the message code is not valid} { notify the user that an internal error has been detected} end; { RecordErrorMessage() } 这时,子程序的特点已经非常明显了,设计工作已经结束了,没看见任何代码,但已经知 道子程序如何工作了。把 PDL 转换成程序语言代码是一件机械、自然、容易的工作。如果你不 第四章 建立子程序的步骤 39 觉得是这样,那么还需要进一步细化 PDL,直到有这种感觉为止。 在每一行注释下面填上代码。在每一行 PDL 注释语句下面填上代码。这有点像给报纸排版。 首先画好轮廓线,然后再把每一篇文章填到空格中,每一个 PDL 注释行都相当于给代码画的轮 廓线,而代码相当于文章。同文学文章的长度一样,代码的长度也是由它所要表达的内容多少 决定的。程序的质量则取决于它的设计思想的侧重点和巧妙程度。 在本例中,前两行 PDL 注释产生了两行代码: Procedure RecordErrorMessage ( ErrorCode:ErrorCode_t; var status:Status_t ) ; { This routine outputs an error message based on an error code Supplied by the calling routine The way it outputs the message depends on the current processing state,which it retrieves on its own. It returns a variable indicating success or failure.} begin 这里是填充的代码 { Set the default status } Status:=Failure; { look up the message based on the error code} LookupErrorMessage(ErrorCode,ErrorMessage); 这里是新变量 ErrorMessage { if the error code is valid } { determine the processing method } { if doing interactive processing } { Print the error message interactively and declare success } { else doing batch processing } { if the batch message file opens properly } { log the error message to the batch file, close the file,and declare success } { else the message code is not valid } { notify the user that an internal error has been detected } end; { RecordErrorMessage() } 这是一个编码的开始,使用了变量 ErrorMessage,所以需要说明它。如果是在事后进行注 释,那么,用两个注释行来注释两行代码就不必要了。但是,采用目前这种方法,是注释的字 面内容而不是它注释了多少行代码。现在,注释行已经存在了,所以还是将其保留。 代码需要变量说明,而且在每一注释行下面都要加入代码,以下是完整的子程序: procedure RecordErrorMessage ( 第四章 建立子程序的步骤 40 ErrorCode:ErrorCode_t; var Status:Status_t ); {This routine outputs an error message based on an error code Supplied by the calling routine. The way it outputs the message depends on the current processing state, whict it retrieves on its own. It returns a variable indicating success or failure. } var 这里是变量声明 ProcessingMehod: ProcessingMethod_t; ErrorMessage: Message_t; FileStatus: Status_t; begin {set the default status} Status:=Failure; {look up the message based on the error code } LookupErrorMessage(ErrorCode,ErrorMessage); {if the error code is valid} if (ErrorMessage.ValidCode) then begin {determine the processing method} ProcessingMethod := CurrentProcessingMehod; {if doing interaction processing} if (ProcessingMethod = Interactive) then begin {print the error message interactively and declare success } PrintInteractiveMessage(ErrorMessage.Text); Status := Success end {else doing batch processing} else if (ProcessingMethod = Batch) then begin {if the batch message file opens properly} FileStatus := OpenMessageFile; If (FileStatus = Success) then begin {log the error message to the batch file,close the file, and declare success} 第四章 建立子程序的步骤 41 LogBatchMessage ( ErrorMessage.Text ) ; CloseMessageFile; Status := Success end { if } end { else } end { else the message code is not valid } else begin { notify the user that an interanl error has been detected } PrintlnteractiveMessage ( 'Internal Error; Invalid error code' , 'in RecordErrorMessage()' } end end; { RecordErrorMessage () } 每一个注释都产生了一行或一行以上的代码,每一块都在注释的基础上形成了一个完整的 思想。保留了注释以便提供一个关于代码的高层次解释,在子程序的开始,对使用的所有变量 都作了说明。 现在,让我们再回过头来看一下前面的关于这个例子的需求定义和最初的 PDL,从最初的 5 行需求定义到 12 行的初始 PDL,接着这段 PDL 又扩大成为一个较大的子程序。即使需求定 义是很详尽的。子程序的创建还是需要在 PDL 和编码阶段进行潜在的设计工作。这种低层次的 设计工作正是为什么编码不是一件琐碎事的原因,同时,也说明本书的内容是很重要的。 非正式地检查代码。在注释下面填上代码之后,可以对每一块代码作一简略检查。尽力想 一下什么因素可能破坏目前的块,然后证明这种情况不会发生。 一旦完成了对某一子程序的实现,停下来检查一下是否有误。在 PDL 阶段,就已经对其作 了检查。但是,在某些情况下,某些重大问题在子程序实现之前是不会出现的。 使得问题直到编码阶段才出现的原因是多方面的。在 PDL 阶段引入错误可能到了详尽的实 现阶段才会变得明显。一个在 PDL 阶段看起来完美无缺的设计,在用程序语言实现时可能会变 得一塌糊涂。在详尽的实现阶段,可能会发现在结构设计或功能分析阶段引入的错误,最后, 代码可能存在一种司空见惯的错误——混用语言,毕竟大家都不是尽善尽美的嘛。由于上述原 因,在继续工作之前,要检查一下代码。 进行收尾工作。检查完代码是否存在问题后,再检查一下它是否满足本书所提到的通用质 量标准。可以采取几个步骤来确认子程序的质量是否满足要求。 · 检查子程序的接口。确认所有的输入和输出数据都已作出了解释,并且使用了所有 参数。关于细节问题,见 5.7 节“怎样使用子程序参数”。 · 检查通用设计质量。确认子程序只完成一项任务而且完成得很好,与其它子程序交 叉是控制不严的表现。并且,应该采用了预防错误的设计。关于细节问题,见第五 章“高质量程序的特点”。 · 检查子程序的数据。查找出不精确的变量名、没有使用的数据、没有说明的数据等 第四章 建立子程序的步骤 42 等。要了解详情,见关于数据使用的第八到第十二章。 · 检查子程序的控制结构。查找无限循环、不适当的嵌套等错误。详见关于使用控制结 构的第 13 到 17 章。 · 检查子程序设计。确认已说明了子程序的表达式、参数表和逻辑结构。详见第 18 章 “设 计与风格”。 · 检查子程序的文档。确认被翻译成注释的 PDL 仍然是精确的。检查算法描述,查找接 口假设和非显式依赖的文件资料,查找不清楚的编码等等。详见第 19 章“自我证明的 代码”。 按需要重复步骤。如果程序的质量很差,请返回 PDL 阶段。高质量程序是一个逐步的过程, 所以在重新进行设计和实现活动时,不要犹豫不决。 4.5 检查子程序 在设计并实现了子程序之后,创建活动的第三个主要步骤是进行检查,以确认所实现的软 件是正确的。你或许会问,对代码进行的非正式检查和收尾工作难道不足以保证其正确性吗? 的确,这两项工作会从某种程度上保证代码正确性,但不能完全保证,在这一阶段工作中漏掉 的错误,只有在后面的测试工作中才会被发现。而到那时纠正它们的成本将变得很高,因此, 最好现在就进行查错工作。 在心里对子程序进行查错处理。在前面提到过的非正式检查和清扫工作就是两种内心检查 方法。另一方法是在心中执行每一个路径。在心中执行一个子程序是比较困难的,这个困难也 是造成很困难保持子程序小规模的原因之一。要保证检查到每一个规定路径和中止点,同时还 要检查所有的例外情况。可以自己进行这项工作,此时叫作“桌面检查”。也可以与同事们一道 检查,这叫作“同事评审”、“过一遍”或者“视察”,这要取决于如何进行这项工作。 业余爱好者与职业程序员之间的最大区别就是迷信还是理解。在这里,“迷信”这个词指并 不是指在月圆之夜产生各种错误或使你毛骨悚然的一段程序。它指的是你对代码的感觉代替对 代码的理解。如果你总是认为编译程序或者硬件系统有故障,那说明你还处在迷信阶段。只有 5% 的错误是由编译程序、硬件或者是操作系统引起的(Brown and sampson, 1973, Ostrand and Weyuher, 1984)。进入理解境界的程序员总是怀疑自己的工作,因为他们知道 95% 的错误出自 这里。要理解每一行编码的意义,并且要明白为什么需要它。没有仅仅因为有效便是正确的东 西。如果你不知道为什么它是有效的,那么往往它是无效的,只不过你没有发现罢了。 最后,要指出的是有了一个有效的子程序并非就完事大吉了。如果你不知道为什么它是有 效的,那就研究并讨论它,或者用替代方案重新实现一次,直到你弄明白为止。 编译子程序。如果检查完了子程序,那就开始编译它。直到现在才开始编译工作,似乎是 我们的工作效率太低了,因为早在几页之前程序就完成了。的确,如果早一些开始编译,让计 算机去检查没有声明的变量,命名冲突等是可能会节约一些时间。 但是,如果晚一些开始编译,将会获得许多收益。其中的一个主要原因是,一旦开始编译, 那么你脑袋里的秒表便开始嘀嗒作响了,在第一次编译之后,你就开始不停地想:下次编译一 定让它全对。结果,在这种“就只再编译一次”的压力下,作了许多匆忙的、更易产生错误的 修改,反而浪费了更多的时间。所以,在确信子程序是正确的之前,不要急于开始编译。 第四章 建立子程序的步骤 43 本书的重点之一,就是想告诉读者如何避免陷入把各种代码拼凑到一起,通过试运行检验 它是否有效的怪圈。而在确信程序是正确的之前,就匆忙开始编译,恰恰是陷入了这种怪圈。 如果你还没有进入这个怪圈,那最好还是当确信程序正确之后再开始编译。 以下是在编译时,尽可能地检查出全部错误的指导方针: · 尽可能把编译程序的警告级别调到最高。只要允许,编译程序应尽量测试,将发现许 多难以察觉的错误。 · 消除所有编译程序指出的错误和提出警告的原因。注意编译程序关于你的代码说了些 什么。大量的警告往往意味着代码质量不高,所以应该尽量理解所得到的每个警 告。 在实际中,反复出现的警告可能产生以下影响:你忽略掉它们,而事实上它们掩盖了 更严重的错误。或者它们会变得使人痛苦,就像日本式的灌水酷刑。因此,比较安全 或痛苦较小的办法,是消灭这些隐蔽的问题以消除警告。 使用计算机来检查子程序错误。子程序编译之后,将其放入调试程序,逐步运行每一行代 码,要保证每一行都是按预期的运行。用这种简单的办法,你可以发现许多错误。 在调试程序中逐步运行程序之后,用在开发子程序时设计的测试用例对其进行测试。或许 你将不得不搭些支架来支撑你的子程序——即那些仅在测试阶段用于支持子程序而最终不包括 在产品中的代码。这些代码可能是调用子程序的程序,也可能是被子程序所调用的子程序。 消除子程序中的错误。一旦发现有错误,就要消除它。如果开发的子程序此时问题较多, 那 它将在这一阶段耗费较长的时间。如果发现程序中的错误异乎寻常的多,那就重新开发一个, 不要试图修补它。修补往往意味着不充分的理解,而且肯定会在现在和将来产生更多的错误, 而进行一个全新的设计将防止这一点。恐怕没有比重新写一个完美无缺的子程序来代替一个漏 洞百出的子程序更能让人满意的事了。 4.5.l 检查表 创建子程序 · 是否检查过先决条件已经满足了? · 定义子程序将要解决的问题了吗? · 结构设计是否足够清楚,使得你可以给子程序起个好名字? · 考虑过如何测试子程序了吗? · 是否从模块化水平或者满足时间和内存需求角度考虑过效率问题? · 是否查阅过参考书;以寻找有帮助的算法? · 是否用详尽的 PDL 设计子程序? · 在必要时,是否在逻辑设计步骤前考虑了数据? · 是否检查过 PDL,它很容易理解吗? · 是否注意到了足以使你返回到结构设计阶段的警告(使用了全局数据,更适合其它子 程序的操作,等等)。 · 是否使用了 PDL 到代码流程,是否把 PDL 作为编码基础并把原有的 PDL 转为注释? · 是否精确地把 PDL 翻译成了代码? · 在作出假设时,验证它们了吗? · 是从几个设计方案中选择了最好的,还是随意选择了一个方案? 第四章 建立子程序的步骤 44 · 是否彻底理解你的代码?它容易理解吗? 4.6 小 结 · 要想写好 PDL,首先要用易懂的自然语言,避免拘泥于某种程序语言,其次要在意向 层次上写 PDL,描述设计作什么而不是如何作。 · PDL 到代码流程方法是详细设计的有力工具,而且使得编码非常容易。可以把 PDL 直 接翻译成注释,但要注意保证注释是精确而有用的。 · 应该在工作的每一步中都检查子程序,并鼓励同事们检查。这样,可以在投入的资金 和工作努力最少时便发现错误,从而极大降低改错成本。 第五章 高质量子程序的特点 45 第五章 高质量子程序特点 目录 5.1 生成子程序的原因 5.2 子程序名称恰当 5.3 强内聚性 5.4 松散耦合性 5.5 子程序长度 5.6 防错性编程 5.7 子程序参数 5.8 使用函数 5.9 宏子程序 5.10 小结 相关章节 生成子程序的步骤:见第 4 章 高质量模块的特点:见第 6 章 通用设计技术:见 7.5 节 软件结构设计:见 3.4 节 第四章讲述了生成子程序时应该采取的步骤,其重点是创建过程。本章的重点则是子程序 本身,即区分好的子程序和低劣子程序的特征。 如果在进入现实而又困难的子程序细节问题之前,想阅读关于高层次设计的问题,那么请 首先阅读第七章,然后再阅读本章。由于模块也要比子程序抽象,因此,也可在读完第六章后 再阅读本章。 在讨论高质量子程序的细节问题之前,我们首先来考虑两个基本名词。什么叫“子程序”? 子程序是具有单一功能的可调用的函数或过程。比如 C 中的函数,Pascal 或 Ada 中的函数或过 程,Basic 中的子程序或 Fortran 中的子程序。有时,C 中的宏指令或者 Basic 中用 GOSUB 调用 的代码块也可认为是子程序。在生成上述函数或过程中,都可以使用创建高质量子程序所使用 的技术。什么是“高质量的子程序”?这是一个比较难以回答的问题。反过来最简单回答方式 是指出高质量子程序不是什么。下面是一个典型的劣质子程序(用 Pascal 写成): Procedure HandleStuff ( Var InputRec:CORP_DATA,CrntQtr:integer, EmpRec:Emp_DATA, Var EstimRevenue:Real, YTDRevenue:Real, ScreenX:integer,ScreenY:integer,Var NewColor:Color_TYPE, Var PrevColor:COLOR_TYPE,Var Status:STATUS_TYPE, ExpenseType:integer); 第五章 高质量子程序的特点 46 begin for i := 1 to 100 do begin InputRec.revenue[1]:= 0; InputRec.expense[i]:=CorpExpensse[CrntQtr,i] end; UpdateCorpDatabase(EmpRec); EstimRevenue:=YTDRevenue * 4.0 /real(CrntQtr) NewColor:=PrevColor; status:=Success; if ExpenseType=1 then begin for i:= 1 to 12 do Profit[i]:= Revenue[i]-Expense.Type[i] end else If ExpneseType= 2 then begin Peofit[i]:=Revenue[i] - Expense.Type2[i] end else if ExpenseType= 3 then begin Profit[i]:=Revenue[i] - Expense.Type3[i] end end 这个子程序有什么问题?给你一个提示:你应该至少从中发现 10 个问题。当你列出所发现 的问题后,再看一下下面所列出的问题; · 程序的名字让人困惑。HandleStuff()能告诉我们程序是干什么的吗? · 程序没有被说明(关于说明的问题已经超出了个别子程序的范围,详见第 19 章“自我 说明的子程序”)。 · 子程序的布局不好。代码的物理组织形式几乎没有给出其逻辑组织形式的任何信息。 布局的使用过于随心所欲,程序每一部分的布局都是不一样的。关于这一点。只要比 较一下 ExpenseType=2 和 ExpenseType=3 两个地方的风格就清楚了(关于布局问题, 详见第十八章“布局与风格”)。 · 子程序的输入变量值 InputRec 被改变过。如果它作为输入变量,那它的值就不该变 化。如果要变化它的值,就不该称之为输入变量 InputRec。 · 子程序进行了全局变量的读写操作。它从 CorpExpense 中读入变量并写给 Profit。它应 该与存取子程序通信,而不应直接读写全局变量。 · 这个子程序的功用不是单一的。它初始化了某些变量。对一个数据库进行写操作,又 进行了某些计算工作,而它们又看不出任何联系。一个子程序的功用应该是单一,明 了的。 · 子程序中没有采取预防非法数据的措施。如果 CrntQtr 的值为“0”,那么,表达式 YTDRevenue*4.0/real(CrntQtr)就会出现被零除的错误。 · 程序中使用了几个常数:100, 4.0, 12, 2 和 3。关于“神秘”(magic)数的问题见 11.1 节“常数” 第五章 高质量子程序的特点 47 · 在程序中仅使用了域的 CORP_DATA 型参数的两个域。如果仅仅使用两个域,那就该 仅仅传入特定的域而不是整个结构化变量。 · 子程序中的一些参数没有使用过。ScreenX 和 ScreenY 在程序中没有涉及。 · 程序中的一个参数被错误标定了。PrevColor 被标定为变量型参数,然而在程序中又没 有对其赋值。 · 程序中的参数太多。程序中参数个数的合理上限应该是七个左右。而这个程序中则多 达 11 个。程序中的参数多得怕人,恐怕没谁会仔细检查它们,甚至连数一下都不愿意。 除了计算机本身之外,子程序可以说是计算机科学最重大的发明。子程序使得程序非常好 读而且也非常容易理解,编程语言中的任何特性都不能和这一点相比。像上例中那样使用子程 序,简直就是对子程序的践踏,甚至可以说是一种犯罪。 子程序也是节省空间和提高性能的最好手段。想象一下,如果用代码段去代替程序中对子 程序的每一次调用,那么程序将会有多么庞大。如果不是把多次重复使用的代码段存放在子程 序中,而是直接把它放在程序中,那么对其进行性能改进将是一件很困难的事。是子程序使现 代编程成为可能。 现在,你可能有些不耐烦。“是好,子程序的确很了不起,我一直都在使用它”。你说,“你 的讨论似乎像是在纠正什么,你到底想让我做什么呢?” 我想说的是:有许多合理的原因使得我们去生成子程序。但是生成方法有好有坏。作为一 个计算机专业的本科生,可以认为生成子程序的主要原因是避免代码段的重复。我所用的入门 课本告诉我说,之所以使用子程序,是因为它可以避免代码段的重复,从而使得一个程序的开 发、调试、注释和维护工作都变得非常容易。除了一些关于如何使用参数和局部变量的语法细 节之外,这就是那本课本关于子程序理论与实践内容的全部。这实在不是一个完全而合理的解 释。下面这节将详细描述为什么和怎样生成子程序。 5.1 生成子程序的原因 以下是关于为什么要生成子程序的一些合理原因,其中有些原因之间可能有互相重叠的地 方。 降低复杂性。使用子程序的最首要原因是为了降低程序的复杂性,可以使用子程序来隐含 信息,从而使你不必再考虑这些信息。当然,在编写子程序时,你还需要考虑这些信息。但是, 一旦写好子程序,就可能不必再考虑它的内部工作细节,只要调用它就可以了。创建子程序的 另外一个原因是尽量减小代码段的篇幅,改进可维护性和正确性。这也是一个不错的解释,但 若没有子程序的抽象功能,将不可能对复杂程序进行明智的管理。 一个子程序需要从另一个子程序中脱离出来的原因之一是,过多重数的内部循环和条件判 断。这时,可以把这部分循环和判断从子程序中脱离出来,使其成为一个独立的子程序,以降 低原有子程序的复杂性。 避免代码段重复。无可置疑,生成子程序最普遍的原因是为了避免代码段重复。事实上, 如果在两个不同子程序中的代码很相似,这往往意味着分解工作有误。这时,应该把两个子程 序中重复的代码都取出来,把公共代码放入一个新的通用子程序中,然后再让这两个子程序调 第五章 高质量子程序的特点 48 用新的通用子程序。通过使公共代码只出现一次,可以节约许多空间。这时改动也很方便,因 为只要在一个地方改动代码就可以了。这时代码也更可靠了,因为只需在一个地方检查代码。 而且,这也使得改动更加可靠,因为,不必进行不断地、非常类似地改动,而这种改动往往又 是认为自己编写了相同的代码这一错误假设下进行的。 限制了改动带来的影响。由于在独立区域进行改动,因此,由此带来的影响也只限于一个 或最多几个区域中。要把最可能改动的区域设计成最容易改动的区域。最可能被改动的区域包 括:硬件依赖部分、输入输出部分、复杂的数据结构和商务规则。 隐含顺序。把处理事件的非特定顺序隐含起来是一个很好的想法。比如,如果程序通常先 从用户那里读取数据,然后再从一个文件中读取辅助数据,那么,无论是读取用户数据的子程 序,还是读取文件中数据的子程序,都不应该对另一个子程序是否读取数据有所依赖。如果利 用两行代码来读取堆栈顶的数据,并减少一个 Stacktop 变量,应把它们放入一个 PopStack()子程 序中,在设计系统时,使哪一个都可以首先执行,然后编写一个子程序,隐含哪一个首先执行 的信息。 改进性能。通过使用子程序,可以只在一个地方,而不是同时几个地方优化代码段。把相 同代码段放在子程序中,可以通过优化这一个子程序而使得其余调用这个子程序的子程序全部 受益。把代码段放入子程序也使得用更快的算法或执行更快的语言(如汇编)来改进这段代码 的工作变得容易些。 进行集中控制。在一个地方对所有任务进行控制是一个很好的想法。控制可能有许多形式。 知道一个表格中的入口数目便是其中一种形式,对硬件系统的控制,如对磁盘、磁带、打印机、 绘图机的控制则是其中另外一种形式。使用子程序从一个文件中进行读操作,而使用另一个子 程序对文件进行写操作便是一种形式的集中控制。当需要把这个文件转化成一个驻留内存的数 据结构时,这一点是非常有用的,因为这一变动仅改变了存取子程序。专门化的子程序去读取 和改变内部数据内容,也是一种集中的控制形式。集中控制的思想与信息隐含是类似的,但是 它有独特的启发能力,因此,值得把它放进你的工具箱中。 隐含数据结构。可以把数据结构的实现细节隐含起来,这样,绝大部分程序都不必担心这 种杂乱的计算机科学结构,而可以从问题域中数据是如何使用的角度来处理数据。隐含实现细 节的子程序可以提供相当高的抽象价值,从而降低程序的复杂程度。这些子程序把数据结构、 操作集中在一个地方,降低了在处理数据结构时出错的可能性。同时,它们也使得在不改变绝 大多数程序的条件下,改变数据结构成为可能。 隐含全局变量。如果需要使用全局变量,也可以像前述那样把它隐含起来、通过存取子程 序来使用全局变量有如下优点:不必改变程序就改变数据结构;监视对数据的访问;使用存取 子程序的约束还可以鼓励你考虑一下这个数据是不是全局的;很可能会把它处理成针对在一个 模块中某几个子程序的局部数据,或处理成某一个抽象数据的一部分。 隐含指针操作。指针操作可读性很差,而且很容易引发错误。通过把它们独立在子程序中, 可以把注意力集中到操作意图而不是机械的指针操作本身。而且,如果操作只在一处进行,也 更容易确保代码是正确的。如果找到了比指针更好的数据结构,可以不影响本应使用指针的子 程序就对程序作改动。 重新使用代码段。放进模块化子程序中的代码段重新使用,要比在一个大型号程序中的代 码段重新使用起来容易得多。 第五章 高质量子程序的特点 49 计划开发一个程序族。如果想改进一个程序,最好把将要改动的那部分放进子程序中,将 其独立。这样,就可以改动这个子程序而不致影响程序的其余部分,或者干脆用一个全新的子 程序代替它。几年前,我曾经负责一个替保险推销员编写系列软件的小组,我们不得不根据每 一个推销员的保险率、报价单格式等等来完成一个特定的程序。但这些程序的绝大部分又都是 相同的:输入潜在客户的子程序,客户数据库中存储的信息、查看、计算价格等等。这个小组 对程序进行了模块化,这样,随推销员而变化的部分都放在自己的模块中。最初的程序可能要 用三个月的时间来开发,但是,在此之后,每来一个推销员,我们只改写其中屈指可数的几个 模块就可以了。两三天就可能写完一个要求的程序,这简直是一种享受! 提高部分代码的可读性。把一段代码放入一个精心命名的子程序,是说明其功能的最好办 法。这样就不必阅读这样一段语句: if ( Node <> nil ) while ( Node.Next <> nil ) do Node = Node.Next LeafName = Node.Name else LeafName = " " 代替它的是: LeafName = GetLeafName(Node) 这个程序是如此简短,它所需要的注释仅仅是一个恰当的名字而已。用一个函数调用来代 替一个有六行的代码段,使得含有这段代码的子程序复杂性大为降低,并且其功能也自动得到 了注释。 提高可移植性。可以使用子程序来把不可移植部分、明确性分析和将来的移植性工作分隔 开来,不可移植的部分包括:非标准语言特性、硬件的依赖性和操作系统的依赖性等。 分隔复杂操作。复杂操作包括:繁杂的算法、通信协议、棘手的布尔测试、对复杂数据的 操作等等。这些操作都很容易引发错误。如果真的有错误,那么如果这个错误是在某个子程序 中,而不是隐藏在整个程序中的话,查找起来要容易得多。这个错误不会影响到其它子程序, 因为为了修正错误只要改动一个子程序就可以了。如果发现了一个更为简单迅速的算法,那么 用它来代替一个被独立在子程序中的算法是非常容易的。在开发阶段,尝试几种方案并选择其 中一个最好的是非常容易的。 独立非标准语言函数的使用。绝大多数实现语言都含有一些非标准的但却方便的扩展。使 用这种扩展的影响是两面性的,因为在另外一个环境下它可能无法使用。这个运行环境的差异 可能是由于硬件不同、语言的生产商不同、或者虽然生产商相同、但版本不同而产生的。如果 使用了某种扩展,可以建立一个作为进入这种扩展大门的子程序。然后,在需要时,可以用订 做的扩展来代替这一非标准扩展。 简化复杂的布尔测试。很少有必要为理解程序流程而去理解复杂的布尔测试。把这种测试 放入函数中可以提高代码的可读性,因为: (1) 测试的细节已经被隐含了。 (2) 清楚的函数名称已经概括了测试目的。 赋予这种测试一个函数,该函数强调了它的意义,而且这也鼓励了在函数内部增强其可读 第五章 高质量子程序的特点 50 性的努力。结果是主程序流和测试本身都显得更加清楚了。 是出于模块化的考虑吗?绝不是。有了这么些代码放入子程序的理由,这个理由是不必要 的。事实上,有些工作更适合放在一个大的子程序中完成(关于程序最佳长度的讨论见 5.5 节“子 程序长度”)。 5.1.1 简单而没有写入子程序的操作 编写子程序的最大心理障碍是不情愿为了一个简单的目的而去编写一个简单的子程序。写 一个只有两或三行代码的子程序看起来是完全没有必要的。但经验表明,小的子程序也同样是 很有帮助的。 小型子程序有许多优点,其中之一是改进了可读性。我曾在程序中采用过如下这样一个仅 有一行的代码段,它在程序中出现了十几次: Points = DeviceUnits * ( POINTS_PER_INCH / DeviceUnitsPerInch ( ) ) 这决不是你所读过的最复杂的一行代码。很多人都明白它是用来转换的。他们也会明白程 序中的每行这个代码都在作同一件事,但是,它还可以变得更清楚些,所以,我创建了一个恰 当命名的子程序来作这些工作。 DeviceUnitsToPoints(DeviceUnits Integer): Integer; begin DeviceUnitsToPoints = DeviceUnits * ( POINTS_PER_INCH / DeviceUnitsPerInch ( ) ) end 在用这段子程序来代替那些十几次重复出现的代码行后,这些代码行基本上都成了如下的 样子: Points = DeviceUnitsToPoints ( DeviceUnits ) 这显然更具可读性,甚至已经达到了自我说明的地步。 这个例子还暗示了把简单操作放入函数的另外一个原因:简单操作往往倾向于变成复杂操 作。在写这个子程序时我还不知道这一点,但在某种情况下,当某个设备打开时, DeviceUnitPerInch()会返回零,这意味着我不得不考虑到被“0”除的情况,这样,又需要另外 的三行代码; DeviceUnitsToPoints( DeviceUnit:integer):integer; begin if( DeviceUnitsPerInch ( ) <> 0 ) then DeviceUnitsPoints = DeviceUnits * ( POINTS_PER_INCH / DeviceUnitsPerInch ( ) ) else DeviceUnitsToPoints= 0 end 如果原来的代码行仍然在程序中出现十几次,那么这一测试也要重复十几次,需要新增加 36 行代码。而一个简单子程序轻而易举地便把 36 变成了 3。 第五章 高质量子程序的特点 51 5.1.2 创建子程序的理由总结 以下是创建子程序理由概述: · 降低复杂性 · 避免重复代码段 · 限制改动带来的影响 · 隐含顺序 · 改进性能 · 进行集中控制 · 隐含数据结构 · 隐含指针操作 · 隐含全局变量 · 促进重新使用代码段 · 计划开发一个软件族 · 改善某一代码段可读性 · 改善可移植性 · 分隔复杂操作 · 独立非标准语言函数的使用 · 简化复杂的布尔测试 5.2 子程序名称恰当 一个恰当的子程序名称应该清晰地描述出子程序所作的每一件事。以下是给子程序有效 命名的指导方针: 对于过程的名字,可以用一个较强的动词带目标的形式。一个带有函数的过程往往是对某 一目标进行操作。名字应该反映出这个过程是干什么的,而对某一目标进行操作则意味着我们 应该使用动宾词组。比如,PrintReport),Checkotderlnfo()等,都是关于过程的比较恰当的名 字。 在面向对象的语言中,不必加上对象名,因为对象本身在被调用时就已经出现了。这时可 求助于诸如 Report.Print(),OrderInfo.Check()和 MonthlyRevenue.Cafe()等名字。而像 RePort. PrintReport 这类名字则是冗余的。 对于函数名字,可以使用返回值的描述。一个函数返回到一个值,函数应该用它所返回的 值命名,例如 Cos(),PrinterReady(),CurrentPenColor()等等都是不错的函数名字,因为它精确 地描述了函数将返回什么。 避免无意义或者模棱两可的动词。有些动词很灵活,可以有任何意义,比如 HandleCalculation(),ProcessInput()等子程序名词并没有告诉你它是作什么的。这些名字至多 告诉你,它们正在进行一些与计算或输入等有关的处理。当然,有特定技术情形下使用“handle” 等词是个例外。 有时,子程序的唯一问题就是它的名字太模糊了,而子程序本身的设计可能是很好的。如 第五章 高质量子程序的特点 52 果用 FormatAndPrintOutput() 来代替 HandleOutPut() ,这是一个很不错的名字。 在有些情况下,所用的动词意义模糊是由于子程序本身要做的工作太模糊。子程序存在着 功能不清的缺陷,其名字模糊只不过是个标志而已。如果是这种情况,最好的解决办法是重新 构造这个子程序,弄清它们的功能,从而使它们有一个清楚的、精确描述其功能的名字。 描述子程序所做的一切。在子程序名字中,应描述所有输出结果及其附加结果。如果一个 子程序用于计算报告总数,并设置一个全局变量来表示所有的数据都已准备好了,正等待打印, 那么,ComputeReportTotal()就不是一个充分的名字了。而 ComputeReport TotalAndSetPrintingReadyVar()又是一个太长而且太愚蠢的命名。如果子程序带有附加结果, 那必然会产生许多又长又臭的名字。解决的办法不应该是使用描述不足名字,而是采用直接实 现每件事的原则来编程,从而避免程序带有附加结果。 名字的长度要符合需要。研究表明,变量名称的最佳长度是 9 到 15 个字母,子程序往往比 变量要复杂,因而其名字也要长些。南安普敦大学的 MichaelRees 认为恰当的长度是 20 到 35 个字母。但是,一般来说 15 到 20 个字母可能更现实一些,不过有些名称可能有时要比它长。 建立用于通用操作的约定。在某些系统中,区分各种不同的操作是非常重要的。而命名约 定可能是区分这些操作最简单也是最可靠的方法。比如,在开发 0S/2 显示管理程序时,如果子 程序是关于直接输入的,就在其名称前面加一个“Get”前缀,如果是非直接输入的则加“Query” 前缀,这样,返回当前输入字符的 GetlnputChar()将清除输入缓冲区.而同样是返回当前输入 字符的 QuerylnPutChar()则不清除缓冲区。 5.3 强内聚性 内聚性指的是在一个子程序中,各种操作之间互相联系的紧密程度。有些程序员喜欢用“强 度”一词来代替内聚性,在一个子程序中各种操作之间的联系程度有多强?一个诸如 Sin()之类 的函数内聚性是很强的,因为整个子程序所从事的工作都是围绕一个函数的。而像 SinAndTan() 的内聚程度就要低得多了,因为子程序中所进行的是一项以上的工作。强调强相关性的目的是, 每一个子程序只需作好一项工作,而不必过分考虑其它任务。 这样作的好处是可以提高可靠性。通过对 450 个 Fortran 子程序的调查表明,50%的强内聚 性子程序是没有错误的,而只有 18%的弱内聚性子程序才是无错的(Card,Carch 和 Agresti 1986)。 另一项对另外 450 个子程序的调查则表明,弱内聚性子程序的出错机会要比强内聚性出错机会 高 6 倍,而修正成本则要高 19 倍(Selby 和 Basili 1991)。 关于内聚性的讨论一般是指几个层次。理解概念要比单纯记住名词重要得多。可以利用这 些概念来生成内聚性尽可能强的子程序。 5.3.1 可取的内聚性 内聚性的想法是由 Wayne Stevens,Glenford Myers 和 Larry Constantine 等人在 1974 年发表 的一篇论文中提出来的,从那以后,这个想法的某些部分又逐渐得到了完善。以下是一些通常 认为是可以接受的一些内聚类型: 功能内聚性。功能内聚性是最强也是最好的一种内聚,当程序执行一项并且仅仅是一项工 作时,就是这种内聚性,这种内聚性的例子有: sin(), GetCustomerName(), EraseFile(), 第五章 高质量子程序的特点 53 CaldoanPayment()和 GetIconlocation()等等。当然,这个评价只有在子程序的名称与其实际内容 相符时才成立。如果它们同时还作其它工作,那么它们的内聚性就要低得多而且命名也不恰当。 顺序内聚性。顺序内聚性是指在子程序内包含需要按特定顺序进行的、逐步分享数据而又 不形成一个完整功能的操作,假设一个子程序包括五个操作:打开文件、读文件、进行两个计 算、输出结果、关闭文件。如果这些操作是由两个子程序完成的,DoStep1()打开文件、读文件 和计算操作,而 DoStep2()则进行输出结果和关闭文件操作。这两个子程序都具有顺序内聚性。 因为用这种方式把操作分隔开来,并没有产生出独立的功能。 但是,如果用一个叫作 GetFileData()的子程序进行打开文件和读文件的操作,那么这个子 程序将具有功能内聚性。当操作来完成一项功能时,它们就可以形成一个具有功能内聚性的子 程序。实际上,如果能用一个很典型的动宾词组来命名一个子程序,那么它往往是功能内聚性, 而不是顺序内聚性。给一个顺序内聚性的子程序命名是非常困难的,于是便产生了像 Dostep1() 这种模棱两可的名字。这往往意味着你需要重新组织和设计子程序,以使它是功能内聚性的。 通讯内聚性。通讯内聚性是在一个子程序中,两个操作只是使用相同数据,而不存在其它 任何联系时产生的。比如,在 GetNameAndChangePhoneNumber()这个子程序中,如果 Name 和 PhoneNumber 是放在同一个用户记录中的,那么这个子程序就是通讯内聚性。这个子程序从事 的是两项而不是一项工作,因此,它不具备功能内聚性。Name 和 PhoneNamber 都存储在用户 记录中,不必按照某一特定顺序来读取它们,所以,它也不具备顺序内聚性。 这个意义上的内聚性还是可以接受的。在实际中,一个系统可能需要在读取一个名字的同 时变更电话号码。一个含有这类子程序的系统可能有些显得别扭,但仍然很清楚且维护性也不 算差,当然从美学角度来说,它与那些只作一项工作的子程序还有一定差距。 临时内聚性。因为同时执行的原因才被放入同一个子程序里,这时产生临时内聚性。典型 的例子有;Startup(),CompleteNewEmployee(),Shutdown()等等,有些程序员认为临时内聚性是 不可接受的,因为它们有时与拙劣的编程联系在一切,比如,在像 Startup()这类子程序中往往含 有东拼西凑的杂烩般的代码。 要避免这个问题,可以把临时内聚性子程序设计成一系列工作的组织者。前述的 Startup ()子程序进行的操作可能包括:读取一个配置文件、初始化一个临时文件、建立内存管理、显示 初始化屏幕。要想使它最有效地完成这些任务,可以让这个子程序去调用其它的专门功能的子 程序,而不是由它自己直接来完成这些任务。 5.3.2 不可取的内聚性 其余类型的内聚性,一般来说都是不可取的。其后果往往是产生一些组织混乱而又难以调 试和改进的代码。如果一个子程序具有不良的内聚性,那最好重新创建一个较好的子程序,而 不要去试图修补它。知道应该避免什么是非常重要的,以下就是一些不可取的内聚性: 过程内聚性。当子程序中的操作是按某一特定顺序进行的,就是过程内聚性。与顺序内聚 性不同,过程内聚性中的顺序操作使用的并不是相同数据。比如,如果用户想按一定的顺序打 印报告,而所拥有的子程序是用于打印销售收入、开支、雇员电话表的。那给这个子程序命名 是非常困难的,而模棱两可的名字往往代表着某种警告。 第五章 高质量子程序的特点 54 逻辑内聚性。当一个子程序中同时含有几个操作,而其中一个操作又被传进来的控制标志 所选择时,就产生了逻辑内聚性。之所以称之为逻辑内聚性,是因为这些操作仅仅是因为控制 流,或者说“逻辑”的原因才联系到一起的,它们都被包括在一个很大的 if 或者 case 语句中, 它们之间并没有任何其它逻辑上的联系。 举例来说,一个叫作 InputAll()的子程序,程序的输入内容可能是用户名字、雇员时间卡信 息或者库存数据,至于到底是其中的哪一个,则由传入子程序的控制标志决定。其余类似的子 程序还有 ComputeAll(),EditAll(),PrintAll()等等。这类子程序的主要问题是一定要通过传入一 个控制标志来决定子程序处理的内容。解决的办法是编写三个不同的子程序,每个子程序只进 行其中一个操作。如果这三个子程序中含有公共代码段,那么还应把这段代码放入一个较低层 次的子程序中,以供三个子程序调用。并且,把这三个子程序放入一个模块中。 但是,如果一个逻辑内聚性的子程序代码都是一系列 if 和 case 语句,并且调用其它子程序, 那么这是允许的。在这种情况下,如果程序的唯一功能是调度命令,而它本身并不进行任何处 理,那么这可以说是一个不错的设计。对这种子程序的专业叫法是“事物处理中心”,事物处理 中心往往被用作基础环境下的事件处理,比如,Apple Macintosh 和 Microsoft Windows。 偶然内聚性。当同一个子程序中的操作之间无任何联系时,为偶然内聚性。也叫作“无内 聚性”。本章开始时所举的 Pascal 例程,就是偶然内聚性。 以上这些名称并不重要,要学会其中的思想而不是这些名词。写出功能内聚性的子程序几 乎总是可能的,因此,只要重视功能内聚性以获取最大的好处就可以了。 5.3.3 内聚性举例 以下是几个内聚性的例子,其中既有好的,也有坏的: 功能内聚性例子。比如计算雇员年龄并给出生日的子程序就是功能内聚性的,因为它只完 成一项工作,而且完成得很好。 顺序内聚性的例子。假设有一个按给出的生日计算雇员年龄、退休时间的子程序,如果它 是利用所计算的年龄来确定雇员将要退休的时间,那么它就具有顺序内聚性。而如果它是分别 计算年龄和退休时间的,但使用相同生日数据,那它就只具有通讯内聚性。 确定程序存在哪种不良内聚性,还不如确定如何把它设计得更好重要。怎样使这个子程序 成为功能内聚性呢?可以分别建立两个子程序,一个根据生日计算年龄,另外一个根据生日确 定退休时间,确定退休时间子程序将调用计算年龄的程序,这样,它们就都是功能内聚性的, 而且,其它子程序也可以调用其中任一个子程序,或这两个部调用。 通讯内聚性的例子。比如有一个打印总结报告,并在完成后重新初始化传进来的总结数据 的子程序,这个子程序具有通信内聚性,因为这两个操作仅仅是由于它们使用了相同的数据才 联系在一起。 同前例一样,我们考虑的重点还是如何把它变成是功能内聚性,总结数据应该在产生它的 地方附近被重新初始化,而不应该在打印子程序中重新初始化。把这个子程序分为两个独立的 子程序.第一个打印报告,第二个则在产生或者改动数据的代码附近重新初始化数据。然后, 利用一个较高层次的子程序来代替原来具有通讯相关的子程序,这个子程序将调用前面两个分 出来的子程序。 逻辑内聚性的例子。一个子程序将打印季度开支报告、月份开支报告和日开支报告.具体 第五章 高质量子程序的特点 55 打印哪一个,将由传入的控制标志决定,这个子程序具有逻辑内聚性,因为它的内部逻辑是由 输进去的外部控制标志决定的。显然,这个子程序不是按只完成一项工作并把它作好的原则。 怎样使这个子程序变为功能内聚性呢?建立三个子程序:一个打印季度报告,一个打印月 报告、一个打印日报告,改进原来的子程序,让它根据传送去控制标志来调用这三个子程序之 一。调用子程序将只有调用代码而没有自己的计算代码,因而具有功能内聚性。而三个被调用 的子程序也显然是功能内聚性的。非常巧合,这个只负责调用其它子程序的子程序也是一个事 务处理中心。最好用如 DispatchReportPrinting()之类带有“调度”或“控制”等字眼的词来给事 务处理中心命名,以表示它只负责命令的调度,而本身并不做任何工作。 逻辑内聚性的另一个例子。考虑一个负责打印开支报表、输入新雇员名字并备份数据库的 子程序,其具体执行内容将由传入的控制标志控制。这个子程序只具有逻辑内聚性,虽然这个 关联看起来是不合逻辑的。 要想使它成为功能内聚性,只要按功能把它分成几部分就可以了。不过,这些操作有些过 于凌乱。因此,最好重新建立一个调用各子程序的代码。当拥有几个需要调用的子程序时,重 新组织调用代码是比较容易的。 过程内聚性的例子。假设有一个子程序,它产生读取雇员的名字,然后是地址,最后是它 的电话号码。这种顺序之所以重要,仅仅是因为它符合用户的需求,用户希望按这种顺序进行 屏幕输入。另外一个子程序将读取关于雇员的其它信息。这个子程序是过程内聚性,因为是由 一个特定顺序而不是其它任何原因,把这些操作组合在一起的。 与以前一样,如何把它变为功能内聚性的答案仍然是把它分为几个部分,并把这几部分分 别放入程序中。要保证调用子程序的功能是单一、完善的。调用子程序应该是诸如 GetEmployeeData()这样的子程序,而不该是像 GetFirstPartofEmployeeData()这类的子程序。可能 还要改动其余读取数据的子程序。为得到功能内聚性,改动几个子程序是很正常的。 同时具有功能和临时内聚性的程序。考虑一个具有完成一项事物处理所有过程的子程序, 从用户那里读取确认信息,向数据存入一个记录,清除数据域,并给计数器加 1。这个程序是 功能内聚性的,因为它只从事一项工作,进行事物处理,但是,更确切地说,这个子程序同时 也是临时内聚性的,不过当一个子程序具有两种以上内聚性时,一般只考虑最强的内聚性。 这个例子提出了如何用一个名字恰如其分地抽象描述出程序内容的问题。比如可以称这 个子程序为 ConfirmEntryAndAdjustData(),表示这个干程序仅具有偶然内聚性。而如果称它为 CompleteTransaction(),那么就可能清楚地表示出这个子程序仅具有一个功能,而已具有功能内 聚性。 过程性、临时或者可能的逻辑内聚性。比如一个进行某种复杂计算前 5 个操作,并把中间 结果返回到调用子程序。由于 5项操作可能要用好几个小时,因此当系统瘫痪时,这个子程序 将把中间结果存入一个文件中,然后,这个子程序检查磁盘,以确定其是否有足够空间来存储 最后计算结果,并把磁盘状态和中间结果返回到调用程序。 这个子程序很可能是过程内聚性的,但你也可能认为它具有临时内聚性,甚至具有逻辑内 聚性。不过,不要忘了问题的关键不是争论它具有哪种不好的内聚性,而是如何改善其内聚性。 原来的子程序是由一系列令人莫名其妙的操作组成的,与功能内聚性相距甚远,首先,调 用子程序不应该调用一个,而应该调用几个独立的子程序:l)进行前 5 步计算的子程序;2)把 中间结果存入一个文件;3)确定可用的磁盘存储空间。如果调用子程序被称作 第五章 高质量子程序的特点 56 ComputeExtravagantNumber(),那么它不应该把中间结果写入一个文件,也决不该为后来的操作 检查磁盘剩余空间,它所作的就仅限于计算一些数而已。对这个子程序的精心重新设计,将至 少影响到一至两个层次上的子程序,对于这顶任务的较好设计,见图 5-l。 图中画阴影的部分是由原 中它们位于不同的层次上, 5.4 松散耦合性 所谓耦合性指的是两个子程序之间联系的紧密程度。耦合性与内聚性是不同的。内聚性 是 是它们之间的耦合是非常松散的,任一个子程序都能很容 它们像商业上的伙伴 一样 5.4.1 耦合标准 以下是几条估计子程序间耦合程度的标准: 联系的数量多少。对于耦合来说,联系的数 图 5-1 进行任务分解以获得功能内聚性举例 来的子程序从事的工作,在新组织结构 这就是为什么为了把这些工作放入恰当的子程序中,要进行这么多重新组织工作的原因。用几 个功能内聚性的子程序来代替一个具有不良内聚性的子程序是很平常的。 指一个子程序的内部各部分之间的联系程度,而耦合指的是子程序之间的联系程度。研究它 们的目的是建立具有内部整体性(强内聚性),而同时与其它子程序之间的联系的直接、可见、 松散和灵活的子程序(松散耦合)。 子程序之间具有良好耦合的特点 易地被其它子程序调用。火车车箱之间的联接是非常容易的,只要把两节车箱推撞到一起,挂 钩就会自动挂上,想象一下,用螺栓把它们固定到一起,或者只有特定的车厢之间才能联接到 一起,那么事情将会有多么麻烦。火车车厢之间的联接之所以非常容易,是因为它们的联接装 置非常简单。同样,在软件中,也应该使子程序之间的耦合尽量简单。 在建立一个子程序时,应尽量避免它对其它子程序有依赖性,应该使 相互分离,而不要使它们像连体婴儿一样密不可分。类似 Sin()的函数是松散耦合的,因为 它所需要的只是一个传递进去的角度值。而类似 InitVars(varl,varZ,var3,⋯⋯,varN)的函 数则是紧密耦合的,因为调用程序事实上知道函数内部做什么。依靠使用同一全局变量联系在 一起的子程序之间,其耦合程度则更高。 耦合规模。所谓耦合规模是指两个子程序之间 第五章 高质量子程序的特点 57 也越少。一个只有一个参数的子程序与调用它的程序间的耦合程序,要比有 6 个参数的子程序 与调用它的程序间的耦合程度松散得多。一个使用整型参数的子程序与其调用程序之间的耦合 程度,也要比一个使用有 10 个元素数组或者结构化数据的子程序与其调用程序之间的耦合程度 松散得多。同样,使用一个全局变量的子程序与使用十二个全局变量的子程序相比,其耦合程 度也要松散得多。 密切性。密切性指的是两个子程序之间联系的直接程度。联系越直接越好,两个子程序之 间联系最密切的是参数表中的参数。当两个程序直接通讯时,这个参数就像接吻时的嘴唇。联 系密切程度稍低的是作用于同一全局数据的两个子程序。它们之间交流的直接性稍低。全局变 量就像是两个子程序之间的爱情,它可能消失在信中,也可能到你想要它到的地方。联系程度 最低的是作用于同一数据库记录或文件的两个子程序,它们都需要这个数据但却又羞于通知对 方,这个被分享的数据就像是在课堂上传阅着的一张写有“你喜欢我吗?请回答‘是’还是‘不 是’”的纸条。 可见性。可见性是指两个子程序之间联系的显著程度。编程不像是在中央情报局中工作, 不会因为行动隐蔽而受到表彰,它更像是作广告,干得越是大张旗鼓,受到的表彰也就越多。 在参数表中传递数据是明显的,因而也是好的。而通过改动全局数据以便让别的子程序来使用 它,则是一个隐蔽的联系因而也是不好的。对全局数据联系进行注释以使其更明显,可能稍好 些。 灵活性。灵活性是指改变两个子程序之间联系的容易程度。形象地说,你更喜欢电话上的 快速插口装置,而不会喜欢用电烙铁把铜线焊到一起,灵活性可能有一部分是由其它耦合特性 决定的,但也有一些区别。比如,有一个按照给定的被雇用日期和被雇用部门,寻找雇员的第 一个监工的子程序,并命名它为 LookUpFirstSupervisor()。同时,还有一个对变量 EmpRec 进行 结构化的子程序,变量 EmpRec 包括雇用日期、雇用部门等信息,第二个子程序把这个变量传 给第一个子程序。 从其它观点来看,两个子程序之间的耦合是非常松散的。因为处于第一个和第二个子程序 之间的变量 EmpRec 是公共的,所以它们之间只有一个联系。现在,假设需要用第三个子程序 来调用子程序 LookUpFirstSupervisor(),但这个子程序中不含 EmpRec,却含有雇用部门和雇用 日期信息。这时 LookUpFirstSupervisor()就不是那么友好了,它不情愿与第三个子程序进行合作。 对于调用 LookUpFirstSupervisor()的子程序来说,它必须知道 EmpRec 的数据结构。它可以 使用一个仅有两个域的变量 EmpRec,但这又需要知道 LookUpFirstSupervisor()内部结构,即那 些仅供它使用的域,这是一个愚蠢的解决方法。第二个方案是改动 LookUpFirstSupervisor,使 它 自带雇用部门和雇用日期信息,而不是使用 EmpRec。不管采用哪种方案,这个子程序都不像最 初看起来那么灵活了。 如果愿意的话,一个不友好的子程序也是可以变为友好的。这种情况可以通过让它明确带 有雇用部门和日期信息,而不再使用 EmpRec 来达到这一目的。 简而言之,如果一个子程序越容易被其它子程序调用,那么它的耦合程度也就越低。这样 的好处是可以增强灵活性和维护性。在建立系统结构时,应该沿着相互耦合程度的最低线将其 分开。如果把程序看成一块木头的话,就是要沿着它的纹理把它劈开。 量越少越好,因为一个子程序的接口越少,那么在把它与其它子程序相连接时,所要做的工作 第五章 高质量子程序的特点 58 5.4.2 耦合层次 “正常耦合”,这也是一种最好的耦合。 序了解被调子程序的内容与结构。 基础之上的,因此其灵活性也是很差的。许多结构化语言中,都有明确禁 以下是上述各种耦合的例子,其中有好的,也有坏的: 简单数据耦合的例子。比如,一个程序 子程序传递含有角度值的变量,那它们之 间就是简单数据耦合的。 数据耦合的另一个例子。两个程序向另一个子程序传递姓名、住址、电话号码、生日 和身份证号码等五个变量。 接受的数据结构耦合的例子。一个程序向另一个子程序传递变量 EmpRec,EmpRec 是一 个结构化的变量,包括姓名、住址、生日等等五个方面的数据,而被调用的子程序则全部使用 这五个域。 不可取的数据结构耦合举例。一个程序向另一个子程序传递同样的变量 EmpRec,但是,如 果被调用的子程序只使用其中两个域,比如电话号码和生日。这虽然还是数据结构耦合,但却 不是个很好的应用,如果把生日和电话号码作为简单变量来传递的话,将使联系更加灵活, 传统上,把耦合层次称为非直觉性(unintuitive)。所以,在以下叙述中,将交替使用这两 个名字。在以下叙述中,既有好的耦合,也有不好的耦合。 简单数据耦合。如果两个子程序之间传递的数据是非结构化的,并且全部都是通过参数表 进行的,这通常称作 数据结构耦合。如果在两个程序之间传递的数据是结构化的,并且是通过参数表实现传递 的,它们之间就是数据结构耦合的。这种耦合有时也称之为“邮票耦合”(stamp coupling)(不 过我总觉得这种叫法非常奇怪)。如果使用恰当的话,这种耦合也不错,它与简单耦合的主要区 别是它所采用的数据是结构化的。 控制耦合。如果一个子程序通过传入另一个子程序的数据通知它该作什么,那么这两个子 程序就是控制耦合的。控制耦合是令人不快的,因为它往往与逻辑内聚性联在一起,并且,通 常都要求调用程 全局数据耦合。如果两个子程序使用同一个全局数据,那它就是全局数据耦合的。这也就 是通常所说的“公共耦合”或“全局耦合”。如果所使用的数据是只读的,那么这种耦合还是可 以忍受的,但是,总的来说,全局耦合是不受欢迎的,因为这时子程序之间的联系既不密切, 又不可见。这种联系容易被疏漏掉,甚至可以认为它是一种由信息隐含带来的错误——信息丢 失。 不合理耦合(pathological)。如果一个子程序使用了另外一个子程序中代码,或者它改变了 其中的局部变量,那么它们就是不合理耦合的。这种耦合也称之为“内容耦合”。这一类耦合是 不能接受的,因为它不满足关于耦合规模、密切性、可见性和灵活性中的任何一条标准。虽然 这是一个很紧的联系,但是这种联系却是不密切的。改动另一个子程序中的数据无异于在其后 背桶上一刀,而且,这背后一刀从表面上又是看不出来的。由于它是建立在一个子程序完全了 解另一个程序内容的 止不合理耦合的语法规则。但是,在 Basic 或汇编语言中,它却是允许的。因此,在用这种语言 编程时,一定要小心! 以上所有类型的耦合,如图 5-2 所示。 5.4.3 耦合举例 向 Tan() 简单 可 第五章 高质量子程序的特点 59 可以进一步对 OfficeRec 进行结构化,以使得在被调用程序中用得到的 简洁的解决办法。 简单数据耦合或可能数据结构耦合的例子。一个程序调用 EraseFile()子程序,通过一个 字符串确定将要删去的文件,这很可能是一个简单数据耦合。但也可以说这 我们的结论是半斤与八两的关系, 叫法都是同样正确的,对其作严格区分 必要的。 ,一个程序向另一个子程序传递控制标志,通知它到底是打印月报 不可取的全局数据耦合的例子。一个程序改动一个表的入口作为全局变量,这个表是以雇 递给它,而这个被调用的子程序则用雇员识别卡去读全局数据表,这是一个典型的全局数据耦 传递雇员识别卡形成的是简单数据耦合,但是第一个程序对表入口的改动,已经 可取的全局数据耦合的例子。一个程序把雇员识别卡传递给另一个子程序,两个程序都利 使用的整个结构 好的数据耦合 由被调用的子程序 局数据 使用的少数结构 可取的 全局数据耦 可取的 Global Data 一个子程序写全局数 一个子程序使用另一 个子程序的内容数据 一个子程序写全局 不可取的 全局数据耦合 不可取的 全局数据耦合 内容耦合 简单的数据耦合 好的数据耦合 坏的数据耦合 合 全局数据耦合 由被调用的子程序 使用的大多数结构 由被调用的子程序 两个子程序 都读取全局数据 数据另一个读 另一个程序的内部数据 由被调用的子程序 使用的简单变量 两个子程序都读取全 据,另一个读 而且会使它们之间的两个特定域真正联系的可见性更好。 有问题的数据结构耦合的例子。一个程序向另一个子程序传递变量 OfficeRec。OfficeRec 有 27 个域,而被调用的子程序使用其中 16 个,这也是数据结构耦合,但是,它是一个好的数 据结构耦合吗?决不是。传递 OfficeRec 使得联系是大规模的,这个事实非常明显,而传递 16 个单独参数,则又再次非常拙劣地表明了这一点,如果被调用子程序仅使用其中的 6 到 7 个域, 那么单个地传递它们是个好主意。 在现在这种情形下, 图 5-2 某些耦合类型 16 个域包含在一个或两个亚结构中,这将是最 含有待删文件名的 是一个数据结构耦合,因为字符串是一个数据结构。我觉得, 这两种 是没 控制耦合的例子。比如 表、季度报表还是年度报表。 员的识别卡作为索引的。然后,这个程序又调用另一个子程序并把雇员识别卡作为一个参数传 合(虽然仅仅 决定了这是一种最坏的耦合——全局数据耦合)。 用这个识别卡从一个全局表中读取雇员的名字,两个子程序都没有改变全局数据。 第五章 高质量子程序的特点 60 常也称为“全局数据耦合”,但事实上它更像“简单数据耦合” “可取的 数据耦合”。与前述一个子程序改变另一个程序使用数据的例子不同,这两个程序并不是由 局数据联系在一起的。比较两个例子,这种对相同全局数据的只读 这两个从 一个全局表读取数值的程序,与上述 用全局数据来掩盖它们之间联系的程序是 全不同的。 内容耦合的例子。在汇编语言中,一个子程序可以知道另一个子 部变量的 的地址。它可以命名用这个地址直接 ,而地址在两个子程序间并没有当作参数 递。 内容耦合另一个例子 sic 程序利用 GO 来执行另一个子程序中的一段代码。 好的耦合关键是 可 的抽象层 好它 为它是独立的。 降低了整个程序的复杂性,并且使你每次只致力于一件事情。如果在使用子程序时要求同时 虑几件事情——知道它的内部内容、 改动、不确定的功能等,就会使其丧失抽 能力,那么使用子程序还有什么用呢?子程序本来是用于降低复杂性的工具,如果使用它没 使 明没有用好它。 5.5 长度 理论上,常把一个子程序的最佳长度定为一两页,即 66 到 132 行。按照这种原则,在七十 IBM 制在 50 行以下,而 TRW 公司则把这个标准定为 132 行 (McCabe 1976)。几乎没有什么证据证明这一限制是正确的。相反,倒是证明较长子程序更有 利的证据更有说服力,请参照以下几点: Basili 和 Perricone 1984 年的研究表明,子程序的长度与错误数量是成反比的。随着子 程序长度的增加(长至 代码),每一行的错误数量开始下降。 · 另一个由 shen et al 1985 年进行的研究表明,程序长度与错误数是无关的,但错误数 量会随着结构复杂性和数据数量的增加而增加。 · 一项由 Card、Church 和 Agresti 在 1986 年,以及 Card 和 Glass 在 1990 年进行的调查 表明,小型子程序(32 行代码或更少)并不意味着低成本和低错误率,证据表明大型 子程序(65 行或更多)的每行成本要低于小型子程序 · 对 450 个子程序的一项调查发现小型子程序(包括注释行,少于 143 行源语句)的每 行错误率要比大型子程序高 23%(Selby 和 Basili 1991)。 · 对计算机专业高年级学生进行的测验表明,学生们对一个被过度模块化的、由许多有 10 行左右代码子程序组成的软件,与同样内容但不含任何子程序的软件的理解程度 是相同的。但若把整个程序分解成中等规模的子程序(每个有 25 行代码),学生们的 理解程度会上升为 65%。 · 最近研究发现,当子程序长度是 100 到 150 行时,错误率最低(Lind 和 Variavan 1989)。 研究子程序长度有什么好处呢?如果你是一个经理,不要限制程序员们编写长于一页的子 程序,刚才引用的资料和程序员们自己的经验都可以证明你这样作是正确的。如果想编写一个 这通 ,我们也可称它为 全局 全 使用是良性的。 同 那两个通过使 完 程序中说明为局 表 去改动这个表 传 。一个 Ba 以提供一个附加 SUB 语句 次——一旦写 它 ,就可以认 它 考 对全局数据的 象 有 工作更简单,那说 子程序 年代, 公司曾把子程序的长度限 · 200 行的 。 第五章 高质量子程序的特点 61 限。在对 IBM 为 OS/360 操作系统及其它系统而开发代码研究中发现,最易出 修复长于 143 行的子程序中错误成本低 2.4 倍(Sely 和 Basin 1991)。 刻认为其它驾驶员的行为都是不可预测的。这样, 就可以在他们做出某些危险举动时,确保自己不会因此受伤。在防错性编程中,其中心思想是, 即使一个子程序被传入了坏数据,它也不会被伤害,哪怕这个数据是由其它子程序错误而产生 的。更一般地说,其思想核心是 要被改动,一个聪明的程序员就 以这点为依据开发软件。 5.6.l 使用断言 出的假设 设为真时的布尔表达式和 A 这个 r<>0 是一个布尔表达式,其 即 ,使用断言还是非常方便 段,可以 断言 Pr seertionn: boolean; 长度是 100 行,150 行或 200 行的子程序,那就按照你想的去作吧。目前的证据表明,这种长度 的子程序并不更易引发错误,而其开发更为容易。 如果要开发长于 200 行的子程序,就要小心了(这里的长度不包括注释行和空行)。目前还 没有任何证据表明长于 200 行的子程序会带来更低的成本或更少的错误。而这样做却会使你达 到可理解性的上 错的子程序是那些大于 500 行的子程序。在超过 500 行之后,错误数量会与子程序的长度成正 比。而且,对一个有 148,000 行代码软件的研究发现,修改少于 143 行子程序错误所耗费的成 本,要比 5.6 防错性编程 防错性编程并不意味着要对自己的程序提高警惕,这一想法是在防错性驾驶的基础上产 生的,在这种驾驶方法中,必须在心中时 承认程序中都会产生问题,都 作为本书介绍的提高软件质量技术之一,防错性编程是非常有用的。最有效的防错性编码 途径是一开始就不要引入错误。可以采用逐步设计方法、在编码前先写好 PDL、进行低层次设 计、审查等都可以防止错误引入。因此,应优先考虑它们,而不是防错性编程。不过,你可以 把防错性编程与这些技术组合起来使用。 断言是一个在假设不正确时会大声抗议的函数或宏指令。可以使用断言来验证在程序中作 并排除意外情况。一个断言函数往往大致带有两个内容:假 一个为假时要打印出来的信息。以下是一个假定变量 Denominator 不为零时一个 Pascal 断言: ssert ( Denominator<>0,'Denominator is unexpected equal to 0.' ) ; 断言假定 Denominator 不等于”0”,第一部分 Denominato 结果为 True 或 False。第二部分是当第一部分的结果为 False 时,将要打印出来的信息。 使不愿让用户在最终软件中看到断言信息,在开发和维护阶段 的。在开发阶段,断言可以消除相互矛盾的假设,消除传入子程序的不良数值等等。在维护阶 表明改动是否影响到了程序其它部分。 过程是非常容易写的,下面就是一个用 Pascal 写成的例子: ocedure Assert ( A Message : string ); 第五章 高质量子程序的特点 62 一旦写好了这样一个过程,就可以用像第一个例子那样的语句来调用它。 下面是使用断言的一些指导 如果有预处理程序的话,使用预处理程序宏指令。如果在开发阶段使用预处理程序处理断 言,那么在最终代码中把断言去掉是非常容易的。 从来不会输出乱七八糟像垃圾似的东西,不管它被输入的是什么。一个好程 序的特点是“输入垃圾,什么也不产生”,或“输入垃圾,输出错误信息”,也可以是“不允许 float AdjacentLength; rt( AdjancentLength != 0,”AdjanceLength deteced to be 0." ); begin if( not Assertion) begin writeln(Messase); writeln('Stopping the program.'); halt(FATAL ERROR) end end; 方针: 在断言中应避免使用可执行代码,把可执行代码放入断言,在关闭断言时,编译程序有可 能把断言捎去。请看以下断言; Assert(FileOpen(InputFile<>NULL,'Couldn't Open input file'); 这行代码产生的问题是,如果不对断言进行编译,也编译不了打开文件的代码,应把可执 行语句放在自己的位置上,把结果赋给一个状态变量,然后再测试状态。以下是一个安全使用 断言的例子: FileStatus : FileOpen (InputFile); Assert(FileStatus<> NULL,'Couldn't Open input file'); 5.6.2 输入垃圾不一定输出垃圾 一个好的程序 垃圾进入”。从现在的观点来看“输入垃圾,输出垃圾”,往往是劣质程序。 检查所有外部程序输入的数值。当从用户文件中读取数据时,要确保读入的数据值在允许 范围之内。要保证数值是可以取的,并且字符串要足够短,以便处理。要在代码中注释出输入 数据的允许范围。 检查全部子程序输入参数值。检查子程序输入参数值,事实上与检查外部程序数据是一样 的,只不过此时由子程序代替了文件或用户。 以下是一个检查其输入参数的子程序的例子,用 c 语言编写: float tan ( float OppositeLength; ) { /*计算角度正切*/ Asee 第五章 高质量子程序的特点 63 turn (OppsiteLenght/AdjancetLength); 法参数。一旦发现了一个非法参数,该如何处理呢?根据不同情况,可以 中间值、用下一个合法数据来代替它并按计划继续执行、与 法值、调用一个处理错误的子程序、从一个子程 关闭程序。由于有这样多的方案可供选择,所以当在程 理非法参数时,一定要仔细,确定处理非法参数的通用办法,是由结构设计 在结构设计层次上予以说明。 以修复的,例如,在某种情况下使用了一 品阶段,应该利用异常情况处理做一些更完美的工作,比如 就可以从开发阶段进 动 免 旧的软件新版本,就需要对原有 代码作出许多改动,不过,即使是在开发一个软件的第一版时,也不得不由于加入某些没有预 发软件时,应该努力作到使它很容易地进行改动。而且,越 是可能的改动,越是要容易进行,把你在其中预想到的改动域隐含起来,是减少由于改动而对 调试帮助措施包括:断言、内存检查报告、打印语句等及其它一些为方便调试而编写的代 码。如果所开发的软件是供自己使用的,那么把它们保留在程序中并无大碍。但是,如果是一 软件,那么这些措施留在程序中,则会影响其速度和占用空间等性能指标。在这种情况 下,应事先作好计划,避免调试信息混在程序中,下面是几种方法。 版本控制工具可以从同一源文件中开发出不同 本的软件。在开发阶段, 试辅助手段的版本控制工具,这样,到了产品阶段,就可以很容易地去掉 希望出现的这些辅助手段。 中带有预处理程序,如 C 语言,那么仅用一下编译 。可以直接使用预处理程序,也可以通过编写宏指 序定义。下面是一个用 c 语言写成的,直接使用预处理程序的例子: ine DEBUG re } 决定如何处理非 希望返回一个错误代码、返回一个 上次一样返回一个正确答案、使用最接近的合 序中调用错误信息并打印出来或者干脆 序的任一个部分处 决定的,应该 5.6.3 异常情况处理 应该预先设计好异常处理措施来注意意想不到的情况。异常处理措施应该能使意外情况的 出现在开发阶段变得非常明显,而在运行阶段又是可 个 case 语句,其中只预计到了五种情况,在开发阶段,应该能利用异常情况处理产生一个警告, 提示出现了另外一种情况。而在产 向一个错误记录文件中写入信息等。总之,应该设计出不必费多大周折, 入产品阶段的程序。 5.6.4 预计改 改动几乎是每个程序都不可避 的现象。比如,开发一个 计到的功能而对其进行改动。在开 程序带来影响的最有力武器之一。 5.6.5 计划去掉调试帮助 个商用 使用版本控制。 版 可以设置包含全部调 在商用版本中所不 使用内部预处理程序。如果在编程环境 程序开关,就可以加入或去掉这些辅助手段 令来进行预处理程 #def … 第五章 高质量子程序的特点 64 #ifdefined(DEBUG) 代码*/ 门用途的,这时可以用#if DEBUG—一 PRINTER ERROR 这样的语句把这段代码围起来,在其它地方,还可能想设置调试层次,可用 如下语句: ugCode(code fragment){code_fragment} #else bugCode(code fragment) #endif DebugCode ( statement 1; statement 2; … ) 约定,并编写 远观点来看,这样做可以节约许多时 间, 根据 DEBUG 是否定义,包含或不包含这段代码 /*调试 … #endif 这种思想可能有几种表现形式。不仅仅是定义 DEBUG,还可以赋给它一个值,然后再测试 它的值,而不是测试它是否被定义了。用这种方法可以区分调试代码的不同层次。也可能希望 某些调试用代码长期驻存在程序中,这时可以使用诸如#if DEBUG>0 之类的语句,把这段代 码围起来,其它一些调试代码可能是有某些专 #if DEBUEG>LEVEL_A 要是不喜欢程序内部充斥着#if defined()这样的语句的话,可以用一个预处理程序宏指 令来完成同一任务。以下是一个例子: #define DEBUG #if defined(DEBUG) #define Deb #define De statment n; 与使用预处理程序的第一个例子一样,这种技术也有多种形式,可以使它更复杂一些,从 而不是简单地全部包括或全部排除调试代码。 编写自己的预处理程序。如果编程语言中没有预处理程序,可以自己编写一个加入或去掉 调试代码的预处理程序,这项工作是非常容易的。还要建立一个标识调试代码的 自己的预编译程序来遵循这一约定。比如,在 Pascal 中,可以编写一个对如下关键字作出反应 的预编译程序:/#BEGIN DEBUG/和/#END DEBUG/。并写一个批处理文件来调用这个 预处理程序,然后再编译这段已经预处理过的代码。从长 因为你不会误编译没有预处理过的代码。 保留使用调试程序。在许多情况下,可以调用一个调试子程序来进行调试工作。开发阶段 第五章 高质量子程序的特点 65 在控制 程序可能进行几项操作。在最终软件中,可以用一个子程序 序,这个子程序将立即返回控制,或者在进行两个快速操作之后, 返回控制 用这种方法,对性能带来的影响很小,与编写自己的预处理程序来相比,其速度 ,有必要保留这个子程序在开发阶段和最终产品阶段的两个版本,以供在将 . r); 这行调用检查指针的子程序 ,CheckPointer()子程序将对指针作全面检查,这项工作可能很费时,但却非 heckPointer( Pointer:POINTER_TYPE) perform check 1 --maybe check that it's not nil} 2 --maybe check that its dogtag is legitimate} -maybe c { ck n ...} 所有的内务操作都与指针检查联系到一起,这 案,但从提供一些在你的环境下能有效工作 5.6.6 尽早引入调试辅助工具 越早引入调试辅助工具,它们所起的作用也就会越大。一般说来,只有在被某一问题困扰 返回调用程序之前,这个子 来代替那个复杂的调试子程 。使 也要快得多。所以 来的开发和产品调试中使用。 比如,可以用一个检查传入其中指针的子程序作为开始: Procedure DoSomething ( Pointer:PONITER_TYPE; .. ); begin { check parameters passed in } CheckPointer(Pointe ... end. 在开发阶段 常有效,它很可能是以下这个样子的: 这个程序检查传入的每个指针, 它可以在开发期间使用,完成 指针检查 Procdure C ; begin { {perform check {perform check 3 - heck that what is point to isn't corrupted} … perform che end; 当程序进入最终产品阶段时,可能并不希望 寸,可以用下面这个子程序来代替刚才那个子程序: Proccure CheckPointer (Pointer: POlNTER_TYPE ); 本程序仅是即返回其调用程序 begin {no code;just return to caller} end; 以上这些并不是去掉调试辅助工具的所有方 方案这个思路的角度来说,这些已经是足够的了。 第五章 高质量子程序的特点 66 5.6.7 使用“防火墙”包容错误带来的危害 危害策略,在建筑物中,防火墙的作用是防止火灾蔓延,把火 果船撞上了冰山,那么只有被 于是与它分隔开来的,所以不会受到它的影响,这样就避免 而带来的全船进水的灾难性后果。 墙。对另一个子程序的内容知道得越少,对它如何操 少,而假设越少,其中一个假设出错的可能性就会越小。 也是在程序内部修建防火墙的手段之一。两个子程序之间的耦合越松散,那么 错误影响到另外一个子程序的机会也越少。相反,如果两个子程序联系得 会影响另外一个子程序。 区边界。对穿越安全区边界的 如果是非法的数据,就要作出合理的反应。基于这种想法的另一种技术 被允许进入手术室之前,要对其进行消毒处理,手术室中的一切都认为 些要被放在它外面?应该把门放在哪儿?应该把哪些 子程序 最简 消毒,但是,数据往往需要在几个层次上进行消毒,因 5.6.8 一个函数,并且可以忽略函数退回值(例如,在 C 语言中,甚至不需要知道函 这个返回值。要对这个值进行检查。如果不想让它出错的话, 一定要对其进行检查。防错性设计的核心就是防止常错误。 对于系统函数来说,这个原则也是适用的,除非在结构设计中制定了不检查系统调用的返 回 。 件中保留多少防错性编程 发过程中,你希望出现错误时越引人注意越好,惹人讨厌 总比冒险忽视它好得多。但在最终产品中,你却希望它越不显眼越好,程序运行不管成功与否, 都要看起来十分优雅。以下是帮助你决定在最终软件中应该保留哪些防错性编程的一些原则: 保留查找重要错误的代码。首先要确定哪些域可以承受未测试到的错误,而哪些域不能。 ,程序中的屏幕更新区就可以承受未测试到的错误,因为发生 这种情况的后果,不过是一个混乱的屏幕而已,而在计算部分,就不能发生这种情况。因为这 。 去掉那些无关紧要错误的代码。如果一个错误的后果是无关紧要的,那就去掉检查它的代 码。在上例中,你很可能会去掉检查屏幕更新区错误的子程序。“去掉”并不是指从物理上把这 几次之后,你才会舍得花功夫去编写调试辅助工具,但如果你在第一次遇到问题时就这样做, 或者引用一个以前遗留下的调试辅助工具,那么它将在整个项目中都会对你有很大帮助。 “防火墙”技术是一种包容 隔离在一个地方。在轮船中使用分隔式水密舱也是同样道理,如 撞的水密舱才会破损,而其它舱由 了由一个洞 信息隐蔽可以帮助在程序中建立防火 作的假设也就越 松散的耦合 其中一个子程序中的 非常紧密,那么一个子程序错误很可能 在程序中建防火墙的最好办法是把某些接口标识成“安全” 数据进行合法性检查, 是手术室技术,在数据 是无毒安全的。当然,这个手术室是指一段代码,这样,在设计中,要作出的一个关键决定, 就是在这个“手术室”中进入些什么?哪 放在安全区内,哪些放在外面?用什么对数据进行消毒? 单的办法是在外部数据进 此,有时需要进行多重消入时对其进行 毒。 检查函数返回值 如果调用了 数是否返回一个值),千万不要忽略 码,而是在每次调用后检查错误代码。如果发现错误,C 语言中的 Perror(),或其它语言中 等效的部分,能同时也查出错误个数和这个错误的说明 5.6.9 在最终软 防错性编程带来的矛盾是,在开 例如,你正在编写一个表格程序 会在表格中产生难以察觉的错误。绝大多数用户都宁愿忍受一个混乱的屏幕而不是错误的表格 第五章 高质量子程序的特点 67 去掉那些引起程序终止的代码。在开发阶段,程序发现了一个错误时,你会希望这个错误 到这一目的最好办法是让一个程序在发现错误时 果程序中含有会使千百万数据丢失的 留。如果程序中含 。从理论上说,程序不应该 对防错性编程提高警惕。过多的防错性编程会带来它自身的问题,如果你对每一种可以 察觉的参数传递,在每一个可以察觉的地方都进行检查,那么程序将变得臃肿而笨拙。更糟的 程的代码本身并非是完善无缺的,同其它代码一样,你也会在其中发 子程序间的接口往往是一个程序中最容易出错的部分,由 Basili 和 Perricone 进行的一项研 误,即子程序间的通信错误。以下是尽量减少这类 错误的一些准则: 段代码删掉,它指的是版本控制预编译开关,或其它不编译那段特定代码的技术。如果不存在 空间限制问题,你也可以保留这段查错代码,并让它向一个错误记录文件隐蔽地传送信息。 越引人注意越好,以便你能修复它,通常,达 打印出错误信息然后终止。即使对于微小错误来说,这样做也是很有用的。 而在最终软件中,在程序终止前,用户总是希望有机会将其工作存盘。他们往往愿为达到 这一目的而忍受一些反常现象,用户们不会感激那些使其工作付诸东流的东西,不管这些东西 在调试阶段多么有用,哪怕最终极大提高了软件质量。如 调试代码,那么在最终产品中应将其去除掉。 保留那些可以使程序延缓终止的代码。同时,那些相反的代码也应该保 有测试潜在致命错误的信息,那么用户会为能在它们最终发展起来之前将自己的工作存盘而感 到高兴。我所使用的文字处理机在溢出内存之前会亮起“SAVE”提示灯进行警告,当发现这一 情况后就立即存盘并退出。当重新启动程序时,一切又变得正常了 溢出内存,而且,在用同一台机器重新启动程序运行同一文件时,它也不应该用更多内存。产 生了内存溢出问题说明程序有缺欠,但是,程序员想得很周到,在程序中保留了内存检查代码, 我也宁愿得到一个警告信息,而不愿失去我前面所做的工作。 保证留在程序中的错误提示信息是友好的。如果在程序中保留了内部错误提示信息,要确 保它是用友好的语言表达。在我早期编程工作中,我曾经收到一个用户的电话,说她在屏幕上 看到了这样的信息“你的指针地址有错,笨蛋!”幸亏她还有一些幽默感,这对我来说是很幸运 的。通常的办法是通知用户存在“内部错误”,并告诉用户一个她可以投诉的电话号码。 要 是,附加的用于防错性编 现错误,而且,如果你是随意写它的,那么错误也会更多。考虑好需要在哪里预防错误,然后 再使用防错性编程。 5.7 子程序参数 究表明,程序中 39%的错误都是内部接口错 确保实际参数与形式参数匹配。形式参数,即哑参数,是在子程序定义中说明的变量,实 际参数是在调用程序中使用的变量和参数。 常见的错误是在子程序调用时变量类型有误,比如,在需要使用实型数时使用了整型数 (这种情况只在像 C 这种弱类型的语言中,才会遇到。例如,在汇编语言或在 C 语言中未使用 全部编译程序的全部警告时就可能产生这种问题。而在 Pascal 中,当变量是单纯输入的时候, 几乎不会产生这个问题)。通常编译程序在把实参传入子程序之前,会把它转为形参的类型。如 果产生这个问题,编译程序通常会产生警告。但是在某些情况下,特别是当变量既用于输入也 用于输出时,你可能由于传递了错误的变量类型而受到惩罚。 第五章 高质量子程序的特点 68 位这样来排参数。 这种排列约定与 C 语言中把被修改的参数放在首位的规定是冲突的。不过我仍然认为上述 对参数排序,这也是可以 帮助提高程序可读性的。 如果几个子程序今使用了相似的参数,应按照不变的顺序排到这些参数。子程序中参数的 )与函数 puts()相比,也只是前者多了一个文件作为最后变量。这实在是一个 数,函数 strncpy()是按照目标字符率、源字符 要养成检查参数表中参数变量类型和注意编译程序关于变量类型不匹配警告的习惯。在 C 语言中,使用 ANSI 函数的原型,以便编译程序会自动检查变量类型,并在发现类型错误时发 出警告。 按照输入一修改一输出的顺序排列参数。不要随机地或者按照字母表的顺序排列参数,应 该将输入参数放在第一位,既输出又输入的参数第二位,仅供输出的参数第三 这种排列方法显示了子程序中操作进行的顺序——输入数据、修改数据、输出一个结果、下面 是一个 Ada 语言中的参数排列顺序。 procedure InvertMatrix ( OringinalMatrix : in MATRIX; ResultMatrix : out MATRIX; ); procedure ChangeStringCase ( DesiredCase : in STRING_CASE; MixedCaseString: in out USER_STRING ); … procedur PringPageNumber ( PageNumber : in INTEGER; Status: out STATUS_TYPE ); 排列顺序至少对我来说是十分明智的。但如果你一直接某种特定方式 排列顺序可以成为一种助记符,而不停变动的排列,会使得这些参数非常难记。比如,在 C 语 言中,fprintf()函数与 printf()函数相比,除了多了一个文件作为第一变量之外,两者其余都是一 样的。而函数 fputs( 糟糕的区别,因为它使得这些函数的参数的难记程度比实际要高多了。 我们来看一个例子,同样是 C 语言中的函 率和字节的最大数目来排列变量的,而函数 memcpy()是按同样的顺序来排列变量的,这种相 似性使得两个函数中的参数都非常好记了。 使用所有的参数。如果向某个子程序中传入了一个参数,那就要在其中使用;如果不用它 的话,就把它从子程序接口中去掉。因为出错率是随着未用参数个数的增加而升高的,一项调 查表明,在没有未用参数的子程序中,有 46%是完全无错的。而在含有未用参数的子程序中, 第五章 高质量子程序的特点 69 仅 变量过程,那可能会有一些子程序拥有完全相同的参数表,而在这其中又可能有几 把状态和“错误”变量放在最后。根据约定,状态变量和表示程序中有错误的变量应该放 于子程序来说是不很重要的。同时又是仅供输出的变量,因此 最后是非常明智的。 量。把传入子程序中的参数用作工作变量是非常危险 。 在下面这个 Pascal 程序段中,不恰当地使用了 InputVal 存放中间运算结果。 proceoure Sample VAR InputVal : Integer; r; gin InputVal := InputVal * CurrentMultiplier( InputVal); rrentAdder( InputVal ); … 程序段中,对 Inputval 的使用是错误的,因为在程序到达最后一行时,InnutVal 不再 上述原因而带来的问 有 17%到 29%是完全正确的(Card,Church,Agresti 1986) 不过,这个去掉未用参数的规则有两个特例。首先,如果你使用了 c 语言中的指针函数或 Pascal 中的 个子程序没有完全用到这些参数,这是允许的。其次,当你按照某种条件对程序进行部分编译, 可能会使用某些参数编译部分程序。但如果你去掉这部分是正确有效的,那这也是允许的。一 般来说,如果你有充分的理由不使用某一参数的话,那就按照你想的大胆去干吧。但如果理由 不是很充分的话,就要保留这个参数。 在参数表的最后。这两种变量对 把它们放在 不要把子程序中的参教当作工作变 的 应该使用局部变量来代替它。比如, 这个变量来 ( OutputVal: Intege ); be HputVal := InputVal + Cu OutputVal := InputVal; end 在这个 保持它输入时的值。这时它的值是程序中计算结果的值,因此,它的名字被起错了。如果你以 后在更改程序,需要用到 InPutVal 的输入值时,那很可能会在 InputVal 的值已经改变后还错误 地认为它保留原有值。 该如何解决这个问题呢?给 InputVal 重新命名吗?恐怕不行。因为假如你将其命名为 WorkingVal 的话,那么这个名称是无法表示出它的值是来自于程序以外这个事实的。你还可以 给它起一个诸如 InputvalThatBeComesAWorkinsVal 之类荒唐的名字或者干脆称之为 X 或者 Val, 但无论哪一种办法,看起来都不是很好。 一个更好的办法是通过显式使用工作变量来避免将来或现在可能由于 题。下面的这个程序段表明了这项技术: procedure Sample ( VAR InputVal: Inteqer; OutputVal: Inteqer; ); 第五章 高质量子程序的特点 70 var 不允许在函数中改变这个参数的值。不过,不要利用这个理由来解 文学性地命名为 WorkingVal,因为这是一个过于模棱两可的名字,我们之所 一些。 的习惯。如果在调用子程序参数表中的变量 ,那么它在调用程序中的值也将被改变。在任何语言中,把输入值赋给工 强调了它的来源。它避免了从参数表中来的变量被偶然改变的可能性。 变量计算一个新值,那应该在 设。如果假定被传入子程序的数据具有某种特性,那么需要对这个假设 序的地方都需要说明这一假设,这绝不是浪费时间。不要等 再回过头来说明这些假设,因为那时很可能你已经忘记这些假设了。如果能在 (英尺、码、还是米等)。 7 个左右。7 对于人的理解能力来说是一个富于魔 数个数超过 7 个,人们就很难记住,这样会更安全 把一个子程序中的参数个数限制在多少,取决于你所用的程序语言是如何处理 化数据的先进语言,你可以传递一个含有 13 个 息。如果你使用的是一种比较原始落后的语言, 这个复合数据结构分解成 13 个单独参数分别传送。 如果你发现自己总是在传递比较多的变量,则说明程序之间的耦合就有些过于紧密了。这 WorkingVal: Integer; begin WorkingVal := InputVal; WorkingVal := WorkingVal * CurrentMultipier (WorkingVal ); WorkingVal := WorkingVal + CurrentAdder( WorkingVal ); ... 如果需要在这里或其他地方使用输入原始值,它还存在 ... OutputVal := WorkingVal; end; 通过引入新变量 WokingVal,即保留了 InputVal 的作用,还消除了在错误的时间使用 InputVal 中值的可能性。在 Ada 语言中,这项原则是通过编译程序进行强化的。如果你给某个 参数的变量名前缀是 in,则 释把一个变量很具 以这样使用它,仅仅是为了使它在这里的作用清楚 在 Fortran 语言中,使用工作变量是一个非常好 被调用子程序改动了 作变量的同时都 同样的技术也被用于保持全局变量的值。如果你需要为全局 计算的最后把最终值赋给全局变量,而不要把中间值赋给它。 说明参数的接口假 作出说明。在子程序中和在调用程 到写完子程序后 代码中放入断言的话,那么其效果要好于说明这些假设。 · 关于参数接口的哪些假设需要作出说明呢? · 参数是仅供输入的,修改的还是仅供输出的? · 数值参数的单位 · 如果没有使用枚举型参数的话,应指出状态参数和错误变量值的意义。 · 预期的取值范围。 · 永远不该出现的某些特定值。 应该把一个子程序中的参数个数限制在 力的数字。心理学研究表明人类很难一次记住超过 7 个方面的信息,这个发现被应用到不计其 数的领域中,因此,如果一个子程序中的参 一些。 在实践中, 复杂数据结构的。如果你所用的是一种支持结构 域的数据结构,而把它只看成是一个独立的信 那你就不得不把 第五章 高质量子程序的特点 71 计子程序或子程序群,来降低耦合的紧密性。如果你把同一数据传给不同的子程序, 模块,并把那些经常使用的数据当做模块数据。 个关于输入、修改和输出参数的命名约定。如果发现区分输入,修改和输出参数 个关于命名的约定,以便区分它们,比如可以用 i_,m_,o_作前 T 来作前缀。 耦合中讨论过的那样:如果子 用结构化变量中绝大部分的话,那么就只传递它所用得到的那一部分。如果你精确 规定 以降低子程序间的耦合程 高子程序的使用灵活性。 型(ADT)时,这一精确接口规则使不适用了。这种数据类 构化变量,但这时你最好不要过分注意结构内部,在这种情况下,应把抽象 数据 T 子 移植性变坏。参数一般是 5.8 使用函数 回一个值。这意味着函数应接受唯一的输 ,cos(), 于输入、修改、输出参数的个数则没有限制。 来使用,并返回一个状态变量。从逻辑上说,它是一 个例子中,从它输出参数的角度来看,是一个过程。但是从纯技术角度来说,因为程 时应重新设 应当把这些子程序组织成一个 考虑建一 是非常重要的,则你可以建立一 缀。要是你不觉得冗长的话,可以用 INPUT,MODIFY 和 OUTPU 仅传递子程序需要的那部分结构化变量。如同在 5.4 节关于 程序不是使 了接口,在别的地方再调用这个子程序会容易些。精确的接口可 度,从而提 不过,当我们使用抽象数据类 型要求我们跟踪结 类型子程序设计成将整个记录作为一个参数来接收的,这可以使你把这个记录当成 AD 程序之外的一个目标,并把这个记录的抽象水平保持在与 ADT 子程序的相同高度上,如果你通 过利用其中的每一个域来打开结构,那你就丧失了由 ADT 所带来的抽象性。 不要对参数传递作出任何设想。有些程序员总是担心与参数传递有关的内部操作,并绕过 高级语言的参数传递机制,这样做是非常危险的,而且使得程序的可 通过系统堆栈传输的,但这决不是系统传递参数的唯一方式。即使是以堆栈为基础的传递机制, 这些参数的传递顺序也是不同的,而且每一个参数的字长都会有不同程度的改变。如果你直接 与参数打交道,事实上就已经注定了你的程序不可能在另一个机器上运行。 像 C、Pascal 和 Ada 等先进的语言,都同时支持函数和过程,函数是返回一个值的子程序, 而过程则是不返回值的子程序。 5.8.1 什么时侯使用函数,什么时侯使用过程 激进者认为函数应该像数学中的函数一样,只返 入数据并返回一个唯一的值。这种函数总是以它所返回的值来命名的,比如 sin() CustomerID()等等,而过程对 公用编程法是指把一个函数当作过程 个过程,但由于它只返回一个值,因此从名义上说,它又是函数。你可能在语句中使用过如下 一个称为 Fotmatoutput()的过程: if( Formatoutput( Input,Formatting,Output ) = Success ) then ... 在这 序返回一个值,它又是一个函数。这是使用函数的合法方式吗?从保护这个方法的角度出发, 你可以认为这个函数返回一个值与这个子程序的主要目的——格式化输出无关。从这个观点来 看,虽然它名义上是一个函数,但它运行起来更像是过程。如果一贯使用这种技术的话,那么 用返回值来表示这个过程的成功与否并不会使人感到困惑。 一个替换的方案是建立一个用状态变量作为显式参数的子程序,从而产生了如下所示的 第五章 高质量子程序的特点 72 代码段: FormatOutput( Input, Formatting, Output, Status ) if( Status = Success ) then … 成一行 我更赞成使用第二种方法,这倒并不是因为我是个坚持严格区分函数与过程的教条主义者, 而是因为它明确区分了调用和测试状态变量值的子程序。把调用和测试状态值的语句写 增加了语句的代码密度,也增加了其复杂性。以下这种函数用法也是很好的: Status := FormatOutput( Input, Formatting, Output ) if( Status = Success ) then … 5.9 宏子程序 你的要求: 把宏指令表达式括在括号中。由于宏指令及其变量被扩展到了代码中,应保证它们是按照 你想要的方式被扩展的。在下面这个宏指令中包含了一种最常见的错误: #define product(a,b)a*b 这个宏指令的问题是,如果你向其中传了一个非基本数据(无论对 a 还是 b),它都无法正 输出结果并不是你想要的结果;一个好一些但并非完美的同 样功能的宏指令如下: #define product ( a,b) (a)*(b) ry (Key,Index )\ 5.8.2 由函数带来的独特危险 使用函数产生了可能不恰当值的危险,这常常是函数有几条可能的路径,而其中一条路径 又没有返回一个值时产生的。在建立一个函数时,应该在心中执行每一条路径,以确认函数在 所有情况下都可以返回一个值。 特殊情况下,用预处理程序宏调用生成子程序。下面的规则和例子仅限于在 C 中使用预处 理程序的情况。如果你使用的是其它语言或处理程序,应调整这些规则以适应 确地进行乘法运算。如果你使用这个表达式来算(x+1,x+2),它会把它扩展到 x+1*y+2,由于 乘法运算对加法具有优先佳,因此 这一次的情况要稍好些,但还没有完全正确,如果你在 preduct()中使用比乘法具有优先 权的因子。这个乘运算还是要被分割开,为防止这一点,可以把整个表达式放人括号: #define preduct ( a,b) ((a)*(b)) 用斜线将多重语句宏指令包围起来。一个宏指令可能具有多重语句,如果你把它当作多重 语句来对待的话就会产生问题,以下是一个会产生麻烦的宏指令例子: #define LookupEnt Index=(key-10)/5;\ Index=min(Index,MAX_INDEX);\ Index=max( Index,MIN_INDEX); … for ( Entrycount=0; Entrycountnull ) then node = node.text 如果在某种情形下,为了更谨慎一些,你可以使用如下代码: if ( node<>null ) then if ( node.next<>null ) then 第六章 模块化设计 80 node = node.text 如果你的程序中充斥着 node=node.next 这样的语句,你可能要在其中某些地方进行测试而 跳过其余部分。但是,如果这个操作是独立在一个子程序调用中: node = NearestNeighbor(node) 那么,你只要在子程序中一个地方进行测试,那么这一测试就会在整个程序中都起作用。 如果有时你想在整个程序中都对使用 node 的地方进行测试,也很容易漏掉其中某些地方。然而, 如果你把这一操作独立在一个子程序中,那么是不可能有遗漏的,因为此时这项工作完全是自 动进行的。 隐含数据结构细节的另一个好处是容易调试,比如,你发现 node 值在某处变得有问题了, 但却不知道是在哪里。如果存取 node 的代码充斥了整个程序的话,那么找到出问题的地方不亚 于大海捞针。但如果它是被孤立在一个子程序中的话,那么你可以在其中加入一段检查 node 的调试码,从而在每次存取它时都进行测试,这样事情就解决了。 应用存取子程序最后一个优点是,可以使所有对数据的存取所遵循的是一种平行的组织形 式;或者通过存取子程序、或者直接对数据进行存取,不会两者兼而有之。当然,在负责数据 的模块内部,对数据的存取都是直接的,在这种情况下失去平行性是不可避免的,这样做的目 的是不在公共场合吹脏肥皂泡。这常常伴随着对在存取程序中进行直接数据操作这一拙劣设计 的隐含。 6.2.3 常见需要隐含的信息 在你所从事的项目中,你可能与不计其数需要隐含的信息打交道,但是,其中只有几种是 你要反复遇到的: · 容易被改动的区域 · 复杂的数据 · 复杂的逻辑 · 在编程语言层次上的操作 以上每一项都将在下面的部分中给予详细论述。 容易被改动的区域 容易改动是好的程序设计中一项最富于挑战性的工作。目的是将不稳定的区域孤立起来, 以便使改动带来的影响仅限于一个模块中。以下是你在为应付改动的工作中要遵循的步骤。 1. 识别出那些可能被改动的地方。如果分析工作做得很好的话,其中应该附有可能改动 的地方和改动内容的明细表。在这种情况下,找出可能的改动是非常容易的。如果需 求分析中没有进行这项工作,可以参阅下面关于在任何项目中都可能被改动的区域的 讨论。 2. 把可能被改动的地方分离出来。把第一步中发现的每一个可能改动的地方分隔到自己 的模块中,或者将其与其它可能一起被改动的要素一起,独立到一个模块中。 3. 独立可能被改动的地方。应把模块间的接口设计成对可能变动不敏感,同时,接口应 该把变动限制在模块内部,外部不会受到内部变动影响。而其它调用这个被改动过模 块的模块,不应感受到这个模块被修改过。模块的接口应该能保护模块的隐私权。 第六章 模块化设计 81 以下是一些可能变动的区域: 对硬件有依赖的地方。对于监视器、打印机、绘图机等,要清楚在尺寸、颜色、控制代码、 图形能力及内存等方面可能的变化。其余对硬件有依赖性的方面包括与磁盘、磁带、通讯口、 声音器件等接口的变化等等。 输入和输出。在比原始的硬件接口稍高一些的设计层次上,输入/输出是另外一个反复无常 的区域。如果某一应用产生它自己的数据文件,那么当这一应用变得复杂起来时,文件的格式 可能也要变化。用户层次上的输入和输出格式也有可能变化,比如,在打印纸上边界的位置、 每页上边界的数量、域的排列顺序等等。总之,检查所有的外部接口以寻找可能的变化是个好 主意。 非标准语言特性。如果在程序中使用了非标准扩展,应该把这些扩展隐含在一个模块中, 以便当运行环境变化时你可以很容易地替换它。同样,如果你使用了不是在所有环境下都存在 的库子程序,应该把实际的库子程序放在另一个环境下也可以使用的接口后面。 难于设计和实现的域。最好把难于设计和实现的域隐含起来,因为此处的工作可能作得很 糟,你可能不得不返工。把它们分隔起来,以便使由于拙劣设计或实现对系统所带来的危害最 小。 状态变量。状态变量指示程序的状态,往往比其它数据更容易被改动。在典型的情形下, 你可能最初把某一错误状态变量定义成逻辑变量。但后来又发现如果把它赋成具有 NoError, WarningError 和 FatalError 三个值的枚举型变量来实现会更好。 你至少可以在使用状态变量时,加上两个层次的灵活性和可读性。 · 不要使用逻辑型变量作为状态变量,应使用枚举型变量。赋予状态变量一种新状态是 非常常见的,给枚举型变量赋一个新的类型只需要重新编译一次,而对于逻辑型变量 则需要重新编写每行检查状态变量的代码,谁难谁易是很明显的。 · 使用存取子程序检查变量,而不要对其直接检查,通过检查存取子程序而不是状态变 量,可以进行更复杂的状态测试。例如,如果想检查一个错误状态变量和一个当前函 数状态变量,那么把测试隐含在子程序中来进行,要比用充斥着程序的复杂代码进行 测试容易得多。 数据规模限制。如果你说明一个数组中含有 15 个元素,那么你就把系统不需要的信息暴 露给了它。应该保护其隐私权,信息隐蔽并不总是意味着把一系列功能装入模块这类复杂的工 作,有时,它简单到就是用一个像 MAX_EMPLOYEES 之类的常量来代替 15,以便隐含它。 商业规则。商业规则指法律、政策、规定、惯例等编入一个计算机系统中的东西。如果你 在编写一个工资发放系统,你可能把 IRS 关于允许的扣留数和估计税率等规则编入程序。其余 附加的规则是由工会规定的关于加班率、节假日付酬等方面的规定。如果你正在编写一个引用 保险率的软件,其规定来源于州关于信誉、实际保险率等的管理规定。 这些规定往往是数据处理系统中频繁变动的部分。因为国会可能修改法律,保险公司会调 整保险率。如果你遵从信息隐蔽原则,那么当规则变动时,建立在这些规则上的逻辑关系不会 完全垮掉。这些逻辑关系会隐藏在系统中唯一一个阴暗角落里,直到需对其作出改动为止。 预防到改动。当考虑一个系统中潜在的改动时,应该按照使得改动范围或大小与其改动可 能性成反比的原则来设计系统。如果改动很可能发生,要确保系统可以容易地容纳这一特征。 只有极其不可能发生的变动,才应该被允许在变动时,会影响到系统中一个以上的模块。 第六章 模块化设计 82 一个寻找可能发生变动域的技术是,首先分析程序中可能会被用户用到的最小的子单元, 这些子单元组成了程序的核心,而且很可能被改变。其次,规定对系统的最小增值。它们可以 小到看起来完全是琐碎的程度。这些潜在改进域组成了对系统的潜在改进。应使用信息隐蔽原 则对这些域进行设计。首先分析核心,可以发现哪些要素事实上是后加上的,从而从那里推测 并隐含改进。 复杂的数据 所有的复杂数据都很可能被改动;如果它很复杂而对它使用得又很多,那么在实现层次 上与其打过交道后,可能会发现实现它的更好方式。如果应用信息隐蔽来隐含数据实现,就 可以付出较少的努力而获得更好的实现方法。如果不是这样,那么你每次与这些数据打交道 时,你可能都会在后悔,如果当初进行了信息隐蔽,改动实现将会是多么容易啊! 对复杂数据的使用程度,主要取决于程序。如果是一个只有几百行代码的小程序,你想 在其中对变量进行直接操作,那就这样干吧,这样可能影响程序,但也可能不会。在担心由 于对数据直接操作而带来的维护问题之前,应首先考虑这个小程序的特点。如果你正在编写 一个大一些的程序或使用了全局数据,那么就该考虑使用存取子程序。 复杂的逻辑 隐含复杂的逻辑可以改善程序的可读性。复杂的逻辑并不总是程序的最主要方面,把它 隐含起来可以使得子程序的活动更清楚。与复杂数据一样,复杂逻辑也是很可能变动的部分。 所以,把程序的其它部分从这种变动里隔离出去是非常有益的。在某些情况下,你可以将所 使用的逻辑种类隐含起来,例如,你可以通过一个大的 if 语句、case 语句或查表方式来进行 测试。除了这些进行测试的代码外,其余的代码不需要知道这些细节。如果程序中的其余代 码只需要知道结果,那么你就应该仅仅告诉它们结果。 在程序语言层次上的操作 你的程序越是像一个实际问题的解决方案,它就越是不像程序语言结构的组合,那么, 其质量也就越好,应该把过于专业化的信息隐含起来,比如,下面的语句: EmployeeID = EmployeeID+1 CurrentEmployee = EmployeeList [ EmployeeID ] 这是一段很不错的程序,但是它是用过于专业化的语言来表达的,应该用较高程度抽象 的语言来进行这个操作: CurrentEmployee = NextAvailableEmployee() 或者用: CurrentEmployee = NextAvailableEmployee( EmployeeList, EmployeeID ) 通过加入一个隐含了用专业化语言解释正在发生什么的子程序,使得在一个更高的抽象 层次上处理这个问题。这使得你的意图更清楚,而且使得代码更容易理解和改动了。 如果用图表来实现一个排序问题。函数 HighestPriorityEvent(),LowestPriorityrEvent()和 NextEvent()是抽象函数,隐含了实现细节;而 FrontOfQueue(),BackOfQueue()和 NextInQueue() 第六章 模块化设计 83 并没有隐含多少细节,因为它们提到了实现,暴露了它们该隐藏的秘密。 一般来说,在设计一组在程序语言语句层次上操作数据的子程序时,应该把对数据操作 隐含在子程序组中,这样程序的其余部分就可能在比较抽象的层次上处理问题了。 6.2.4 信息隐蔽的障碍 绝大多数信息隐蔽障碍都是心理上的,它主要来自于在使用其它技术时形成的习惯。但 在某些情况下,信息隐蔽也的确是不可能的,而一些看起来像是隐蔽障碍的东西,但仅仅是 借口而已。 信息过度分散 信息隐蔽的一个常见障碍是系统中信息过于分散。比如在一个系统中到处分布着常数 100。把 100 当作一个常数,降低了引用它的集中程度。如果把信息隐蔽在一个地方会更好, 因为这样它的值将只在一个地方改变。 另一个信息过于分散的例子是程序中分布着与用户交互的接口。如果需要改变交互方式, 比如,从命令行方式改为格式驱动方式,那么所有的代码事实上都要被改动。因此,最好把用 户交互接口放入一个单独的模块中,这样,你不必影响到整个系统就可以对交互方式进行改动。 而还有一个例子则是全局数据结构,比如,一个在整个系统中四处被存取的拥有多达 1000 个元素的雇员数据数组。如果程序直接使用这个全局数据,那么这个数据结构的实现信息—— 它是一个数组且拥有最多 1000 个元素——将充斥着整个程序。如果这个程序只通过存取子程 序来使用这个数据结构,那么就只有这个存取子程序才知道这些细节。 交叉依赖 一个不易察觉的信息隐蔽障碍是交叉依赖。比如模块 A 中的某一部分调用了模块 B 中的 一个子程序,而模块 B 中又有一部分调用了模块 A 中的子程序。应避免这种交叉依赖现象。 因为只有在两者都已准备好的情况下,你才能测试其中的一个。当程序被覆盖时,必须使 A 和 B 同时驻留在内存中,才能避免系统失败。通过寻找两个模块中被其它模块使用的部分, 把这些部分放入新的模块 A’和 B’中,用模块 A 和 B 中的其余部分来调用 A’和 B’,基本上可 以消除这一问题。 误把模块数据当成全局数据 如果你是个谨慎的程序员,那么信息隐蔽的障碍之一便是误把模块数据当作全局数据而 避免使用它,因为要避免由于使用全局数据而带来的麻烦。但是,如同在 6.1 节“模块化: 内聚性与耦合性”中所说的那样,这两种数据是不同的。由于只有在模块中的子程序才可以 对其进行存取,因而由模块数据带来的麻烦要比全局数据小得多。 如果不使用模块数据,就不会知道了解由模块所带来的巨大收益。如果一个子程序向模 块传递了只有它才能处理的数据的话,那么就不该由模块来承担拥有数据集合并对其进行操 作的罪责。比如,在前面列举的建议利用如下语句来提高抽象程度的例子中: CurrentEmployee = NextAvaliableEmployee() 或使用: 第六章 模块化设计 84 CurrentEmployee=NextAvailableEmployee(EmployeeList, EmployeeID) 这两个赋值语句间的区别是:在第一种情形下,NextAvailableEmployee()拥有关于雇员表 和目前表中的入口是哪一个入口的信息,而在第二种情况下,NextAvailableEmployee (EmployeeList, EmployeeID)只是从向它传递数据的子程序中借用这些信息。当你使用 NextAvailableEmployee()时,为了提供全部的抽象能力,不必担心它所需要的数据,只要记住 它负责自己的问题就可以了。 全局数据主要会产生两个问题:(1)一个子程序在对其进行操作时并不知道其它子程序也 在对它进行操作;(2)这个子程序知道其它子程序也在对其进行操作,但不知道它们对它干了 什么。而模块数据则不会产生这些问题,因为只有被放在一个单独模块中的有限几个子程序 才被允许对模块数据进行直接存取操作,当一个子程序进行这种操作时,它知道别的子程序 也在进行同样操作,并确切知道这些是哪几个子程序。如果你还不相信的话,试一下,结果 会令你满意的。 误认为会损失性能 信息隐蔽的最后一个障碍是在结构设计和编码两个层次上,都试图避免性能损失。事实 上,在两个层次上你都不必担心这一点。在结构设计层次上,这种担心之所以不必要是因为, 以信息隐蔽为目标进行结构设计,与以性能为目标进行结构设计是不矛盾的,只要你同时考 虑到这两点,那么就可以同时达到这两个目标。更常见的担心是在编码层次上,这种担心主 要是认为间接而不是直接地存取数据结构会带来运行时间上的损失,因为这样做附加了调用 层次。当测试了系统的性能并在瓶颈问题上有所突破时,这种担心是不成熟的。为提高软件 性能做准备的最好手段之一就是模块化设计,这样,在发现了更好的方案之后,不必改变系 统其余部分,就可以对个别子程序进行优化。 6.3 建立模块的理由 即使不经常使用模块,凭直觉也很可能会对可以放入模块的数据和子程序种类有所了解。 从某种意义来说,模块并不是人们的目标,它只是数据及对数据所进行的操作的集合,并且支 持面向对象的概念——抽象和封装。模块不支持继承性,因而它也并不完全支持面向对象编程, 描述它的这种有限的面向对象特性的词汇是 Booch 1991 年提出来的“基于对象”编程。 以下是一些适合使用模块的域: 用户接口。可以建立一个模块来把用户接口要素独立起来。这样,不会影响程序其它部分, 你就可以进行改进。在许多情况下,用户接口模块中往往包含有几个模块来进行诸如菜单操作、 窗口管理、系统帮助等。 对硬件有依赖的区域。把对硬件有依赖的区域放入一个或几个模块中。这些区域常见的有: 与屏幕、打印机、绘图机、磁盘驱动器、鼠标等的接口。把这些对硬件有依赖的区域独立起来 可能帮助把程序移植到新环境下运行。设计一个硬件经常变动的程序时,这也是很有帮助的, 可以编写软件表模拟与特定硬件的交互作用,硬件不存在或不稳定时,让接口子程序与这些模 拟软件打交道。然后在硬件稳定时,再让接口子程序与硬件打交道。 输入与输出。把输入/输出封装起来,可以使程序其余部分免受经常变动的文件和报告 格式的影响。把输入/输出放入模块,也使得程序很容易适应输入/输出设备的变动。 操作系统依赖部分。把对操作系统有依赖的部分放入模块的原因与把对硬件有依赖部分放 入模块的原因是相同的。如果你正在编写一个在 Microsoft Windows 下运行的软件,为什么要把 它局限于 Windows 环境下呢?你完全可以把对 Windows 的调用放在一个 Windows 接口模块中。 如果以后想把程序移植到 Macintosh 或者 OS/2 环境下,你所要做的只是改动一下接口模块而已。 第六章 模块化设计 85 数据管理。应把数据管理部分放入模块中,让其中的子程序去与那些杂乱的实现细节打交 道。而让模块外的子程序用抽象的方式与数据打交道,这种方式应该尽可能避免实际处理问题, 如果你认为将数据管理模块化是将其放入一个单独模块中,那你就错了。通常,每一种主要的 抽象数据类型,都需要一个单独的模块来管理。 真实目标与抽象数据类型。在程序中,需要为每个真实目标创立一个模块。把这一目标所 需要的数据放入模块中,然后再在其中建立对目标进行模块化的子程序。这就是所谓抽象数据 类型。 可再使用的代码。应把计划在其它程序中再用的程序部分进行模块化。建立模块的一个优 点是,重新使用模块要比重新使用面向功能的程序实用得多。在面向对象设计和面向功能设计 方法中,刚开始的项目都不能从以前的项目中借用许多代码,因为以前项目还不够多,无法提 供足够的代码基础。使用面向功能设计方法开发的程序,大约可以从以前的项目中借用 35%的 代码:而在使用面向对象设计方法开发的项目中,则大约可以从以前的项目中借用 70%的代码。 如果可以通过深思远虑而在以后的项目中避免重写 70%的代码,那为什么不这样做呢? 可能发生变动的相互联系的操作。应该在那些可能发生变动的操作周围修建一道隔墙。这 事实上是容错原则的一种,因为这样可以避免局部的变动影响到程序的其余部分。在 6.2 节中, 给出了一些经常发生变动的区域。 互相联系的操作。最后,应把互相联系的操作放到一起。在绝大多数情况下,都可以发现 把看起来互相联系的子程序和数据放在一起的更强的组织原则。在无法隐蔽信息的情况下,比 如共享数据或计划增强灵活性时,仍然可以把成组操作放在一起,比如,三角函数、统计函数、 字符串操作子程序、图像子程序等。通过精心地成组放置相关操作,还可以在下一个项目中重 新使用它。 6.4 任何语言中实现模块 有些语言直接支持模块化,但有些语言则需要补充一些编程标准才可以。 6.4.1 模块化所需的语言支持 模块包括数据、数据类型、数据操作以及公共和局部操作的区分等。为了支持模块化,一 种语言必须支持多种模块。如果没有多模块,其它任何要求都是空谈。 数据需要在三个层次上可以被存取和隐含,在局部,在模块中及在全局中,绝大多数语言 都支持局部数据和全局数据。如果想要使某些数据仅对模块中的子程序才是可以存取的,那么 就要求语言支持模块数据,即只有某些而不是全部子程序都可以存取的数据。 第六章 模块化设计 86 对于数据类型的可存取性和可隐含性的要求,与对数据的要求是类似的。某些类型应该隐 含在某一特定模块中,而另一些类型应该是对其它模块开放的。模块需要能够对那些可以知道 其它类型的模块进行控制。 对模块层次上的子程序的要求也与上述相类似。有些子程序应该只有在模块内部才能调 用,而且模块应该对某一子程序是专用的还是公用的可以进行控制。在模块之外,不应该有其 它模块或子程序知道这个模块中存在专用子程序。如果模块设计得很好,那么其它模块或子程 序不应该有任何理由来关心专用子程序的存在。 6.4.2 语言支持慨述 在下表中,对几种语言支持信息隐蔽的必要结构进行了总结: 通用 Basic 和通用 Pascal 不支持多模块,所以被排在支持模块化的前列,Fortran 和 QuickBasic 不能控制支持模块化的数据类型。只有 Ada、C++和 C 以及 Turbo Pascal 才允许模块 限制对某一子程序的调用,从而使这个子程序真正是专用的。 数据 数据类型 源程序 语言 多模块 局部 模块 全局 局部 模块 全局 专用 模块/全局 Ada · · · · · · · · · C · · · · · + · · · C++ · · · · · · · · · Fortran77 + · + · - - - - · 通用 Basic - - - · - - - - · 通用 Pascal · · · · · · · · · Turbo Pascal - · - · · - · - · QuickBasic · · · - · - - - · 简而言之,除非使用 Ada、C、C++或 Turbo Pascal,否则,就不得不通过命名或其它约 定来扩充所使用语言的能力,以便模拟使用模块。以下部分简单论述了直接支持模块化的语 言,并且将告诉你在其它语言中怎样模拟使用模块。 Ada 与 Modula-2 支持 Ada 通过“包”的概念来支持模块化。如果用 Ada 编过程序,那么就已经知道如何建立包 了。Modula-2 通过模块的概念来支持模块化。虽然 Modula-2 并不是本书的特性,它对模块化 的支持仍然是非常直接的,以下给出了一个用 Modula-2 进行排序的例子: definition module Events; export EVENT, EventAvailable, 这些是公共的 HighestPriorityEvent, LowestPriorityEvent; 第六章 模块化设计 87 type EVENT = integer; var EventAvailable:boolean; { true if an event is available } function HighestPriorityEvent:Event; function LowestPriorityEvent:Event; end Events; 面向对象的语言支持 面向对象的语言,如 C++,对模块化的支持是直接的,模块化是面向对象编程的核心。以 下是一个 C++来实现排序的模块的例子: class Buffer { public; typedef int EVENT; BOOL Eventavailable; /*true if an event is available */ EVENT HighestPriorityEvent(void); EVENT LowestPriorityEvent(void); Private; ... }; Pascal 的支持 某些版本的 Pascal,即 4.0 版和随后的 Turbo Pascal,利用单元的概念来支持模块化。“单 元”是一个可以包括数据、数据类型和子程序的数据文件。单元中有一个说明了可供模块外部 使用的子程序和数据接口。数据也可以被说明为在这个文件内部的函数和过程使用,而且是仅 在其内部使用。这为 Turbo Pascal 提供了可以在局部、模块和全局层次上的数据可存取性。以 下是在 Turbo Pascal 5.0 版中的排序模块形式: unit Events; INTERFACE type EVENT=integer; var EventAvailable:boolean;{ true if an event is available } 第六章 模块化设计 88 function HighestPriorityEvent: Event; function LowestPriorityEvnet:Event; IMPLEMENTATION ... ——文件中这部分的子程序数据,如果没有在上面 INTERFACE 中说明 的话,则对其它文件来说是隐蔽的 end.{unit Events} 属于 Generic Pascal 标准的 Pascal 实现并不直接支持模块化,不过你可以通过扩展它们来达 到模块化,这将在后面讨论。 C 的支持 虽然用 C 语言编程的程序员们并不习惯在 C 中使用模块,但事实上 C 也直接支持模块化。 每一个 C 源文件都可以同时含有数据和函数,可以把这些数据和函数说明为 Static,这将使它 们只在源文件内部才能使用。也可以不把它们说明为 Static,此时它们在源文件外也可以使用。 当每一个源文件都被当作模块时,C 就完全支持模块化了。 由于源文件和模块并不完全相同,你需要为每一个源文件创建两个头文件——一个作为公 用、模块头文件,另一个作为专用的、源文件头文件。在源文件的公用头文件中,只放入公用 数据和函数说明,下面是一个例子: /* File:Event.h 本文件仅包含公共的可用类型、数据和函数说明 Contains public declarations for the "Event" module. */ typedef int EVENT; extern BOOL EventAvailable; /* true if an event is available */ EVENT HighestPriorityEvent(void); EVENT LowestPriorityEvent(void); 而在源文件专用头文件,只放入供内部使用的数据和函数说明。用#include 命令把头文件 只放入组成模块的源文件中,不要允许其它文件含有它。以下是一个例子: /* File:_Event.h Contains private declarations for the "Event" module. */ /* private declarations */ ——这里是专用类型、数据和函数 ... C 源文件中,要使用#include 来把两个头文件都包含在其中。在其它使用了 Event 模块中 的公用子程序模块中,用#include 命令来只把公用的模块标题包含进去。 如果在单独一个模块中,需要使用一个以上源文件,可能要为每一个源文件都加一个专用 头文件;但对组成模块的子程序组,应该只加一个公共的模块头文件。对于模块中的源
还剩99页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

qaxx87029675

贡献于2013-11-25

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf