瑞星卡卡安全论坛

首页 » 技术交流区 » 系统软件 » 【转载】软件开发人员必备工具书 【代码大全】
TinKingTZW - 2006-8-21 17:36:00
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%的时间中不包括进行详
细设计的时间,因为它是创建活动的一部分。
TinKingTZW - 2006-8-21 17:36:00
如果你正从事一个正式项目,而要求又是不稳定的,那么,你将不得不与需求分析员一道
解决要求定义问题,拿出你的一部分时间与需求分析员讨论,并给需求分析员一定时间以便让
他重新征求用户意见,可以使要求定义更适合项目需要。
如果你从事的是一个非正式项目,而要求是不稳定的,应该给需求分析留出足够的时间,
以免反复无常的要求定义影响你的创建工作。
如果要求对于任何项目——不管是正式还是非正式的,都是不稳定的,那你就该亲自从事
需求分析工作。当完成需求分析后,再估计从事项目其余部分所需要的时间。这是一个很明智
的办法,因为在你知道自己将作些什么之前,你是不可能知道需要多长时间来完成它的。打个
比方,假设你是一个建筑承包商,你的顾客问:“这项工程要花多少钱?”你则问他要干些什么,
而他却接着说:“我不能告诉你,我只想知道工程要花多少钱?”这时你最好对他说声谢谢,然
后吹着口哨回家吧。
在建筑中,在知道要建什么之前,就进行工程预算显然是荒谬的。在设计师完成草图之前,
老板是不会问要用多少水泥、钉子和木材的。但人们对于软件开发的理解往往不是如此清楚的,
所以你的老板可能一时还弄不明白为什么要把需求分析当作一个单独的项目,这时你就需要作
出解释。
3.8  改变先决条件以适应你的项目
先决条件随项目规模和正式性不同而变化。本章指出了大规模和小型项目之间先决条件的
判别,可以根据项目的特点对先决条件作出合适的调整。要想详细了解大项目与小项目之间的
不同,请参看第21 章“程序规模是如何影响创建活动的”。
3.9  小 结
· 如果想开发一个高质量的软件,必须自始至终重视质量问题。在开始阶段强调质量往
往比在最后强调质量更为有效。
· 程序员的份内工作之一便是向老板和同事宣传软件的开发过程,包括在编程开始前从
事先决条件准备工作的重要性。
· 如果问题定义工作做得不好,那么在创建阶段,所解决的问题可能并不是用户真正要
解决的问题。
· 如果需求分析工作做得不好,很可能因此而漏掉要解决问题中的重要细节。在创建工
作后更改要求,要比在需求分析阶段进行更改的成本高20 到100倍。所以,在开始编
程前一定要确认要求定义工作一切正常。
· 在编程前规定好约定,在创建工作结束后再改变代码来满足约定几乎是不可能的。
· 在创建活动开始之前如果无法完成准备工作,可以尝试在不太稳固的基础上进行创建
活动。
TinKingTZW - 2006-8-21 17:36:00
第四章建立子程序的步骤
目录
    4.1  建立程序步骤概述
    4.2  程序设计语言(PDL)
    4.3  设计子程序
    4.4  子程序编码
    4.5  检查子程序
4.6  小结
相关章节
    高质量程序的特点:见第5 章
    高层次设计;见第7 章
    注释方式:见第19 章
    创建工作先决条件:见第3 章

本章详细讲述了在建立一个子程序时的典型步骤。虽然从广义上讲,你可以把本书所有的
描述都当作是在讲如何建立程序,但本章把这些步骤放在同一背景下讲述。本章的中心内容是
如何编写小规模的程序,以及编写对各种规模项目都十分关键的程序的特定步骤。本章也描述
了从程序设计语言(PDL)到编码的转换过程,几乎没有哪些程序员充分利用了这一过程所带来
的方便,这部分论述会给大家以启迪。
4.1  建立程序步骤概述
    在建立程序过程中引入的许多低层次细节问题,并不需要按某一特点顺序来进行,但是一
些主要活动——设计程序、检查程序、子程序编码、检查代码,则应该按图41 的顺序来进行。
4.2  程序设计语言( PDL L)
    PDL(程序设计语言)是由Came,Father 和Gordon 共同开发的,在1975 年发表之后.曾
作过重大修改。因为PDL 是在模仿英语,所以认为任何像是英语的PDL,都可以正确表达思想
是很自然的。但是,事实上PDL 之间的好坏是有判别的。下面是有效使用PDL 的一些方针:
·  用模拟英语的语句来精确描述每一个特定操作。
·  避免使用最终程序语言的语句。PDL 使你比在代码稍高级的层次上进行设计工作。
    当使用程序语言进行创建时,就又回到了低层次上,从而得不到由于在高层次上
    进行设计的好处,而且会受到不必要的程序语言语法规则的限制。
·  在设计意向这一层次上写PDL。说明方法的意义,而不是描述如何用目标语言实现。
TinKingTZW - 2006-8-21 17:36:00

图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 的典型,是为它使用了像*hRSrcPtr这种特定的 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
TinKingTZW - 2006-8-21 17:36:00
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()的输入变元是非法代码,输出是与这个非法代码相对应的错误信息,
它负责处理非法代码。如果程序运算方式是交互式,那么这个错误信息就打印给用户。如果运
行方式是批处理式的,那么这个信息就送入一个信息文件。在输出信息后,这个子程序应该能
返回到一种状态,指出程序是否成功。
TinKingTZW - 2006-8-21 17:37:00
在本章的其余部分,用这个子程序作为一个实际例子。这一部分的其余内容将论述如何设
计这个子程序,设计这个子程序所需要进行的活动见图4-2。
    检查先决条件。在进行与子程序有关的任何工作之前,首先检查是否定义了这个子程序的
工作任务,这项任务是否和整个结构设计融为一体?通过检查确定是否这个子程序被调用了?
至少,在项目的要求定义中就涉及到它。
    定义这个子程序将要解决的问题。应该足够详尽地规定它需要解决的问题,以便于创建。
如果结构设计是非常详尽的,那么这项工作可能已经完成了,结构设计应该至少指出以下这些
问题:
·  这个子程序将要隐含的信息。
·  这个子程序的输入。
·  这个子程序的输出,包括受到影响的全局变量。
·  这个子程序将如何处理错误?
 
图4-2  设计程序中的所有实现步骤     
    下面是在RecordErrorMessage()这个子程序中,上述考虑是如何得以阐明的。这个子程序隐
含了如下两个事实;错误信息与现存的处理方式(交互式或者批处理),子程序的输入是非法代
码,要求两种输出方式:第一是错误信息;第二是RecordErrorMessass()子程序返回到调用它的
程序时的状态。
    问题说明之后,并没有直接给出解决方案。假设以这个例子来说,程序约定是在发现错误
时立即报告。在这种情况下,这个子程序必须报告它所发现的每一个错误,假定其它错误都已
经报告过了。根据要求,这时子程序应把状态变量设置为失败。
    给子程序命名。给子程序命名似乎是小事一桩,但好的子程序名字往往是一个高质量软件
的标志之一,而且,命名并不是件容易的事情。一般来说,子程序应该有一清楚的、不容易引
起异义的名字。如果在给程序找一个好名字时感到困难,这往往意味着对程序的功能还不十分
清楚。一个模棱两可的名字就像是一个在进行竞选辩论的政治家,似乎他在说着什么,可是当
TinKingTZW - 2006-8-21 17:38:00
你仔细听时,又分辨不出他的话到底有什么意义、应尽量将名字起得清楚。如果产生一个模棱
两可名字的原因是模棱两可的结构设计,那么就应注意这个危险信号,这时应追回去改进结构
设计。
    在这个例子中,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:
TinKingTZW - 2006-8-21 17:40:00
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 并对其作出进一步说明,直到你看
到这样作是在浪费时间时,再开始实际的编码工作。
TinKingTZW - 2006-8-21 17:40:00
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. }
    set the default staus
look up the message based on the error code
已转化成Pascal 风格
注释的标题注释
这些是接口语句
TinKingTZW - 2006-8-21 17:40:00
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转换成程序语言代码是一件机械、自然、容易的工作。如果你不
TinKingTZW - 2006-8-21 17:40:00
觉得是这样,那么还需要进一步细化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);
{ 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

这里是填充的代码
这里是新变量ErrorMessage
TinKingTZW - 2006-8-21 17:40:00
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}
这里是变量声明
TinKingTZW - 2006-8-21 17:41:00
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 节“怎样使用子程序参数”。
·  检查通用设计质量。确认子程序只完成一项任务而且完成得很好,与其它于程序交
叉是控制不严的表现。并且,应该采用了预防错误的设计。关于细节问题,见第五
章“高质量程序的特点”。
·  检查子程序的数据。查找出不精确的变量名、没有使用的数据、没有说明的数据等
TinKingTZW - 2006-8-21 17:44:00
等。要了解详情,见关于数据使用的第八到第十二章。
·  检查子程序的控制结构。查找无限循环、不适当的嵌套等错误。详见关于使用控制结
构的第13 到17 章。
·  检查子程序设计。确认已说明了子程序的表达式、参数表和逻辑结构。详见第18章 “设
计与风格”。
·  检查子程序的文档。确认被翻译成注释的PDL仍然是精确的。检查算法描述,查找接
口假设和非显式依赖的文件资料,查找不清楚的编码等等。详见第19 章“自我证明的
代码”。
按需要重复步骤。如果程序的质量很差,请返回PDL阶段。高质量程序是一个逐步的过程,
所以在重新进行设计和实现活动时,不要犹豫不决。
4.5  检查子程序
在设计并实现了子程序之后,创建活动的第三个主要步骤是进行检查,以确认所实现的软
件是正确的。你或许会问,对代码进行的非正式检查和收尾工作难道不足以保证其正确性吗?
的确,这两项工作会从某种程度上保证代码正确性,但不能完全保证,在这一阶段工作中漏掉
的错误,只有在后面的测试工作中才会被发现。而到那时纠正它们的成本将变得很高,因此,
最好现在就进行查错工作。
在心里对子程序进行查错处理。在前面提到过的非正式检查和清扫工作就是两种内心检查
方法。另一方法是在心中执行每一个路径。在心中执行一个子程序是比较困难的,这个困难也
是造成很困难保持子程序小规模的原因之一。要保证检查到每一个规定路径和中止点,同时还
要检查所有的例外情况。可以自己进行这项工作,此时叫作“桌面检查”。也可以与同事们一道
检查,这叫作“同事评审”、“过一遍”或者“视察”,这要取决于如何进行这项工作。
业余爱好者与职业程序员之间的最大区别就是迷信还是理解。在这里,“迷信”这个词指并
不是指在月圆之夜产生各种错误或使你毛骨悚然的一段程序。它指的是你对代码的感觉代替对
代码的理解。如果你总是认为编译程序或者硬件系统有故障,那说明你还处在迷信阶段。只有 5%
的错误是由编译程序、硬件或者是操作系统引起的(Brown and sampson, 1973, Ostrand and
Weyuher, 1984)。进入理解境界的程序员总是怀疑自己的工作,因为他们知道 95% 的错误出自
这里。要理解每一行编码的意义,并且要明白为什么需要它。没有仅仅因为有效便是正确的东
西。如果你不知道为什么它是有效的,那么往往它是无效的,只不过你没有发现罢了。
最后,要指出的是有了一个有效的子程序并非就完事大吉了。如果你不知道为什么它是有
效的,那就研究并讨论它,或者用替代方案重新实现一次,直到你弄明白为止。
编译子程序。如果检查完了子程序,那就开始编译它。直到现在才开始编译工作,似乎是
我们的工作效率太低了,因为早在几页之前程序就完成了。的确,如果早一些开始编译,让计
算机去检查没有声明的变量,命名冲突等是可能会节约一些时间。
但是,如果晚一些开始编译,将会获得许多收益。其中的一个主要原因是,一旦开始编译,
那么你脑袋里的秒表便开始嘀嗒作响了,在第一次编译之后,你就开始不停地想:下次编译一
定让它全对。结果,在这种“就只再编译一次”的压力下,作了许多匆忙的、更易产生错误的
修改,反而浪费了更多的时间。所以,在确信子程序是正确的之前,不要急于开始编译。
TinKingTZW - 2006-8-21 17:45:00
本书的重点之一,就是想告诉读者如何避免陷入把各种代码拼凑到一起,通过试运行检验
它是否有效的怪圈。而在确信程序是正确的之前,就匆忙开始编译,恰恰是陷入了这种怪圈。
如果你还没有进入这个怪圈,那最好还是当确信程序正确之后再开始编译。
以下是在编译时,尽可能地检查出全部错误的指导方针:
·  尽可能把编译程序的警告级别调到最高。只要允许,编译程序应尽量测试,将发现许
多难以察觉的错误。
·  消除所有编译程序指出的错误和提出警告的原因。注意编译程序关于你的代码说了些
什么。大量的警告往往意味着代码质量不高,所以应该尽量理解所得到的每个警 告。
在实际中,反复出现的警告可能产生以下影响:你忽略掉它们,而事实上它们掩盖了
更严重的错误。或者它们会变得使人痛苦,就像日本式的灌水酷刑。因此,比较安全
或痛苦较小的办法,是消灭这些隐蔽的问题以消除警告。
使用计算机来检查子程序错误。子程序编译之后,将其放入调试程序,逐步运行每一行代
码,要保证每一行都是按预期的运行。用这种简单的办法,你可以发现许多错误。
在调试程序中逐步运行程序之后,用在开发子程序时设计的测试用例对其进行测试。或许 
你将不得不搭些支架来支撑你的子程序——即那些仅在测试阶段用于支持子程序而最终不包括
在产品中的代码。这些代码可能是调用子程序的程序,也可能是被子程序所调用的子程序。
消除子程序中的错误。一旦发现有错误,就要消除它。如果开发的子程序此时问题较多, 那
它将在这一阶段耗费较长的时间。如果发现程序中的错误异乎寻常的多,那就重新开发一个,
不要试图修补它。修补往往意味着不充分的理解,而且肯定会在现在和将来产生更多的错误,
而进行一个全新的设计将防止这一点。恐怕没有比重新写一个完美无缺的子程序来代替一个漏
洞百出的子程序更能让人满意的事了。
4.5.l 检查表
创建子程序
·  是否检查过先决条件已经满足了?
·  定义子程序将要解决的问题了吗?
·  结构设计是否足够清楚,使得你可以给子程序起个好名字?
·  考虑过如何测试子程序了吗?
·  是否从模块化水平或者满足时间和内存要求角度考虑过效率问题?
·  是否查阅过参考书;以寻找有帮助的算法?
·  是否用详尽的PDL设计子程序?
·  在必要时,是否在逻辑设计步骤前考虑了数据?
·  是否检查过PDL,它很容易理解吗?
·  是否注意到了足以使你返回到结构设计阶段的警告(使用了全局数据,更适合其它子
程序的操作,等等)。
·  是否使用了PDL到代码流程,是否把PDL 作为编码基础并把原有的PDL 转为注释?
·  是否精确地把PDL翻译成了代码?
·  在作出假设时,验证它们了吗?
·  是从几个设计方案中选择了最好的,还是随意选择了一个方案?
TinKingTZW - 2006-8-21 17:45:00
·  是否彻底理解你的代码?它容易理解吗?
4.6  小    结
·  要想写好PDL,首先要用易懂的自然语言,避免拘泥于某种程序语言,其次要在意向
层次上写PDL,描述设计作什么而不是如何作。
·  PDL到代码流程方法是详细设计的有力工具,而且使得编码非常容易。可以把PDL直
接翻译成注释,但要注意保证注释是精确而有用的。
·  应该在工作的每一步中都检查子程序,并鼓励同事们检查。这样,可以在投入的资金
和工作努力最少时便发现错误,从而极大降低改错成本。
TinKingTZW - 2006-8-21 17:45:00
第五章    高质量子程序特点
目录
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);
TinKingTZW - 2006-8-21 17:45:00
begin
for i := 1 to 100 do begin
InputRec.revenue[1]:= 0;
InputRec.expense:=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:= Revenue-Expense.Type
end
else If ExpneseType= 2 then begin
Peofit:=Revenue - Expense.Type2
end
else if ExpenseType= 3 then
begin
Profit:=Revenue - Expense.Type3
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
节“常数”
TinKingTZW - 2006-8-21 17:46:00
· 在程序中仅使用了域的CORP_DATA 型参数的两个域。如果仅仅使用两个域,那就该
仅仅传入特定的域而不是整个结构化变量。
· 子程序中的一些参数没有使用过。ScreenX 和ScreenY 在程序中没有涉及。
· 程序中的一个参数被错误标定了。PrevColor被标定为变量型参数,然而在程序中又没
有对其赋值。
· 程序中的参数太多。程序中参数个数的合理上限应该是七个左右。而这个程序中则多
达11个。程序中的参数多得怕人,恐怕没谁会仔细检查它们,而且连数一下都不愿意。
除了计算机本身之外,子程序可以说是计算机科学最重大的发明。子程序使得非常好读而
且也非常容易理解,编程语言中的任何特性都不能和这一点相比。像上例中那样使用子程序,
简直就是对子程序的践踏,甚至可以说是一种犯罪。
子程序也是节省空间和提高性能的最好手段。想象一下,如果用代码段去代替程序中对子
程序的每一次调用,那么程序将会有多么庞大。如果不是把多次重复使用的代码段存放在子程
序中,而是直接把它放在程序中,那么对其进行性能改进将是一件很困难的事。是子程序使现
代编程成为可能。
现在,你可能有些不耐烦。“是好,子程序的确很了不起,我一直都在使用它”。你说,“你
的讨论似乎像是在纠正什么,你到底想让我做什么呢?”
我想说的是:有许多合理的原因使得我们去生成子程序。但是生成方法有好有坏。作为一
个计算机专业的本科生,可以认为生成子程序的主要原因是避免代码段的重复。我所用的入门
课本告诉我说,之所以使用于程序,是因为它可以避免代码段的重复,从而使得一个程序的开
发、调试、注释和维护工作都变得非常容易。除了一些关于如何使用参数和局部变量的语法细
节之外,这就是那本课本关于子程序理论与实践内容的全部。这实在不是一个完全而合理的解
释。下面这节将详细描述为什么和怎样生成子程序。
5.1  生成子程序的原因
以下是关于为什么要生成于程序的一些合理原因,其中有些原因之间可能有互相重叠的地
方。
降低复杂性。使用子程序的最首要原因是为了降低程序的复杂性,可以使用子程序来隐含
信息,从而使你不必再考虑这些信息。当然,在编写子程序时,你还需要考虑这些信息。但是,
一旦写好子程序,就可能不必再考虑它的内部工作细节,只要调用它就可以了。创建子程序的
另外一个原因是尽量减小代码段的篇幅,改进可维护性和正确性。这也是一个不错的解释,但
若没有子程序的抽象功能,将不可能对复杂程序进行明智的管理。
一个子程序需要从另一个子程序中脱离出来的原因之一是,过多重数的内部循环和条件判
断。这时,可以把这部分循环和判断从子程序中脱离出来,使其成为一个独立的子程序,以降
低原有子程序的复杂性。
避免代码段重复。无可置疑,生成子程序最普遍的原因是为了避免代码段重复。事实上,
如果在两个不同子程序中的代码很相似,这往往意味着分解工作有误。这时,应该把两个子程
序中重复的代码都取出来,把公共代码放入一个新的通用子程序中,然后再让这两个子程序调
TinKingTZW - 2006-8-21 17:48:00
用新的通用子程序。通过使公共代码只出现一次,可以节约许多空间。这时改动也很方便,因
为只要在一个地方改动代码就可以了。这时代码也更可靠了,因为只需在一个地方检查代码。
而且,这也使得改动更加可靠,因为,不必进行不断地、非常类似地改动,而这种改动往往又
是认为自己编写了相同的代码这一错误假设下进行的。
限制了改动带来的影响。由于在独立区域进行改动,因此,由此带来的影响也只限于一个
或最多几个区域中。要把最可能改动的区域设计成最容易改动的区域。最可能被改动的区域包
括:硬件依赖部分、输入输出部分、复杂的数据结构和商务规则。
隐含顺序。把处理事件的非特定顺序隐含起来是一个很好的想法。比如,如果程序通常先
从用户那里读取数据,然后再从一个文件中读取辅助数据,那么,无论是读取用户数据的子程
序,还是读取文件中数据的子程序,都不应该对另一个子程序是否读取数据有所依赖。如果利
用两行代码来读取堆栈顶的数据,并减少一个Stacktop变量,应把它们放入一个PopStack()子程
序中,在设计系统时,使哪一个都可以首先执行,然后编写一个子程序,隐含哪一个首先执行
的信息。
改进性能。通过使用子程序,可以只在一个地方,而不是同时几个地方优化代码段。把相
同代码段放在子程序中,可以通过优化这一个子程序而使得其余调用这个子程序的子程序全部
受益。把代码段放入子程序也使得用更快的算法或执行更快的语言(如汇编)来改进这段代码
的工作变得容易些。
进行集中控制。在一个地方对所有任务进行控制是一个很好的想法。控制可能有许多形式。
知道一个表格中的入口数目便是其中一种形式,对硬件系统的控制,如对磁盘、磁带、打印机、
绘图机的控制则是其中另外一种形式。使用子程序从一个文件中进行读操作,而使用另一个子
程序对文件进行写操作便是一种形式的集中控制。当需要把这个文件转化成一个驻留内存的数
据结构时,这一点是非常有用的,因为这一变动仅改变了存取子程序。专门化的子程序去读取
和改变内部数据内容,也是一种集中的控制形式。集中控制的思想与信息隐含是类似的,但是
它有独特的启发能力,因此,值得把它放进你的工具箱中。
隐含数据结构。可以把数据结构的实现细节隐含起来,这样,绝大部分程序都不必担心这
种杂乱的计算机科学结构,而可以从问题域中数据是如何使用的角度来处理数据。隐含实现细
节的子程序可以提供相当高的抽象价值,从而降低程序的复杂程度。这些子程序把数据结构、
操作集中在一个地方,降低了在处理数据结构时出错的可能性。同时,它们也使得在不改变绝
大多数程序的条件下,改变数据结构成为可能。
隐含全局变量。如果需要使用全局变量,也可以像前述那样把它隐含起来、通过存取子程
序来使用全局变量有如下优点:不必改变程序就改变数据结构;监视对数据的访问;使用存取
子程序的约束还可以鼓励你考虑一下这个数据是不是全局的;很可能会把它处理成针对在一个
模块中某几个子程序的局部数据,或处理成某一个抽象数据的一部分。
隐含指针控作。指针操作可读性很差,而且很容易引发错误。通过把它们独立在子程序中,
可以把注意力集中到操作意图而不是机械的指针操作本身。而且,如果操作只在一处进行,也
更容易确保代码是正确的。如果找到了比指针更好的数据结构,可以不影响本应使用指针的子
程序就对程序作改动。
重新使用代码段。放进模块化子程序中的代码段重新使用,要比在一个大型号程序中的代
码段重新使用起来容易得多。
TinKingTZW - 2006-8-21 17:49:00
计划开发一个程序族。如果想改进一个程序,最好把将要改动的那部分放进子程序中,将
其独立。这样,就可以改动这个子程序而不致影响程序的其余部分,或者干脆用一个全新的子
程序代替它。几年前,我曾经负责一个替保险推销员编写系列软件的小组,我们不得不根据每
一个推销员的保险率、报价单格式等等来完成一个特定的程序。但这些程序的绝大部分又都是
相同的:输入潜在客户的子程序,客户数据库中存储的信息、查看、计算价格等等。这个小组
对程序进行了模块化,这样,随推销员而变化的部分都放在自己的模块中。最初的程序可能要
用三个月的时间来开发,但是,在此之后,每来一个推销员,我们只改写其中屈指可数的几个
模块就可以了。两三天就可能写完一个要求的程序,这简直是一种享受!
提高部分代码的可读性。把一段代码放入一个精心命名的子程序,是说明其功能的最好办
法。这样就不必阅读这样一段语句:
if ( Node <> NULL )
while ( Node.Next <> NULL ) do
Node = Node.Next
LeafName = Node.Name
else
LeafName = " "
代替它的是:
LeafName = GetleafName(Node)
这个程序是如此简短,它所需要的注释仅仅是一个恰当的名字而已。用一个函数调用来代
替一个有六行的代码段,使得含有这段代码的子程序复杂性大为降低,并且其功能也自动得到
了注释。
提高可移植性。可以使用子程序来把不可移植部分、明确性分析和将来的移植性工作分隔
开来,不可移植的部分包括:非标准语言特性、硬件的依赖性和操作系统的依赖性等。
分隔复杂操作。复杂操作包括:繁杂的算法、通信协议、棘手的布尔测试、对复杂数据的
操作等等。这些操作都很容易引发错误。如果真的有错误,那么如果这个错误是在某个子程序
中,而不是隐藏在整个程序中的话,查找起来要容易得多。这个错误不会影响到其它子程序,
因为为了修正错误只要改动一个子程序就可以了。如果发现了一个更为简单迅速的算法,那么
用它来代替一个被独立在子程序中的算法是非常容易的。在开发阶段,尝试几种方案并选择其
中一个最好的是非常容易的。
独立非标准语言函数的使用。绝大多数实现语言都含有一些非标准的但却方便的扩展。使
用这种扩展的影响是两面性的,因为在另外一个环境下它可能无法使用。这个运行环境的差异
可能是由于硬件不同、语言的生产商不同、或者虽然生产商相同、但版本不同而产生的。如果
使用了某种扩展,可以建立一个作为进入这种扩展大门的子程序。然后,在需要时,可以用订
做的扩展来代替这一非标准扩展。
简化复杂的布尔测试。很少有必要为理解程序流程而去理解复杂的布尔测试。把这种测试
放入函数中可以提高代码的可读性,因为:
(1) 测试的细节已经被隐含了。
(2) 清楚的函数名称已经概括了测试目的。
赋予这种测试一个函数,该函数强调了它的意义,而且这也鼓励了在函数内部增强其可读
TinKingTZW - 2006-8-21 17:49:00
性的努力。结果是主程序流和测试本身都显得更加清楚了。
是出于模块化的考虑吗吗?绝不是。有了这么些代码放入子程序的理由,这个理由是不必要
的。事实上,有些工作更适合放在一个大的子程序中完成(关于程序最佳长度的讨论见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。
TinKingTZW - 2006-8-21 17:49:00
5.1.2  创建子程序的理由总结
以下是创建子程序理由概述:
· 降低复杂性
· 避免重复代码段
· 限制改动带来的影响
· 隐含顺序
· 改进性能
· 进行集中控制
· 隐含数据结构
· 隐含指针操作
· 隐含全局变量
· 促进重新使用代码段
· 计划开发一个软件族
· 改善某一代码段可读性
· 改善可移植性
· 分隔复杂操作
· 独立非标准语言函数的使用
· 简化复杂的布尔测试
5.2    子程序名称恰当
一个恰当的子程序名称应该清晰地描述出于程序所作的每一件事。以下是给子程序有效
命名的指导方针:
对于过程的名字,可以用一个较强的动词带目标的形式。一个带有函数的过程往往是对某
一目标进行操作。名字应该反映出这个过程是干什么的,而对某一目标进行操作则意味着我们
应该使用动宾词组。比如,PrintReport),Checkotderlnfo()等,都是关于过程的比较恰当的名
字。
在面向对象的语言中,不必加上对象名,因为对象本身在被调用时就已经出现了。这时可
求助于诸如RePort.Print(),Orderlnfo.Check()和MonthlyRevenu.Cafe()等名字。而像RePort.
PrintRePort这类名字则是冗余的。
对于函数名字,可以使用返回值的描述。一个函数返回到一个值,函数应该用它所返回的
值命名,例如Cos(),PrinterReady(),CurrentPenColor()等等都是不错的函数名字,因为它精确
地描述了函数将返回什么。
避免无意义或者模棱两可的动词。有些动词很灵活,可以有任何意义,比如
HandleCalculation(),ProcessInput()等于程序名词并没有告诉你它是作什么的。这些名字至多
告诉你,它们正在进行一些与计算或输入等有关的处理。当然,有特定技术情形下使用“handle”
等词是个例外。
有时,子程序的唯一问题就是它的名字太模糊了,而子程序本身的设计可能是很好的。如
TinKingTZW - 2006-8-21 17:49:00
果用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(),
TinKingTZW - 2006-8-21 17:50:00
CaldoanPayment()和GetIconlocation()等等。当然,这个评价只有在子程序的名称与其实际内容
相符时才成立。如果它们同时还作其它工作,那么它们的内聚性就要低得多而且命名也不恰当。
顺序内聚性。顺序内聚性是指在子程序内包含需要按特定顺序进行的、逐步分享数据而又
不形成一个完整功能的操作,假设一个子程序包括五个操作:打开文件、读文件、进行两个计
算、输出结果、关闭文件。如果这些操作是由两个子程序完成的,DoStep1()打开文件、读文件
和计算操作,而DoStep2()则进行输出结果和关闭文件操作。这两个子程序都具有顺序内聚性。
因为用这种方式把操作分隔开来,并没有产生出独立的功能。
但是,如果用一个叫作GetFileData()的子程序进行打开文件和读文件的操作,那么这个子
程序将具有功能内聚性。当操作来完成一项功能时,它们就可以形成一个具有功能内聚性的子
程序。实际上,如果能用一个很典型的动宾词组来命名一个子程序,那么它往往是功能内聚性,
而不是顺序内聚性。给一个顺序内聚性的子程序命名是非常困难的,于是便产生了像Dostep1()
这种模棱两可的名字。这往往意味着你需要重新组织和设计子程序,以使它是功能内聚性的。
通讯内聚性。通讯内聚性是在一个子程序中,两个操作只是使用相同数据,而不存在其它
任何联系时产生的。比如,在GetNameAndChangePhoneNumber()这个子程序中,如果Name 和
PhoneNumber 是放在同一个用户记录中的,那么这个子程序就是通讯内聚性。这个子程序从事
的是两项而不是一项工作,因此,它不具备功能内聚性。Name 和PhoneNamber 都存储在用户
记录中,不必按照某一特定顺序来读取它们,所以,它也不具备顺序内聚性。
这个意义上的内聚性还是可以接受的。在实际中,一个系统可能需要在读取一个名字的同
时变更电话号码。一个含有这类子程序的系统可能有些显得别扭,但仍然很清楚且维护性也不
算差,当然从美学角度来说,它与那些只作一项工作的子程序还有一定差距。
临时内聚性。因为同时执行的原因才被放入同一个子程序里,这时产生临时内聚性。典型
的例子有;Startup(),CompleteNewEmployee(),Shutdown()等等,有些程序员认为临时内聚性是
不可接受的,因为它们有时与拙劣的编程联系在一切,比如,在像Startup()这类子程序中往往含
有东拼西凑的杂烩般的代码。
要避免这个问题,可以把临时内聚性子程序设计成一系列工作的组织者。前述的Startup
()子程序进行的操作可能包括:读取一个配置文件、初始化一个临时文件、建立内存管理、显示
初始化屏幕。要想使它最有效地完成这些任务,可以让这个子程序去调用其它的专门功能的子
程序,而不是由它自己直接来完成这些任务。
5.3.2  不可取的内聚性
其余类型的内聚性,一般来说都是不可取的。其后果往往是产生一些组织混乱而又难以调
试和改进的代码。如果一个子程序具有不良的内聚性,那最好重新创建一个较好的子程序,而
不要去试图修补它。知道应该避免什么是非常重要的,以下就是一些不可取的内聚性:
过程内聚性。当子程序中的操作是按某一特定顺序进行的,就是过程内聚性。与顺序内聚
性不同,过程内聚性中的顺序操作使用的并不是相同数据。比如,如果用户想按一定的顺序打
印报告,而所拥有的子程序是用于打印销售收入、开支、雇员电话表的。那给这个子程序命名
是非常困难的,而模棱两可的名字往往代表着某种警告。
TinKingTZW - 2006-8-21 17:50:00
逻辑内聚性。当一个子程序中同时含有几个操作,而其中一个操作又被传进来的控制标志
所选择时,就产生了逻辑内聚性。之所以称之为逻辑内聚性,是因为这些操作仅仅是因为控制
流,或者说“逻辑”的原因才联系到一起的,它们都被包括在一个很大的if或者case语句中,
它们之间并没有任何其它逻辑上的联系。
举例来说,一个叫作InputAll()的子程序,程序的输入内容可能是用户名字、雇员时间卡信
息或者库存数据,至于到底是其中的哪一个,则由传入子程序的控制标志决定。其余类似的子
程序还有ComputeAll(),EditAll(),PrintAll()等等。这类子程序的主要问题是一定要通过传入一
个控制标志来决定子程序处理的内容。解决的办法是编写三个不同的子程序,每个子程序只进
行其中一个操作。如果这三个子程序中含有公共代码段,那么还应把这段代码放入一个较低层
次的子程序中,以供三个子程序调用。并且,把这三个子程序放入一个模块中。
但是,如果一个逻辑内聚性的子程序代码都是一系列if和case语句,并且调用其它子程序,
那么这是允许的。在这种情况下,如果程序的唯一功能是调度命令,而它本身并不进行任何处
理,那么这可以说是一个不错的设计。对这种子程序的专业叫法是“事物处理中心”,事物处理
中心往往被用作基础环境下的事件处理,比如,Apple Macintosh和Microsoft Windows。
偶然内聚性。当同一个子程序中的操作之间无任何联系时,为偶然内聚性。也叫作“无内
聚性”。本章开始时所举的Pascal例程,就是偶然内聚性。
以上这些名称并不重要,要学会其中的思想而不是这些名词。写出功能内聚性的子程序几
乎总是可能的,因此,只要重视功能内聚性以获取最大的好处就可以了。
5.3.3 内聚性举例
以下是几个内聚性的例子,其中既有好的,也有坏的:
功能内聚性例子。比如计算雇员年龄并给出生日的子程序就是功能内聚性的,因为它只完
成一项工作,而且完成得很好。
顺序内聚性的例子。假设有一个按给出的生日计算雇员年龄、退休时间的子程序,如果它
是利用所计算的年龄来确定雇员将要退休的时间,那么它就具有顺序内聚性。而如果它是分别
计算年龄和退休时间的,但使用相同生日数据,那它就只具有通讯内聚性。
确定程序存在哪种不良内聚性,还不如确定如何把它设计得更好重要。怎样使这个子程序
成为功能内聚性呢?可以分别建立两个子程序,一个根据生日计算年龄,另外一个根据生日确
定退休时间,确定退休时间子程序将调用计算年龄的程序,这样,它们就都是功能内聚性的,
而且,其它子程序也可以调用其中任一个子程序,或这两个部调用。
通讯内聚性的例子。比如有一个打印总结报告,并在完成后重新初始化传进来的总结数据
的子程序,这个子程序具有通信内聚性,因为这两个操作仅仅是由于它们使用了相同的数据才
联系在一起。
同前例一样,我们考虑的重点还是如何把它变成是功能内聚性,总结数据应该在产生它的
地方附近被重新初始化,而不应该在打印子程序中重新初始化。把这个子程序分为两个独立的
子程序.第一个打印报告,第二个则在产生或者改动数据的代码附近重新初始化数据。然后,
利用一个较高层次的子程序来代替原来具有通讯相关的子程序,这个子程序将调用前面两个分
出来的子程序。
逻辑内聚性的例子。一个子程序将打印季度开支报告、月份开支报告和日开支报告.具体
TinKingTZW - 2006-8-21 17:50:00
打印哪一个,将由传入的控制标志决定,这个子程序具有逻辑内聚性,因为它的内部逻辑是由
输进去的外部控制标志决定的。显然,这个子程序不是按只完成一项工作并把它作好的原则。
怎样使这个子程序变为功能内聚性呢?建立三个子程序:一个打印季度报告,一个打印月
报告、一个打印日报告,改进原来的子程序,让它根据传送去控制标志来调用这三个子程序之
一。调用子程序将只有调用代码而没有自己的计算代码,因而具有功能内聚性。而三个被调用
的手程序也显然是功能内聚性的。非常巧合,这个只负责调用其它子程序的子程序也是一个事
务处理中心。最好用如DispatchReporPrinting()之类带有“调度”或“控制”等字眼的词来给事
务处理中心命名,以表示它只负责命令温调度,而本身并不做任何工作。
逻辑内聚性的另一个例子。考虑一个负责打印开支报表、输入新雇员名字并备份数据库的
子程序,其具体执行内容将由传入的控制标志控制。这个子程序只具有逻辑内聚性,虽然这个
关联看起来是不合逻辑的。
要想使它成为功能内聚性,只要按功能把它分成几部分就可以了。不过,这些操作有些过
于凌乱。因此,最好重新建立一个调用各子程序的代码。当拥有几个需要调用的子程序时,重
新组织调用代码是比较容易的。
过程内聚性的例子。假设有一个子程序,它产生读取雇员的名字,然后是地址,最后是它
的电话号码。这种顺序之所以重要,仅仅是因为它符合用户的要求,用户希望按这种顺序进行
屏幕输入。另外一个子程序将读取关于雇员的其它信息。这个子程序是过程内聚性,因为是由
一个特定顺序而不是其它任何原因,把这些操作组合在一起的。
与以前一样,如何把它变为功能内聚性的答案仍然是把它分为几个部分,并把这几部分分
别放入程序中。要保证调用子程序的功能是单一、完善的。调用子程序应该是诸如
GetEmployeeData()这样的子程序,而不该是像GetFirstPartofEmployeeData()这类的子程序。可能
还要改动其余读取数据的子程序。为得到功能内聚性,改动几个子程序是很正常的。
同时具有功能和临时内聚性的程序。考虑一个具有完成一项事物处理所有过程的子程序,
从用户那里读取确认信息,向数据存入一个记录,清除数据域,并给计数器加1。这个程序是
功能内聚性的,因为它只从事一项工作,进行事物处理,但是,更确切地说,这个子程序同时
也是临时内聚性的,不过当一个子程序具有两种以上内聚性时,一般只考虑最强的内聚性。
这个例子提出了如何用一个名字恰如其分地抽象描述出程序内容的问题。比如可以称这
个子程序为ConfirmEntryAndAdjustData(),表示这个干程序仅具有偶然内聚性。而如果称它为
CompleteTransaction(),那么就可能清楚地表示出这个子程序仅具有一个功能,而已具有功能内
聚性。
过程性、临时或者可能的逻辑内聚性。比如一个进行某种复杂计算前5个操作,并把中间
结果返回到调用子程序。由于5 项操作可能要用好几个小时,因此当系统瘫痪时,这个子程序
将把中间结果存入一个文件中,然后,这个子程序检查磁盘,以确定其是否有足够空间来存储
最后计算结果,并把磁盘状态和中间结果返回到调用程序。
这个子程序很可能是过程内聚性的,但你也可能认为它具有临时内聚性,甚至具有逻辑内
聚性。不过,不要忘了问题的关键不是争论它具有哪种不好的内聚性,而是如何改善其内聚性。
原来的子程序是由一系列令人莫名其妙的操作组成的,与功能内聚性相距甚远,首先,调
用子程序不应该调用一个,而应该调用几个独立的子程序:l)进行前5步计算的子程序;2)把
中间结果存入一个文件;3)确定可用的磁盘存储空间。如果调用子程序被称作
TinKingTZW - 2006-8-21 17:51:00
ComputeExtravagantNumber(),那么它不应该把中间结果写入一个文件,也决不该为后来的操作
检查磁盘剩余空间,它所作的就仅限于计算一些数而已。对这个子程序的精心重新设计,将至
少影响到一至两个层次上的子程序,对于这顶任务的较好设计,见图5-l。
图5-1 进行任务分解以获得功能内聚性举例
图中画阴影的部分是由原来的子程序从事的工作,在新组织结构中它们位于不同的层次上,
这就是为什么为了把这些工作放人恰当的子程序中,要进行这么多重新组织工作的原因。用几
个功能内聚性的子程序来代替一个具有不良内聚性的子程序是很平常的。
5.4  松散耦合性
所谓耦合性指的是两个子程序之间联系的紧密程度。耦合性与内聚性是不同的。内聚性
是指一个子程序的内部各部分之间的联系程度,而耦合指的是子程序之间的联系程度。研究它
们的目的是建立具有内部整体性(强内聚性),而同时与其它子程序之间的联系的直接、可见、
松散和灵活的子程序(松散耦合)。
子程序之间具有良好耦合的特点是它们之间的耦合是非常松散的,任一个子程序都能很容
易地被其它子程序调用。火车车箱之间的联接是非常容易的,只要把两节车箱推撞到一起,挂
钩就会目前挂上,想象一下,用螺栓把它们固定到一起,或者只有特定的车厢之间才能联接到
一起,那么事情将会有多么麻烦。火车车厢之间的联接之所以非常容易,是因为它们的联接装
置非常简单。同样,在软件中,也应该使子程序之间的耦合尽量简单。
在建立一个子程序时,应尽量避免它对其它子程序有依赖性,应该使它们像商业上的伙伴
一样相互分离,而不要使它们像连体婴儿一样密不可分。类似Sin()的函数是松散耦合的,因为
它所需要的只是一个传递进去的角度值。而类似InitVars(varl,varZ,var3,……,varN)的函
数则是紧密耦会的,因为调用程序事实上知道函数内部做什么。依靠使用同一全局变量联系在
一起的子程序之间,其耦合程度则更高。
5.4.1 耦合标准
以下是几条估计子程序间耦合程度的标准:
耦合规模。所谓耦合规模是指两个子程序之间联系的数量多少。对于耦合来说,联系的数
TinKingTZW - 2006-8-21 17:51:00
量越少越好,因为一个于程序的接口越少,那么在把它与其它子程序相连接时,所要做的工作
也越少。一个只有一个参数的子程序与调用它的程序间的耦合程序,要比有6 个参数的子程序
与调用它的程序间的耦合程度松散得多。一个使用整型参数的子程序与其调用程序之间的耦合
程度,也要比一个使用有10 个元素数组或者结构化数据的子程序与其调用程序之间的耦合程度
松散得多。同样,使用一个全局变量的子程序与使用十二个全局变量的子程序相比,其耦合程
度也要松散得多。
密切性。密切性指的是两个子程序之间联系的直接程度。联系越直接越好,两个子程序之
间联系最密切的是参数表中的参数。这时,两个程序直接通讯。这时这个参数就像接吻时的嘴
唇。联系密切程度稍低的是作用于同一全局数据的两个子程序。它们之间交流的直接性稍低。
全局变量就像是两个子程序之间的爱情,它可能消失在信中,也可能到你想要它到的地方。联
系程度最低的是作用于同一数据库记录或文件的两个子程序,它们都需要这个数据但却又羞于
通知对方,这个被分享的数据就像是在课堂上传阅着的一张写有“你喜欢我吗?请回答‘是’
还是‘不是’”的纸条。
可见性。可见性是指两个子程序之间联系的显著程度。编程不像是在中央情报局中工作,
不会因为行动隐蔽而受到表彰,它更像是作广告,干得越是大张旗鼓,受到的表彰也就越多。
在参数表中传递数据是明显的,因而也是好的。而通过改动全局数据以便让别的子程序来使用
它,则是一个隐蔽的联系因而也是不好的。对全局数据联系进行注释以使其更明显,可能稍好
些。
灵活性。灵活性是指改变两个子程序之间联系的容易程度。形象地说,你更喜欢电话上的
快速插口装置,而不会喜欢用电烙铁把铜线焊到一起,灵活性可能有一部分是由其它耦合特性
决定的,但也有一些区别。比如,有一个按照给定的被雇用日期和被雇用部门,寻找雇员的第
一个监工的子程序,并命名它为LookUpFirstsupervisor()。同时,还有一个对变量EmpRec 进行
结构化的子程序,变量EmpRec 包括雇用日期、雇用部门等信息,第二个子程序把这个变量传
给第一个子程序。
从其它观点来看,两个子程序之间的耦合是非常松散的。因为处于第一个和第二个子程序
之间的变量EmpRec 是公共的,所以它们之间只有一个联系。现在,假设需要用第三个子程序
来调用子程序LookUpFirstSupervisor(),但这个子程序中不含EmpRec,却含有雇用部门和雇用
日期信息。这时LookUpFirstSupervisor()就不是那么友好了,它不情愿与第三个子程序进行合作。
对于调用LookUpFirstsupervisor()的子程序来说,它必须知道EmpRec 的数据结构。它可以
使用一个仅有两个域的变元EmpRec,但这又需要知道LookUpFirstSupervisor()内部结构,即那
些仅供它使用的域,这是一个愚蠢的解决方法。第二个方案是改动LookUpFirstSupervisor,使它
自带雇用部门和雇用日期信息,而不是使用EmpRec。不管采用哪种方案,这个子程序都不像最
初看起来那么灵活了。
如果愿意的话,一个不友好的子程序也是可以变为友好的。这种情况可以通过让它明确带
有雇用部门和日期信息,而不再使用EmpRec来达到这一目的。
简而言之,如果一个子程序越容易被其它子程序调用,那么它的耦合程度也就越低。这样
的好处是可以增强灵活性和维护性。在建立系统结构时,应该沿着相互耦合程度的最低线将其
分开。如果把程序看成一块木头的话,就是要沿着它的纹理把它劈开。
TinKingTZW - 2006-8-21 17:51:00
5.4.2  耦合层次
传统上,把耦合层次称为非直觉性(unintuitive)。所以,在以下叙述中,将交替使用这两
个名字。在以下叙述中,既有好的耦合,也有不好的耦合。
简单数据耦合。如果两个子程序之间传递的数据是非结构化的,并且全部都是通过参数表
进行的,这通常称作“正常耦合”,这也是一种最好的耦合。
数据结构耦合。如果在两个程序之间传递的数据是结构化的,并且是通过参数表实现传递
的,它们之间就是数据结构耦合的。这种耦合有时也称之为“邮票耦合”(stamp coupling)(不
过我总觉得这种叫法非常奇怪)。如果使用恰当的话,这种耦合也不错,它与简单耦合的主要区
别是它所采用的数据是结构化的。
控制耦合。如果一个子程序通过传入另一个子程序的数据通知它该作什么,那么这两个子
程序就是控制耦合的。控制耦合是令人不快的,因为它往往与逻辑内聚性联在一起,并且,通
常都要求调用程序了解被调子程序的内容与结构。
全局数据耦合。如果两个子程序使用同一个全局数据,那它就是全局数据耦合的。这也就
是通常所说的“公共耦合”或“全局耦合”。如果所使用的数据是只读的,那么这种耦合还是可
以忍受的,但是,总的来说,全局耦合是不受欢迎的,因为这时子程序之间的联系既不密切,
又不可见。这种联系容易被疏漏掉,甚至可以认为它是一种由信息隐含带来的错误——信息丢
失。
不合理耦合( pathological)。如果一个子程序使用了另外一个子程序中代码,或者它改变了
其中的局部变量,那么它们就是不合理耦合的。这种耦合也称之为“内容耦合”。这一类耦合是
不能接受的,因为它不满足关于耦合规模、密切性、可见性和灵活性中的任何一条标准。虽然
这是一个很紧的联系,但是这种联系却是不密切的。改动另一个子程序中的数据无异于在其后
背桶上一刀,而且,这背后一刀从表面上又是看不出来的。由于它是建立在一个子程序完全了
解另一个程序内容的基础之上的,因此其灵活性也是很差的。许多结构化语言中,都有明确禁
止不合理耦合的语法规则。但是,在Basic或汇编语言中,它却是允许的。因此,在用这种语言
编程时,一定要小心!
以上所有类型的耦合,如图5-2 所示。
5.4.3  耦合举例
以下是上述各种耦合的例子,其中有好的,也有坏的:
简单数据耦合的例子。比如,一个程序向Tan()子程序传递含有角度值的变量,那它们之
间就是简单数据耦合的。
简单数据耦合的另一个例子。两个程序向另一个子程序传递姓名、住址、电话号码、生日
和身份证号码等五个变量。
可接受的数据结构耦合的例子。一个程序向另一个子程序传递变量EmpRec,EmpRec是一
个结构化的变量,包括姓名、住址、生日等等五个方面的数据,而被调用的子干程序则全部使
用这五个域。
不可取的数据结构耦合举例。一个程序向另一个子程序传递同样的变量EmpRec,但是,如
果被调用的子程序只使用其中两个域,比如电话号码和生日。这虽然还是数据结构耦合,但却
不是个很好的应用,如果把生日和电话号码作为简单变量来传递的话,将使联系更加灵活,