瑞星卡卡安全论坛

首页 » 技术交流区 » 系统软件 » 【转载】软件开发人员必备工具书 【代码大全】
TinKingTZW - 2006-8-21 17:52:00
而且会使它们之间的两个特定域真正联系的可见性更好。
有问题的数据结构耦合的例子。一个程序向另一个子程序传递变量OfficeRec。OfficeRec
有27 个域,而被调用的子程序使用其中16 个,这也是数据结构耦合,但是,它是一个好的数
据结构耦合吗?决不是。传递OfficeRec 使得联系是大规模的,这个事实非常明显,而传递16
个单独参数,则又再次非常拙劣地表明了这一点,如果被调用子程序仅使用其中的6到7 个域,
那么单个地传递它们是个好主意。
在现在这种情形下,可以进一步对OfficeRec进行结构化,以使得在被调用程序中用得到的
16 个域包含在一个或两个亚结构中,这将是最简洁的解决办法。
简单数据耦合或可能数据结构耦合的例子。一个程序调用EraseFile()子程序,通过一个
含有待删文件名的字符串确定将要删去的文件,这很可能是一个简单数据耦合。但也可以说这
是一个数据结构耦合,因为字符串是一个数据结构。
我觉得,我们的结论是半斤与八两的关系,这两种叫法都是同样正确的,对其作严格区分是没
必要的。
控制耦合的例子。比如,一个程序向另一个子程序传递控制标志,通知它到底是打印月报
表、季度报表还是年度报表。
不可取的全局数据耦合的例子。一个程序改动一个表的人口作为全局变量,这个表是以雇
员的识别卡作为索引的。然后,这个程序又调用另一个子程序并把雇员识别卡作为一个参数传
递给它,而这个被调用的子程序则用雇员识别卡去读全局数据表,这是一个典型的全局数据耦
合(虽然仅仅传递雇员识别卡形成的是简单数据耦合,但是第一个程序对表入口的改动,已经
决定了这是一种最坏的耦合——全局数据耦合)。
可取的全局数据耦合的例子。一个程序把雇员识别卡传递给另一个子程序,两个程序都利
用这个识别卡从一个全局表中读取雇员的名字,两个子程序都没有改变全局数据。
TinKingTZW - 2006-8-21 17:52:00
这通常也称为“全局数据耦合”,但事实上它更像“简单数据耦合”,我们也可称它为“可取的
全局数据耦合”。与前述一个子程序改变另一个程序使用数据的例子不同,这两个程序并不是由
全局数据联系在一起的。比较两个例子,这种对相同全局数据的只读使用是良性的。这两个从
同一个全局表读取数值的程序,与上述那两个通过使用全局数据来掩盖它们之间联系的程序是
完全不同的。
内容耦合的例子。在汇编语言中,一个子程序可以知道另一个子程序中说明为局部变量的
表的地址。它可以命名用这个地址直接去改动这个表,而地址在两个子程序间并没有当作参数
传递。
内容耦合另一个例子。一个Basic程序利用GOSUB语句来执行另一个子程序中的一段代码。
好的耦合关键是它可以提供一个附加的抽象层次——一旦写好它,就可以认为它是独立的。
它降低了整个程序的复杂性,并且使你每次只致力于一件事情。如果在使用子程序时要求同时
考虑几件事情——知道它的内部内容、对全局数据的改动、不确定的功能等,就会使其丧失抽
象能力,那么使用子程序还有什么用呢?子程序本来是用于降低复杂性的工具,如果使用它没
有使工作更简单,那说明没有用好它。
5.5 子程序长度
理论上,常把一个子程序的最佳长度定为一两页,即66 到132行。按照这种原则,在七十
年代,IBM 公司曾把子程序的长度限制在50 行以下,而TRW 公司则把这个标准定为132 行
(McCabe 1976)。几乎没有什么证据证明这一限制是正确的。相反,倒是证明较长子程序更有
利的证据更有说服力,请参照以下几点:
· Basili和Perricone 1984 年的研究表明,子程序的长度与错误数量是成反比的。随着子
程序长度的增加(长至200 行的代码),每一行的错误数量开始下降。
· 另一个由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)。
研究子程序长度有什么好处呢?如果你是一个经理,不要限制程序员们编写长于一页的子
程序,刚才引用的资料和程序员们自己的经验都可以证明你这样作是正确的。如果想编写一个
TinKingTZW - 2006-8-21 17:52:00
长度是100行,150行或200 行的子程序,那就按照你想的去作吧。目前的证据表明,这种长度
的子程序并不更易引发错误,而其开发更为容易。
如果要开发长于200 行的子程序,就要小心了(这里的长度不包括注释行和空行)。目前还
没有任何证据表明长于200 行的子程序会带来更低的成本或更少的错误。而这样做却会使你达
到可理解性的上限。在对IBM为OS/360操作系统及其它系统而开发代码研究中发现,最易出
错的子程序是那些大于500 行的子程序。在超过500 行之后,错误数量会与子程序的长度成正
比。而且,对一个有148,000 行代码软件的研究发现,修改少于143行子程序错误所耗费的成
本,要比修复长于143 行的子程序中错误成本低2.4 倍(Sely和Basin 1991)。
5.6 防错性编程
防错性编程并不意味着要对自己的程序提高警惕,这一想法是在防错性驾驶的基础上产
生的,在这种驾驶方法中,必须在心中时刻认为其它驾驶员的行为都是不可预测的。这样,
就可以在他们做出某些危险举动时,确保自己不会因此受伤。在防错性编程中,其中心思想是,
即使一个子程序被传入了坏数据,它也不会被伤害,哪怕这个数据是由其它干程序错误而产生
的。更一般地说,其思想核心是承认程序中都会产生问题,都要被改动,一个聪明的程序员就
以这点为依据开发软件。
作为本书介绍的提高软件质量技术之一,防错性编程是非常有用的。最有效的防错性编码
途径是一开始就不要引人错误。可以采用逐步设计方法、在编码前先写好PDL、进行低层次设
计、审查等都可以防止错误引入。因此,应优先考虑它们,而不是防错性编程。不过,你可以
把防错性编程与这些技术组合起来使用。
5.6.l  使用断言
断言是一个在假设不正确时会大声抗议的函数或宏指令。可以使用断言来验证在程序中作
出的假设并排除意外情况。一个断言函数往往大致带有两个内容:假设为真时的布尔表达式和
一个为假时要打印出来的信息。以下是一个假定变量Denominator不为零时一个Pascal断言:
Assert ( Denominator<>0,'Denominator is unexpectedlg equal to 0.' ) ;
这个断言假定Denominator 不等于”0”,第一部分Denominator<>0 是一个布尔表达式,其
结果为True或False。第二部分是当第一部分的结果为False肘,将要打印出来的信息。
即使不愿让用户在最终软件中看到断言信息,在开发和维护阶段,使用断言还是非常方便
的。在开发阶段,断言可以消除相互矛盾的假设,消除传入于程序的不良数值等等。在维护,
可以表明改动是否影响到了程序其它部分。
断言过程是非常容易写的,下面就是一个用Pascal写成的例子:
Procedure Assert
(
Aseertionn: boolean;
Message : string
);
TinKingTZW - 2006-8-21 17:52:00
begin
if( not Assertion)
begin
writeln(Messase);
writeln('stopping the program.');
halt(FATAL ERROR)
end
end;
一旦写好了这样一个过程,就可以用像第一个例子那样的语句来调用它。
下面是使用断言的一些指导方针:
如果有预处理程序的话,使用预处理程序宏指令。如果在开发阶段使用预处理程序处理断
言,那么在最终代码中把断言去掉是非常容易的。
在断言中应避免使用可执行代码,把可执行代码放入断言,在关闭断言时,编译程序有可
能把断言捎去。请看以下断言;
ASsert(FileOpen(InputFile<>NULL,’Coulldnt Oped input file’);
这行代码产生的问题是,如果不对断言进行编译,也编译不了打开文件的代码,应把可执
行语句放在自己的位置上,把结果赋给一个状态变量,然后再测试状态。以下是一个安全使用
断言的例子:
FileStatus : FileOpen (InputFile);
Assert(FileStatus<> NULL,'couldn't Opeh input file');
5.6.2  输入垃圾不一定输出垃圾
一个好的程序从来不会输出乱七八糟像垃圾似的东西,不管它被输入的是什么。一个好程
序的特点是“输入垃圾,什么也不产生”,或“输入垃圾,输出错误信息”,也可以是“不允许
垃圾进入”。从现在的观点来看“输入垃圾,输出垃圾”,往往是劣质程序。
检查所有外部程序输入的数值。当从用户文件中读取数据时,要确保读人的数据值在允许
范围之内。要保证数值是可以取的,并且字符串要足够短,以便处理。要在代码中注释出输入
数据的允许范围。
检查全部子程序输入参数值。检查子程序输入参数值,事实上与检查外部程序数据是一样
的,只不过此时由子程序代替了文件或用户。
以下是一个检查其输入参数的子程序的例子,用c语言编写:
float tan
(
float OppositeLength;
float AdjacentLength;
)
{
/*计算角度正切*/
Aseert( AdjancentLength != 0,”AdjanceLength deteced to be 0." );
TinKingTZW - 2006-8-21 17:53:00
return (OppsiteLenght/AdjancetLength);
}
决定如何处理非法参数。一旦发现了一个非法参数,该如何处理呢?根据不同情况,可
以希望返回一个错误代码、返回一个中间值、用下一个合法数据来代替它并按计划继续执行、
与上次一样返回一个正确答案、使用最接近的合法值、调用一个处理错误的子程序、从一个子
程序中调用错误信息并打印出来或者干脆关闭程序。由于有这样多的方案可供选择,所以当在
程序的任一个部分处理非法参数时,一定要仔细,确定处理非法参数的通用办法,是由结构设
计决定的,应该在结构设计层次上予以说明。
5.6.3  异常情况处理
应核预先设计好异常处理措施来注意意想不到的情况。异常处理措施应该能使意外情况
的出现在开发阶段变得非常明显,而在运行阶段又是可以修复的,例如,在某种情况下使用了
一个case 语句,其中只预计到了五种情况,在开发阶段,应该能利用异常情况处理产生一个警
告,提示出现了另外一种情况。而在产品阶段,应该利用异常情况处理做一些更完美的工作,
比如向一个错误记录文件中写入信息等。总之,应该设计出不必费多大周折,就可以从开发阶
段进入产品阶段的程序。
5.6.4  预计改动
改动几乎是每个程序都不可避免的现象。比如,开发一个旧的软件新版本,就需要对原有
代码作出许多改动,不过,即使是在开发一个软件的第一版时,也不得不由于加入某些没有预
计到的功能而对其进行改动。在开发软件时,应该努力作到使它很容易地进行改动。而且,越
是可能的改动,越是要容易进行,把你在其中预想到的改动域隐含起来,是减少由于改动而对
程序带来影响的最有力武器之一。
5.6.5 计划去掉调试帮助
调试帮助措施包括:断言、内存检查报告、打印语句等及其它一些为方便调试而编写的代
码。如果所开发的软件是供自己使用的,那么把它们保留在程序中并无大碍。但是,如果是一
个商用软件,那么这些措施留在程序中,则会影响其速度和占用空间等性能指标。在这种情况
下,应事先作好计划,避免调试信息混在程序中,下面是几种方法。
使用版本控制。版本控制工具可以从同一源文件中开发出不同版本的软件。在开发阶段,
可以设置包含全部调试辅助手段的版本控制工具,这样,到了产品阶段,就可以很容易地去掉
在商用版本中所不希望出现的这些辅助手段。
使用内部预处理程序。如果在编程环境中带有预处理程序,如C 语言,那么仅用一下编译
程序开关,就可以加入或去掉这些辅助手段。可以直接使用预处理程序,也可以通过编写宏指
令来进行预处理程序定义。下面是一个用c语言写成的,直接使用预处理程序的例子:
#define DEBUG
TinKingTZW - 2006-8-21 17:53:00
#ifdefined(DEBUG)
/*调试代码*/

#endif
这种思想可能有几种表现形式。不仅仅是定义DEBUG,还可以赋给它一个值,然后再测试
它的值,而不是测试它是否被定义了。用这种方法可以区分调试代码的不同层次。也可能希望
某些调试用代码长期驻存在程序中,这时可以使用诸如#if DEBUG>0 之类的语句,把这段代
码围起来,其它一些调试代码可能是有某些专门用途的,这时可以用#if DEBUG—一
PRINTER ERROR 这样的语句把这段代码围起来,在其它地方,还可能想设置调试层次,可用
如下语句:
#if DEBUEG>LEVEL_A
要是不喜欢程序内部充斥着#if defined()这样的语句的话,可以用一个预处理程序宏指
令来完成同一任务。以下是一个例子:
#define DEBUG
#if defined( DEBUG)
#define DebugCode( code fragment){code_fragment}
#else
#define DebugCode(code fragment)
#endif
DebugCode
(
statement 1;
statement 2;

statment n;
)
与使用预处理程序的第一个例子一样,这种技术也有多种形式,可以使它更复杂一些,从
而不是简单地全部包括或全部排除调试代码。
编写自己的预处理程序。如果编程语言中没有预处理程序,可以自己编写一个加入或去掉
调试代码的预处理程序,这项工作是非常容易的。还要建立一个标识调试代码的约定,并编写
自己的预编译程序来遵循这一约定。比如,在Pascal 中,可以编写一个对如下关键字作出反应
的预编译程序:/#BEGIN DEBUG/和/#END DEBUG/人并写一个批处理文件来调用这个
预处理程序,然后再编译这段已经预处理过的代码。从长远观点来看,这样做可以节约许多时
间,因为你不会误编译没有预处理过的代码。
保留使用调试程序。在许多情况下,可以调用一个调试子程序来进行调试工作。开发阶段
根据DEBUG是否定义,包含或不包含这段代码
TinKingTZW - 2006-8-21 17:53:00
在控制返回调用程序之前,这个子程序可能进行几项操作。在最终软件中,可以用一个子程序
来代替那个复杂的调试子程序,这个子程序将立即返回控制,或者在进行两个快速操作之后,
返回控制。使用这种方法,对性能带来的影响很小,与编写自己的预处理程序来相比,其速度
也要快得多。所以,有必要保留这个子程序在开发阶段和最终产品阶段的两个版本,以供在将
来的开发和产品调试中使用。
比如,可以用一个检查传入其中指针的子程序作为开始:
Procedure DoSomething
(
Pointer:PONITER_TYPE;
...
);
begin
{ check parameters passed in }
CheckPointer(Pointer); 这行调用检查指针的子程序
...
end.
在开发阶段,CheckPointer()子程序将对指针作全面检查,这项工作可能很费时,但却非
常有效,它很可能是以下这个样子的:
Procdure CheCPOnter( POinter: POINTER_TYPE);——这个程序检查传入的每个指
针,它可以在开发期间使用.完成格外检查
begin
{perform check 1--maybe chech that it's not nil}
{perfom check 3 --maybe check that what is point to isn't corrupted}
{perform check n--...}
end;
当程序进入最终产品阶段时,可能并不希望所有的内务操作都与指针检查联系到一起,这
寸,可以用下面这个子程序来代替刚才那个子程序:
Proccure CheckPointer (Pointer: POlNTER_TYPE ); 本程序仅是即返回其调用程序
begin
{no code;just return to caller}
end;
以上这些并不是去掉调试辅助工具的所有方案,但从提供一些在你的环境下能有效工作
守案这个思路的角度来说,这些已经是足够的了。
5.6.6  尽早引入调试辅助工具
越早引入调试辅助工具,它们所起的作用也就会越大。一般说来,只有在被某一问题困扰
TinKingTZW - 2006-8-21 17:54:00
几次之后,你才会舍得花功夫去编写调试辅助工具,但如果你在第一次遇到问题时就这样做,
或者引用一个以前遗留下的调试辅助工具,那么它将在整个项目中都会对你有很大帮助。
5.6.7  使用“防火墙”包容错误带来的危害
“防火墙”技术是一种包容危害策略,在建筑物中,防火墙的作用是防止火灾蔓延,把火
隔离在一个地方。在轮船中使用分隔式水密能也是同样道理,如果船撞上了冰山,那么只有被
撞的水密舱才会破损,而其它舱由于是与它分隔开来的,所以不会受到它的影响,这样就避免
了由一个洞而带来的全船进水的灾难性后果。
信息隐蔽可以帮助在程序中建立防火墙。对另一个子程序的内容知道得越少,对它如何操
作的假设也就越少,而假设越少,其中一个假设出错的可能性就会越小。
松散的耦合也是在程序内部修建防火墙的手段之一。两个子程序之间的耦合越松散,那么
其中一个子程序中的错误影响到另外一个子程序的机会也越少。相反,如果两个子程序联系得
非常紧密,那么一个子程序错误很可能会影响另外一个子程序。
在程序中建防火墙的最好办法是把某些接口标识成“安全”区边界。对穿越安全区边界的
数据进行合法性检查,如果是非法的数据,就要作出合理的反应。基于这种想法的另一种技术
是手术室技术,在数据被允许进入手术室之前,要对其进行消毒处理,手术室中的一切都认为
是无毒安全的。当然,这个手术室是指一段代码,这样,在设计中,要作出的一个关键决定,
就是在这个“手术室”中进入些什么?哪些要被放在它外面?应该把门放在哪儿?应该把哪些
子程序放在安全区内,哪些放在外面?用什么对数据进行消毒?最简单的办法是在外部数据进
入时对其进行消毒,但是,数据往往需要在几个层次上进行消毒,因此,有时需要进行多重消
毒。
5.6.8  检查函数返回值
如果调用了一个函数,并且可以忽略函数退回值(例如,在C 语言中,甚至不需要知道函
数是否返回一个值),千万不要忽略这个返回值。要对这个值进行检查。如果不想让它出错的话,
一定要对其进行检查。防错性设计的核心就是防止常错误。
对于系统函数来说,这个原则也是适用的,除非在结构设计中制定了不检查系统调用的返
回码,而是在每次调用后检查错误代码。如果发现错误,C 语言中的Perror(),或其它语言中
等效的部分,能同时也查出错误个数和这个错误的说明。
5.6.9 在最终软件中保留多少防错性编程
防错性编程带来的矛盾是,在开发过程中,你希望出现错误时越引人注意越好,惹人讨厌
总比冒险忽视它好得多。但在最终产品中,你却希望它越不显眼越好,程序运行不管成功与否,
都要看起来十分优雅。以下是帮助你决定在最终软件中应该保留哪些防错性编程的一些原则:
保留查找在要错误的代码。首先要确定哪些域可以承受本测试到的错误,而哪些域不能。
例如,你正在编写一个表格程序,程序中的屏幕更新区就可以承受末测试到的错误,因为发生
这种情况的后果,不过是一个混乱的屏幕而已,而在计算部分,就不能发生这种情况。因为这
会在表格中产生难以察觉的错误。绝大多数用户都宁愿忍受一个混乱的屏幕而不是错误的表格。
去掉那些无关紧要错误的代码。如果一个错误的后果是无关紧要的,那就去掉检查它的代
码。在上例中,你很可能会去掉检查屏幕更新区错误的子程序。“去掉”并不是指从物理上把这
TinKingTZW - 2006-8-21 17:54:00
段代码删掉,它指的是版本控制预编译开关,或其它不编译那段特定代码的技术。如果不存在
空间限制问题,你也可以保留这段查错代码,并让它向一个错误记录文件隐蔽地传送信息。
去掉那些引起程序终止的代码。在开发阶段,程序发现了一个错误时,你会希望这个错误
越引人注意越好,以便你能修复它,通常,达到这一目的最好办法是让一个程序在发现错误时
打印出错误信息然后终止。即使对于微小错误来说,这样做也是很有用的。
而在最终软件中,在程序终止前,用户总是希望有机会将其工作存盘。他们往往愿为达到
这一目的而忍受一些反常现象,用户们不会感激那些使其工作付诸东流的东西,不管这些东西
在调试阶段多么有用,哪怕最终极大提高了软件质量。如果程序中含有会使干百万数据丢失的
调试代码,那么在最终产品中应将其去除掉。
保留那些可以使程序延缓终止的代码。同时,那些相反的代码也应该保留。如果程序中含
有测试潜在致命错误的信息,那么用户会为能在它们最终发展起来之前将自己的工作有盘而感
到高兴。我所使用的文字处理机在溢出内存之前会亮起“SAVE”提示灯进行警告,当发现这一
情况后就立即存盘并退出。当重新启动程序时,一切又变得正常了。从理论上说,程序不应该
溢出内存,而且,在用同一台机器重新启动程序运行同一文件时,它也不应该用更多内存。产
生了内存溢出问题说明程序有缺欠,但是,程序员想得很周到,在程序中保留了内存检查代码,
我也宁愿得到一个警告信息,而不愿失去我前面所做的工作。
保证留在程序中的错误提示信息是友好的。如果在程序中保留了内部错误提示信息,要确
保它是用友好的语言表达。在我早期编程工作中,我曾经收到一个用户的电话,说她在屏幕上
看到了这样的信息“你的指针地址有错,笨蛋!”幸亏她还有一些幽默感,这对我来说是很幸运
的。通常的办法是通知用户存在“内部错误”,并告诉用户一个她可以投诉的电话号码。
要对防错性编程提高警惕。过多的防错性编程会带来它自身的问题,如果你对每一种可以
察觉的参数传递,在每一个可以察觉的地方都进行检查,那么程序将变得臃肿而笨拙。更糟的
是,附加的用于防错性编程的代码本身并非是完善无缺的,同其它代码一样,你也会在其中发
现错误,而且,如果你是随意写它的,那么错误也会更多。考虑好需要在哪里预防错误,然后
再使用防错性编程。
                      5.7  子程序参数
子程序间的接口往往是一个程序中最容易出错的部分,由Basili 和Perricone 进行的一项研
究表明,程序中39%的错误都是内部接口错误,即予程序间的通信错误。以下是尽量减少这类
错误的一些准则:
确保实际参数与形式参数匹配。形式参数,即哑参数,是在子程序定义中说明的变量,实
际参数是在调用程序中使用的变量和参数。
常见的错误是在子程序调用时变量类型有误,比如,在需要使用实型数时使用了整型数
(这种情况只在像C 这种弱类型的语言中,才会遇到。例如,在汇编语言或在C语言中未使用
全部编译程序的全部警告时就可能产生这种问题。而在Pascal 中,当变元是单纯输入的时候,
几乎不会产生这个问题)。通常编译程序在把实参传入子程序之前,会把它转为形参的类型。如
果产生这个问题,编译程序通常会产生警告。但是在某些情况下,特别是当变元既用于输入也
用于输出时,你可能由于传递了错误的变元类型而受到惩罚。
TinKingTZW - 2006-8-21 17:54:00
要养成检查参数表中参数变元类型和注意编译程序关于变量类型不匹配警告的习惯。在
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 语言中把被修改的参数放在首位的规定是冲突的。不过我仍然认为上述
排列顺序至少对我来说是十分明智的。但如果你一直接某种特定方式对参数排序,这也是可以
帮助提高程序可读性的。
如果几个子程序今使用了相似的参数,应按照不变的顺序排到这些参数。子程序中参数的
排列顺序可以成为一种助记符,而不停变动的排列,会使得这些参数非常难记。比如,在C 语
言中,fptintf()函数与printf()函数相比,除了多了一个文件作为第一变元之外,两者其余都是一
样的。而函数fputs()与函数puts()相比,也只是前者多了一个文件作为最后变元。这实在是一个
糟糕的区别,因为它使得这些函数的参数的难记程度比实际要高多了。
我们来看一个例子,同样是C语言中的函数,函数Strncpy()是按照目标字符率、源字符
率和字节的最大数目来排列变元的,而函数memcpy()是按同样的顺序来排列变元的,这种相
似性使得两个函数中的参数都非常好记了。
使用所有的多数。如果向某个子程序中传入了一个参数,那就要在其中使用;如果不用它
的话,就把它从子程序接口中去掉。因为出错率是随着未用参数个数的增加而升高的,一项调
查表明,在没有未用参数的子程序中,有46%是完全无错的。而在含有未用参数的子程序中,
TinKingTZW - 2006-8-21 17:55:00
仅有17%到29%是完全正确的(Card,Church,Agresti 1986)
不过,这个去掉未用参数的规则有两个特例。首先,如果你使用了c 语言中的指什函数或
Pascal 中的变量过程,那可能会有一些子程序拥有完全相同的参数表,而在这其中又可能有几
个子程序没有完全用到这些参数,这是允许的。其次,当你按照某种条件对程序进行部分编译,
可能会使用某些参数编译部分程序。但如果你去掉这部分是正确有效的,那这也是允许的。一
般来说,如果你有充分的理由不使用某一参数的话,那就按照你想的大胆去干吧。但如果理由
不是很充分的话,就要保留这个参数。
把状态和“错误”变量放在最后。根据约定,状态变量和表示程序中有错误的变量应该放
在参数表的最后。这两种变量对于子程序来说是不很重要的。同时又是仅供输出的变量,因此
把它们放在最后是非常明智的。
不要把子程序中的参教当作工作变量。把传入子程序中的参数用作工作变量是非常危险
的。应该使用局部变量来代替它。比如,在下面这个Pascal程序段中,不恰当地使用了InnutVal
这个变量来存放中间运算结果。
Proceoure ffemPe

VAR InputVal : Integer;
OutputVal: Integer;
);
begin
InputVal:=InputVal * CurrentMultiplier( InputVal);
HputVal:= InputVal + CurrentAdder( InputVal)

OutputVal := Inputval;
end
在这个程序段中,对Inputval的使用是错误的,因为在程序到达最后一行时,InnutVal不再
保持它输入时的值。这时它的值是程序中计算结果的值,因此,它的名字被起错了。如果你以
后在更改程序,需要用到InPutVal 的输入值时,那很可能会在InputVal 的值已经改变后还错误
地认为它保留原有值。
该如何解决这个问题呢?给InputVal重新命名吗?恐怕不行。因为假如你将其命名为
WorkingVal 的话,那么这个名称是无法表示出它的值是来自于程序以外这个事实的。你还可以
给它起一个诸如InputvalThatBeComesAWorkinsVal之类荒唐的名字或者干脆称之为X或者Val,
但无论哪一种办法,看起来都不是很好。
一个更好的办法是通过显式使用工作变量来避免将来或现在可能由于上述原因而带来的问
题。下面的这个程序段表明了这项技术:
procedure Sample

VAR InputVal: Inteqer;
OutputVal: Inteqer;
);
TinKingTZW - 2006-8-21 17:55:00
var
WorkingVal: Integer;
begin
WorkingVal := InputVal;
WorkingVal := WorkingVal * CurrentMultipier (WorkingVal );
WorkingVal := WorkingVal + CurrentAdder( WorkingVal );
...
如果需要在这里或其他地方使用输入原始值,它还存在
...
OutputVal := WorkingVal;
end;
通过引入新变量WokingVal,即保留了InputVal的作用,还消除了在错误的时间使用
InputVal 中值的可能性。在Ada 语言中,这项原则是通过编译程序进行强化的。如果你给某个
参数的变量名前缀是in,则不允许在函数中改变这个参数的值。不过,不要利用这个理由来解
释把一个变量很具文学性地命名为WorkingVal,因为这是一个过于模棱两可的名字,我们之所
以这样使用它,仅仅是为了使它在这里的作用清楚一些。
在Fortran 语言中,使用工作变量是一个非常好的习惯。如果在调用于程序参数表中的变量
被调用于程序改动了,那么它在调用程序中的值也将被改变。在任何语言中,把输入值赋给工
作变量的同时都强调了它的来源。它避免了从参数表中来的变量被偶然改变的可能性。
同样的技术也被用于保持全局变量的值。如果你需要为全局变量计算一个新值,那应该在
计算的最后把最终值赋给全局变量,而不要把中间值赋给它。
说明参数的接口假设。如果假定被传入子程序的数据具有某种特性,那么需要对这个假设
作出说明。在子程序中和在调用程序的地方都需要说明这一假设,这绝不是浪费时间。不要等
到写完子程序后再回过头来说明这些假设,因为那时很可能你已经忘记这些假设了。如果能在
代码中放入断言的话,那么其效果要好于说明这些假设。
· 关于参数接口的哪些假设需要作出说明呢?
· 参数是仅供输入的,修改的还是仅供输出的?
· 数值参数的单位(英尺、码、还是米等)。
· 如果没有使用枚举型参数的话,应指出状态参数和错误变量值的意义。
· 预期的取值范围。
· 永远不该出现的某些特定值。
应该把一个子程序中的参数个数限制在7 个左右。7 对于人的理解能力来说是一个富于魔
力的数字。心理学研究表明人类很难一次记住超过7 个方面的信息,这个发现被应用到不计其
数的领域中,因此,如果一个子程序中的参数个数超过7 个,人们就很难记住,这样会更安全
一些。
在实践中,把一个子程序中的参数个数限制在多少,取决于你所用的程序语言是如何处理
复杂数据结构的。如果你所用的是一种支持结构化数据的先进语言,你可以传递一个含有13个
域的数据结构,而把它只看成是一个独立的信息。如果你使用的是一种比较原始落后的语言,
那你就不得不把这个复合数据结构分解成13 个单独参数分别传送。
如果你发现自己总是在传递比较多的变元,则说明程序之间的耦合就有些过于紧密了。这
TinKingTZW - 2006-8-21 17:55:00
时应重新设计子程序或子程序群,来降低耦合的紧密性。如果你把同一数据传给不同的子程序,
应当把这些子程序组织成一个模块,并把那些经常使用的数据当做模块数据。
考虑建一个关于输入、修改和输出参数的命名约定。如果发现区分输入,修改和输出参数
是非常重要的,则你可以建立一个关于命名的约定,以便区分它们,比如可以用i_,m_,o_作前
缀。要是你不觉得冗长的话,可以用INPUT,MODIFY 和OUTPUT 来作前缀。
仅传递子理序器要的那部分结构化变量。如同在5.4 节关于耦合中讨论过的那样:如果子
程序不是使用结构化变量中绝大部分的话,那么就只传递它所用得到的那一部分。如果你精确
规定了接口,在别的地方再调用这个子程序会容易些。精确的接口可以降低子程序间的耦合程
度,从而提高子程序的使用灵活性。
不过,当我们使用抽象数据类型(ADT)时,这一精确接口规则使不适用了。这种数据类
型要求我们跟踪结构化变量,但这时你最好不要过分注意结构内部,在这种情况下,应把抽象
数据类型子程序设计成将整个记录作为一个参数来接收的,这可以使你把这个记录当成ADT子
程序之外的一个目标,并把这个记录的抽象水平保持在与ADT 子程序的相同高度上,如果你通
过利用其中的每一个域来打开结构,那你就丧失了由ADT 所带来的抽象性。
不要对参数传递作出任何设想。有些程序员总是担心与参数传递有关的内部操作,并绕过
高级语言的参数传递机制,这样做是非常危险的,而且使得程序的可移植性变坏。参数一般是
通过系统堆栈传输的,但这决不是系统传递参数的唯一方式。即使是以堆栈为基础的传递机制,
这些参数的传递顺序也是不同的,而且每一个参数的字长都会有不同程度的改变。如果你直接
与参数打交道,事实上就已经注定了你的程序不可能在另一个机器上运行。
5.8  使用函数
像C、Pascal和Ada等先进的语言,都同时支持函数和过程,函数是返回一个值的子程序,
而过程则是不返回值的子程序。
5.8.1  什么时侯使用函数,什么时侯使用过程
激进者认为函数应该像数学中的函数一样,只返回一个值。这意味着函数应接受唯一的输
入数据并返回一个唯一的值。这种函数总是以它所返回的值来命名的,比如sin(),cos(),
CustomerID()等等,而过程对于输入、修改、输出参数的个数则没有限制。
公用编程法是指把一个函数当作过程来使用,并返回一个状态变量。从逻辑上说,它是一
个过程,但由于它只返回一个值,因此从名义上说,它又是函数。你可能在语句中使用过如下
一个称为Fotmatoutput()的过程:
if (Formatoutput(Input,Formatting,Output) = Success ) then ...
在这个例子中,从它输出参数的角度来看,是一个过程。但是从纯技术角度来说,因为程
序返回一个值,它又是一个函数。这是使用函数的合法方式吗?从保护这个方法的角度出发,
你可以认为这个函数返回一个值与这个子程序的主要目的——格式化输击无关。从这个观点来
看,虽然它名义上是一个函数,但它运行起来更像是过程。如果一贯使用这种技术的话,那么
用返回值来表示这个过程的成功与否并不会使人感到困惑。
一个替换的方案是建立一个用状态变量作为显式参数的子程序,从而产生了如下所示的
TinKingTZW - 2006-8-21 17:55:00
代码段:
    FormatOutput( Input, Formatting, Output, status )
if(Status = Success) then …
    我更赞成使用第二种方法,这倒并不是因为我是个坚持严格区分函数与过程的教条主义者,
而是因为它明确区分了调用和测试状态变量值的子程序。把调用和测试状态值的语句写成一行
增加了语句的代码密度,也增加了其复杂性。以下这种函数用法也是很好的:
Status:=FormatOutput( Input, Formatting, output )
if( status = success ) then...
5.8.2  由函数带来的独特危险
    使用函数产生了可能不恰当值的危险,这常常是函数有几条可能的路径,而其中一条路径
又没有返回一个值时产生的。在建立一函数时,应该在心中执行每一条路径,以确认函数在所
有情况下都可以返回一个值。
5.9  宏子程序
        特殊情况下,用预处理程序宏调用生成子程序。下面的规则和例子仅限于在C 中使用
预处理程序的情况。如果你使用的是其它语言或处理程序,应调整这些规则以适应你的要求:
    把宏指令表达式括在括号中。由于宏指令及其变元被扩展到了代码中,应保证它们是按照
你想要的方式被扩展的。在下面这个宏指令中包含了一种最常见的错误:
                  #define product(a,b)a*b
    这个宏指令的问题是,如果你向其中传了一个非基本数据(无论对a 还是b),它都无法正
确地进行乘法运算。如果你使用这个表达式来算(x+1,x+2),它会把它扩展到x+1*y+2,由于
乘法运算对加法具有优先佳,因此输出结果并不是你想要的结果;一个好一些但并非完美的同
样功能的宏指令如下:
              #define product ( a,b) (a)*(b)
    这一次的情况要稍好些,但还没有完全正确,如果你在preduct()中使用比乘法具有优先
权的因子。这个乘运算还是要被分割开,为防止这一点,可以把整个表达式放人括号:
              #define preduct ( a,b) ((a)*(b))
  用斜线将多重语句宏指令包围起来。一个宏指令可能具有多重语句,如果你把它当作多重
语句来对待的话就会产生问题,以下是一个会产生麻烦的宏指令例子:
#define LookupEntry (Key,Index )\
Index=(key-10)/5;\
Index=min(Index,MAX_INDEX);\
Index=max( Index,MIN_INDEX);

for ( Entrycount=0; Entrycount<NumEntries; Entrycount ++)
TinKingTZW - 2006-8-21 17:55:00
用子程序的命名方法来给扩展为代码的宏命名,以便在必要时用子程序代替它。在C 语言
中给宏命名的规定是应该使用大写字母来命名,如果可以使用子程序来代替它,那么就使用子
程序命名规定来代替C 中的宏命名规定。这样,你只要改变所涉及的子程序,就可以非常容易
地对宏和子程序进行互相替换。
    采纳这种建议也会带来一些危险,如果你一直使用++和一,当你误把宏当作子程序来用
时就会产生副作用,考虑到副作用带来的问题。如果你采纳这个建议避免副作用的话,你就可
以干得更好。
5.8.2  检查表
高质量的子程序
    总体问题
    ·  创建子程序的理由充分吗?
    ·  如果把一个子程序中的某些部分独立成另一个子程序会更好的话,你这样做了吗?
    ·  是否用了明显而清楚的动宾词组对过程进行命名?是否是用返回值的描述来命名函
数?
    ·  子程序的名称是否描述了它做的所有工作?
    ·  子程序的内聚性是不是很强的功能内聚性?它只做一件工作并做得很好吗?
    ·  子程序的耦合是不是松散的?两个子程序之间的联系是不是小规模、密切、可见和灵
活的?
    ·  子程序的长度是不是它的功能和逻辑自然地决定的:而不是由人为标准决定的?
    防错性编程
    ·  断言是否用于验证假设?
    ·  子程序对于非法输入数据进行防护了吗?
    ·  子程序是否能很好地进行程序终止?
    ·  子程序是否能很好地处理修改情况?
    ·  是否不用很麻烦地启用或去掉调试帮助?
    ·  是否信息隐蔽、松散耦合,以及使用“防火墙”数据检查,以使得它不影响子程序之
外的代码?
TinKingTZW - 2006-8-21 17:56:00
·  子程序是否检查返回值?
    ·  产品代码中的防错性代码是否帮助用户,而不是程序员?
    参数传递问题
    ·  形式参数与实际参数匹配吗?
    ·  子程序中参数的排列合理吗?与相似子程序中的参数排列顺序匹配吗?
    ·  接口假设说明了吗?
    ·  子程序中参数个数是不是7 个或者更少,
    ·  是否只传递了结构化变量中另一个子程序用得到的部分?
    ·  是否用到了每一个输入参数?
    ·  是否用到了每一个输出参数?
    ·  如果子程序是一函数,是否在所有情况下它都会返回一个值?
5.10  小  结
    ·  建立子程序的最重要原因是加强可管理性(即降低复杂性),其它原因还有节省空间、
改进正确性、可靠性、可修改性等等。
    ·  强调强内聚性和松散耦合的首要原因是它们提供了较高层次的抽象性,你可以认为一
个具备这种特性的子程序运行是独立的,这可以使你集中精力完成其它任务。
    ·  有些情况下,放入子程序而带来巨大收益的操作可能是非常简单的。
    ·  子程序的名称表明了它的质量,如果名称不好但却是精确的,那么说明它的设计也是
非常令人遗憾的。如果一个子程序的名称既不好又不精确,那它根本就无法告诉你程
序作了些什么。无论哪种情况,都说明程序需要改进。
·  防错性编程可以使错误更容易被发现和修复,对最终软件的危害性显著减小。
一条小小虫 - 2006-8-21 19:03:00
好,真的很好,我会好好学习的,请问楼主是什么职业啊?
TinKingTZW - 2006-8-22 18:19:00
学生
TinKingTZW - 2006-8-22 19:02:00
第六章  模块化设计
目录
    6.1  模块化:内聚性与耦合性
    6.2  信息隐蔽
    6.3  建立模块的理由
    6.4  任何语言中实现模块
    6.5  小结
相关章节
    高质量子程序的特点:见第5 章
    高层次设计:见第7 章
    抽象数据类型:见第12.3 节
   
  “你已经把你的子程序放入我的模块中”
    “不,你已经围绕着我的子程序设计好了模块”
    人们对于子程序和模块之间的区别往往不很注意,但事实上应该充分了解它们之间的区别,
以便尽可能地利用模块所带来的便利。
  “Routine”和“Modu1e”这两个单词的意义是很灵活的,在不同的环境下,它们之间的区
别可能会变化很大。在本书中,子程序是具有一定功能的,可以调用的函数或过程,关于这一
点在第五章已经论述过了。
    而模块则是指数据及作用于数据的子程序的集合。模块也可能是指,可以提供一系列互相
联系功能的子程序集合,而这些子程序之间不一定有公共的数据。模块的例子有:C 语言中的
源文件,某些Pascal版本中的单元及Ada语言中的“包”等等。如果你所使用的语言不直接支
持模块,那么可以通过用分别编程技术来模仿它,这也可以得到许多由模块带来的优点。
6.1  模块化:内聚性与耦合性
    “模块化”同时涉及到子程序设计和模块设计。这是一种值得研究的,非常有用的思想方
法。
    在1981 年出版的《Software Maintenance Guidebook》一书中,Glass和Noiseux 认为模
块化给维护性带来的好处要比给结构带来的好处多得多,它是提高维护性的最重要因素。Lientz
和Swanson 在《Software Maintenance Management》一书中引用的一项研究表明,89%的代码使
用者认为使用模块化编程改进了维护性(1980)。在一次理解测验中发现,采用模块化设计程序
的可读性要比不采用这种设计的程序可读性高15%(1979)。
    模块化设计的目标是使每个子程序都成为一个“黑盒子”,你知道进入盒子和从盒子里出来
的是什么,却不知道里边发生什么。它的接口非常简单,功能明确,对任何一个特定的输入,
TinKingTZW - 2006-8-22 19:02:00
你都可以精确地预测它相应的输出结果。如果你的子程序像一个黑盒子,那么它将是高度模块
化的,其功能明确,接口简单,使用也灵活。
    使用单独一个子程序是很难达到这一目的的,这也正是引入模块的原因。一组子程序常常
要使用一套公用的数据,在这种情况下,由于子程序间要共享数据,因而它们不是高度模块化
的,作为一个单个的子程序,它们的接口也不简单。但是,作为一个整体,这组子程序则完全
有可能为程序的其它部分提供一个简单的接口,也完全有可能达到高度模块化这一目标。
6.1.1  模块内聚性
    模块的内聚性准则,与单个子程序的内聚性准则一样,都是十分简单的。一个模块应该提
供一组相互联系的服务。
    比如一个进行驾驶控制模拟的模块,其中应含有描述汽车目前的控制设置和目前速度的数
据。它可以提供像设定速度、恢复到刚才的速度、刹车等功能。在其内部,可能还有附加的子
程序和数据来支持这些功能,但是,模块外的子程序则不需对它们有任何了解。如果这样的话,
那么这个模块的内聚性将是非常强的,因为模块中的每个子程序都是为提供驾驶控制模拟服务
的。
    再比如一个进行三角函数计算的子程序,模块中可能含有Sin()、Cos()、Tan()、Arcsin()等
全部密切相关的三角函数子程序。如果这些子程序都是标准的三角函数,那么它们无须共享数
据,但这些子程序间仍然是有联系的,因此这个模块的内聚性仍然是非常强的。
    下面是一个内聚性不好的模块例子,设想一个模块中含有几个子程序为实现一个堆栈:
init_stack()、push()和pop();模块中同时还含有格式化报告数据和定义子程序中用到的所有全局
数据的子程序。很难看出堆栈与报告子程序或全局数据部分有什么联系,因此模块的内聚性是
很差的。这些子程序应该按照模块中心的原则进行重新组织。
    在上例中,对模块内聚性的估计是以模块数据和功能为基础进行的。它是在把模块作为一
个整体的层次上进行的。因而,模块中的子程序并不会因为模块内聚性好而一定具有良好的内
聚性。所以模块中的每个子程序设计,也要以保证良好内聚性为准则。关于这方面的问题,见
5.3 节“强内聚性”。
6.1.2  模块耦合
    模块与程序其它部分间的耦合标准与子程序间的耦合标准也是类似的。模块应被设计成可
以提供一整套功能,以便程序的其它部分与它清楚地相互作用。
    在上述的驾驶控制例子中,模块担任了如下功能:SetSpeed()、GetCurrentSettings()、
ResumeFormerSpeed()和Deactivate()。这是一套完整的功能,因而程序的其它部分与它的相互作
用完全是通过规定的公用接口进行的。
如果模块所提供的功能是不完善的,其它子程序可能被迫对其内部数据进行读写操作。这
就打开了黑盒子盖而使其成为透明的了,这实际上破坏了模块化。结构化设计的先驱Larry
Constantine指出,模块提供的功能必须是完整的,以便它的调用者们可以各取所需。
为了设计出强内聚而又松散耦合的模块,必须在设计模块和设计单个子程序的标准之间进
行平衡与折衷。降低子程序之间耦合性的重要措施之一,就是尽可能减少使用全局变量。而创
建模块的原因之一则是为了让子程序可以共享数据;你若想使同一模块中的子程序不必通过参
TinKingTZW - 2006-8-22 19:03:00
数表进行传递,可以采用对其中所有数据进行直接存取来实现。
    从所有模块中的子程序可以对它进行存取的角度来说,模块中数据很像是全局数据。但从
不是程序中所有的子程序都可以对它进行存取的角度来说,它又不像是全局数据,它只对模块
中的子程序来说,才是可以存取的。因此,在模块设计中的最重要决定之一,便是决定哪个子
程序需要对模块中数据进行直接存取。如果某个子程序仅仅是由于可以对模块中数据进行存取
的原因才留在模块中的,那么,它应该被从模块中去掉。
6.2  信息隐蔽
      如果你阅读了书中所有推荐参阅文献的注释,你就会发现其中有400 多个是关于信息隐
蔽的。拥有这么多参考文献的内容一定是非常重要的吧?是的,它的确非常重要。
    进行信息隐蔽的设计思想贯穿了软件开发的每一个层次,从使用命名的常量而不是使用自
由常量到子程序设计、模块设计和整个程序设计。由于这一思想往往是在模块这一层次得到最
充分体现的。因此,我们在本章详细讨论它。
    信息隐蔽是为数不多的几个在实践中无可辩驳地证明了自己价值的理论之一(Boehm
1987)。研究发现,应用信息隐蔽进行设计的大型程序容易更改指数要比没采用这一技术的高4
倍。同时,信息隐蔽也是结构化设计和面向对象设什的基础之一。在结构化设计中,黑盒子思
想便来源于信息隐蔽。在面向对象设计中,也是信息隐蔽引发了抽象化和封装化的设计思想。
6.2.1  保密
    信息隐蔽中的关键概念是“保密”。每一个模块的最大特点都是通过设计和实现,使它对其
它模块保密。这个秘密或许是可能被改动的区域、某个文件的格式化、一个数据结构的实现方
式、或是一个需要与程序其它部分隔离开来,以便其中的错误产生的危害最小的区域。模块的
作用是将自己的信息隐蔽起来以保卫自己的隐私权。信息隐蔽的另一个称谓是“封装”,其意思
是一个外表与内容不一样的盒子。
    无论管它叫什么,信息隐蔽都是设计子程序和模块的一种方法,它对模块的意义更重要些。
当你隐藏秘密时,你就设计了一组存取同一套数据的子程序。对一个系统的改动可能涉及到几
个子程序,但是,它只应涉及一个模块。
    在设计模块时,一项重要任务就是决定哪些特性应该是对模块外部公开的,哪些应该是作
为秘密隐藏起来的,一个模块中可能使用25 个子程序而只暴露出其中的5 个,其余20 个都只
在内部使用。模块中也可能用到了几个数据结构,但却把它们全部隐藏起来。它可能会也可能
不会提供把数据结构信息通知给程序其余部分的子程序。模块设计的这一方面一般被称作“可
见性”,因为它主要涉及了模块的功能特性是否是对外部分开或暴露的。
模块的接口应该尽可能少地暴露它的内部内容。一个模块应该像是一座冰山,你只看到它
的一角,而它其余7/8 的部分则藏在水面下。
与设计的其它方面一样,设计模块的接口也是一个逐渐的过程,如果接口在第一次是不正
确的,可以再试几次直到它稳定下来;如果它稳定不下来,那么就需要重新设计它。
可以用各种不同的图形来代表模块。模块表示图的关键是,它应该区分开仅供模块内部使
用的功能和对外开放的功能。这种图形通常称之为“积木图”,是由Erody Boock 在开发Ada
TinKingTZW - 2006-8-22 19:08:00
语言过程中提出来的。图6-1 表示出了一种模块图。


    其中公用部分是矩形块,个别部分如黑盒子那样表示。
信息隐蔽不必暗示出一个系统的形状;系统可能具有分层结构,也可能像图6-2 中所示那
样具有网状结构。

    在网状结构中,你只要规定哪些模块可以与其它模块通信,这种特定的通信是如何进行的,
然后再进行联接的就可以了,如图6-3 所示,积木图也可以用在网状结构中。

                     
图6-1  一个模块中公用和个别部分
图6-2  网状结构系统
图6-3  用包含信息隐蔽思想的符号表示网状系统

附件: 7341152006822190027.bmp
TinKingTZW - 2006-8-22 19:08:00
6.2.2  信息隐蔽举例
几年前我曾写了一个中型系统(有20K行代码),在其中广泛使用了链表结构。问题域是由
数据结点构成的,每一个结点又与亚坐标、实坐标和等同点相联接。由于我选用了链表结构,
因此在程序中到处都是类似这样的语句:
node = node.next
and
phone = node.data.phone
这些语句直接对链表数据结构进行操作。尽管链表非常自然地将问题进行了模块化,但是
这种方法对内存的使用效率却非常低,于是我想使用整型数组索引来代替内存指针,因为这样
可以提高内存利用率,并且为在其它区域进行性能优化创造机会。但是,由于刚才提到的那种
编码语句充满了程序,因而修改工作非常困难。因为我无法在20000 多行代码中把它们一一找
出来。如果当初我采用了含有如下存取子程序的模块的话,我只要在一个地方即存取子程序中
改动代码就可能了。
      node = NearestNeighbor(node)
phone = EmergencyContact(node)
    我到底赢得了多少内存?我将会赢得或者失去多少速度,我不知道,但是如果当初我隐含
了数据结构的细节并且使用了存取子程序,我就可以很容易地找到答案。而且,我还可以尝试
一下另外几种方法。我本来可以从许多方案中挑选一个最好的,可是,由于我把数据结构的细
节暴露给了整个程序,我不得不使用我所厌恶的方案。
    除了方便修改,隐含复杂数据结构细节的另一个重要原因是:隐含细节可以澄清你编写某
段代码的意图。在上例中,一个富有经验的程序员不难读懂下面这条语句的:
      node = node.next
    显然,这个语句指的是一个链表结构,但除此之外,它什么也不能告诉你。然而,一个像
node = NearestNeighbour(node)这样的存取子程序,则清楚描述了链表所代表的内容,因而这是
很有用的,并且提醒你应该对node 这个名称进行改进(node 与其邻居有什么关系),node =
node.text这样的语句与实际相脱离,你根本无法想到应该改进它们的名称以说明实际问题。
    隐含数据结构的最后一个原因是出于对可靠性的考虑。如果你用一个专门的子程序来存取
数据结构,你只需在其中设置一个安全验证就可以了。否则,你就不得不在所有这个子程序访
问变量的地方设置安全验证。比如,如果你使用了链表,并且想读取链表中的下一个元素,并
且要注意不超过链表的最后一个元素,你可能用如下的代码:
        if ( node.text<>null ) then
node = node.text
    如果在某种情形下,为了更谨慎一些,你可以使用如下代码:
      if ( node<>null ) then
if ( node.next<>null ) then
TinKingTZW - 2006-8-22 19:09:00
node = node.text
    如果你的程序中充斥着node=node.next 这样的语句,你可能要在其中某些地方进行测试而
跳过其余部分。但是,如果这个操作是独立在一个子程序调用中:
      node=NearestNeighbor(node)
    那么,你只要在子程序中一个地方进行测试,那么这一测试就会在整个程序中都起作用。
如果有时你想在整个程序中都对使用node 的地方进行测试,也很容易漏掉其中某些地方。然而,
如果你把这一操作独立在一个子程序中,那么是不可能有遗漏的,因为此时这项工作完全是自
动进行的。
    隐含数据结构细节的另一个好处是容易调试,比如,你发现node值在某处变得有问题了,
但却不知道是在哪里。如果存取node 的代码充斥了整个程序的话,那么找到出问题的地方不亚
于大海捞针。但如果它是被孤立在一个子程序中的话,那么你可以在其中加入一段检查node
的调试码,从而在每次存取它时都进行测试,这样事情就解决了。
应用存取子程序最后一个优点是,可以使所有对数据的存取所遵循的是一种平行的组织形
式;或者通过存取子程序、或者直接对数据进行存取,不会两者兼而有之。当然,在负责数据
的模块内部,对数据的存取都是直接的,在这种情况下失去平行性是不可避免的,这样做的目
的是不在公共场合吹脏肥皂泡。这常常伴随着对在存取程序中进行直接数据操作这一拙劣设计
的隐含。
6.2.3  常见需要隐含的信息
    在你所从事的项目中,你可能与不计其数需要隐含的信息打交道,但是,其中只有几种是
你要反复遇到的:
· 容易被改动的区域
· 复杂的数据
· 复杂的逻辑
· 在编程语言层次上的操作
以上每一项都将在下面的部分中给予详细论述。
    容易被改动的区域
容易改动是好的程序设计中一项最富于挑战性的工作。目的是将不稳定的区域孤立起来,
以便使改动带来的影响仅限于一个模块中。以下是你在为应付改动的工作中要遵循的步骤。
    1. 识别出那些可能被改动的地方。如果分析工作做得很好的话,其中应该附有可能改动
的地方和改动内容的明细表。在这种情况下,找出可能的改动是非常容易的。如果需
求分析中没有进行这项工作,可以参阅下面关于在任何项目中都可能被改动的区域的
讨论。
    2. 把可能被改动的地方分离出来。把第一步中发现的每一个可能改动的地方分隔到自己
的模块中,或者将其与其它可能一起被改动的要素一起,独立到一个模块中。
    3. 独立可能被改动的地方。应把模块间的接口设计成对可能变动不敏感,同时,接口应
该把变动限制在模块内部,外部不会受到内部变动影响。而其它调用这个被改动过模
块的模块,不应感受到这个模块被修改过。模块的接口应该能保护模块的隐私权。
TinKingTZW - 2006-8-22 19:09:00
以下是一些可能变动的区域:
对硬件有依赖的地方。对于监视器、打印机、绘图机等,要清楚在尺寸、颜色、控制代码、
图形能力及内存等方面可能的变化。其余对硬件有依赖性的方面包括与磁盘、磁带、通讯口、
声音器件等接口的变化等等。
输入和输出。在比原始的硬件接口稍高一些的设计层次上,输入/输出是另外一个反复无常
的区域。如果某一应用产生它自己的数据文件,那么当这一应用变得复杂起来时,文件的格式
可能也要变化。用户层次上的输入和输出格式也有可能变化,比如,在打印纸上边界的位置、
每页上边界的数量、域的排列顺序等等。总之,检查所有的外部接口以寻找可能的变化是个好
主意。
非标准语言特性。如果在程序中使用了非标准扩展,应该把这些扩展隐含在一个模块中,
以便当运行环境变化时你可以很容易地替换它。同样,如果你使用了不是在所有环境下都存在
的库子程序,应该把实际的库子程序放在另一个环境下也可以使用的接口后面。
难于设计和实现的域。最好把难于设计和实现的域隐含起来,因为此处的工作可能作得很
糟,你可能不得不返工。把它们分隔起来,以便使由于拙劣设计或实现对系统所带来的危害最
小。
状态变量。状态变量指示程序的状态,往往比其它数据更容易被改动。在典型的情形下,
你可能最初把某一错误状态变量定义成逻辑变量。但后来又发现如果把它赋成具有 NoError,
WarningError和FatalError三个值的枚举型变量来实现会更好。
你至少可以在使用状态变量时,加上两个层次的灵活性和可读性。
·  不要使用逻辑型变量作为状态变量,应使用枚举型变量。赋予状态变量一种新状态是
非常常见的,给枚举型变量赋一个新的类型只需要重新编译一次,而对于逻辑型变量
则需要重新编写每行检查状态变量的代码,谁难谁易是很明显的。
·  使用存取子程序检查变量,而不要对其直接检查,通过检查存取子程序而不是状态变
量,可以进行更复杂的状态测试。例如,如果想检查一个错误状态变量和一个当前函
数状态变量,那么把测试隐含在子程序中来进行,要比用充斥着程序的复杂代码进行
测试容易得多。
数据规模限制。如果你说明一个数组中含有 15 个元素,那么你就把系统不需要的信息暴
露给了它。应该保护其隐私权,信息隐蔽并不总是意味着把一系列功能装入模块这类复杂的工
作,有时,它简单到就是用一个像MAX_EMPLOYEES 之类的常量来代替15,以便隐含它。
商业规则。商业规则指法律、政策、规定、惯例等编入一个计算机系统中的东西。如果你
在编写一个工资发放系统,你可能把IRS 关于允许的扣留数和估计税率等规则编入程序。其余
附加的规则是由工会规定的关于加班率、节假日付酬等方面的规定。如果你正在编写一个引用
保险率的软件,其规定来源于州关于信誉、实际保险率等的管理规定。
这些规定往往是数据处理系统中频繁变动的部分。因为国会可能修改法律,保险公司会调
整保险率。如果你遵从信息隐蔽原则,那么当规则变动时,建立在这些规则上的逻辑关系不会
完全垮掉。这些逻辑关系会隐藏在系统中唯一一个阴暗角落里,直到需对其作出改动为止。
预防到改动。当考虑一个系统中潜在的改动时,应该按照使得改动范围或大小与其改动可
能性成反比的原则来设计系统。如果改动很可能发生,要确保系统可以容易地容纳这一特征。
只有极其不可能发生的变动,才应该被允许在变动时,会影响到系统中一个以上的模块。
TinKingTZW - 2006-8-22 19:09:00
一个寻找可能发生变动域的技术是,首先分析程序中可能会被用户用到的最小的子单元,
这些子单元组成了程序的核心,而且很可能被改变。其次,规定对系统的最小增值。它们可以
小到看起来完全是琐碎的程度。这些潜在改进域组成了对系统的潜在改进。应使用信息隐蔽原
则对这些域进行设计。首先分析核心,可以发现哪些要素事实上是后加上的,从而从那里推测
并隐含改进。
复杂的数据
    所有的复杂数据都很可能被改动;如果它很复杂而对它使用得又很多,那么在实现层次
上与其打过交道后,可能会发现实现它的更好方式。如果应用信息隐蔽来隐含数据实现,就
可以付出较少的努力而获得更好的实现方法。如果不是这样,那么你每次与这些数据打交道
时,你可能都会在后悔,如果当初进行了信息隐蔽,改动实现将会是多么容易啊!
对复杂数据的使用程度,主要取决于程序。如果是一个只有几百行代码的小程序,你想
在其中对变量进行直接操作,那就这样干吧,这样可能影响程序,但也可能不会。在担心由
于对数据直接操作而带来的维护问题之前,应首先考虑这个小程序的特点。如果你正在编写
一个大一些的程序或使用了全局数据,那么就该考虑使用存取子程序。
复杂的逻辑
隐含复杂的逻辑可以改善程序的可读性。复杂的逻辑并不总是程序的最主要方面,把它
隐含起来可以使得子程序的活动更清楚。与复杂数据一样,复杂逻辑也是很可能变动的部分。
所以,把程序的其它部分从这种变动里隔离出去是非常有益的。在某些情况下,你可以将所
使用的逻辑种类隐含起来,例如,你可以通过一个大的if语句、case 语句或查表方式来进行
测试。除了这些进行测试的代码外,其余的代码不需要知道这些细节。如果程序中的其余代
码只需要知道结果,那么你就应该仅仅告诉它们结果。
在程序语言层次上的操作
你的程序越是像一个实际问题的解决方案,它就越是不像程序语言结构的组合,那么,
其质量也就越好,应该把过于专业化的信息隐含起来,比如,下面的语句:
  EmployeeID = EmployeeID+1
CurrentEmployee = EmployeeList [ EmployeeID ]
这是一段很不错的程序,但是它是用过于专业化的语言来表达的,应该用较高程度抽象
的语言来进行这个操作:
      CurrentEmployee = NextAvailableEmployee()
    或者用:
      CurrentEmployee = NextAvailableEmployee( EmployeeList, EmployeeID )
通过加入一个隐含了用专业化语言解释正在发生什么的子程序,使得在一个更高的抽象
层次上处理这个问题。这使得你的意图更清楚,而且使得代码更容易理解和改动了。
如果用图表来实现一个排序问题。函数HighestPriorityEvent(),LowestPriorityrEvent()和
NextEvent()是抽象函数,隐含了实现细节;而FrontOfQueue(),BackOfQueue()和NextInQueue()
TinKingTZW - 2006-8-22 19:09:00
并没有隐含多少细节,因为它们提到了实现,暴露了它们该隐藏的秘密。
一般来说,在设计一组在程序语言语句层次上操作数据的子程序时,应该把对数据操作
隐含在子程序组中,这样程序的其余部分就可能在比较抽象的层次上处理问题了。
6.2.4  信息隐蔽的障碍
绝大多数信息隐蔽障碍都是心理上的,它主要来自于在使用其它技术时形成的习惯。但
在某些情况下,信息隐蔽也的确是不可能的,而一些看起来像是隐蔽障碍的东西,但仅仅是
借口而已。
信息过度分散
    信息隐蔽的一个常见障碍是系统中信息过于分散。比如在一个系统中到处分布着常数
100。把100 当作一个常数,降低了引用它的集中程度。如果把信息隐蔽在一个地方会更好,
因为这样它的值将只在一个地方改变。
    另一个信息过于分散的例子是程序中分布着与用户交互的接口。如果需要改变交互方式,
比如,从命令行方式改为格式驱动方式,那么所有的代码事实上都要被改动。因此,最好把用
户交互接口放入一个单独的模块中,这样,你不必影响到整个系统就可以对交互方式进行改动。
    而还有一个例子则是全局数据结构,比如,一个在整个系统中四处被存取的拥有多达1000
个元素的雇员数据数组。如果程序直接使用这个全局数据,那么这个数据结构的实现信息——
它是一个数组且拥有最多1000 个元素——将充斥着整个程序。如果这个程序只通过存取子程
序来使用这个数据结构,那么就只有这个存取子程序才知道这些细节。
交叉依赖
    一个不易察觉的信息隐蔽障碍是交叉依赖。比如模块A 中的某一部分调用了模块B 中的
一个子程序,而模块B 中又有一部分调用了模块A 中的子程序。应避免这种交叉依赖现象。
因为只有在两者都已准备好的情况下,你才能测试其中的一个。当程序被覆盖时,必须使A
和B 同时驻留在内存中,才能避免系统失败。通过寻找两个模块中被其它模块使用的部分,
把这些部分放入新的模块A’和B’中,用模块A 和B 中的其余部分来调用A’和B’,基本上可
以消除这一问题。
误把模块数据当成全局数据
    如果你是个谨慎的程序员,那么信息隐蔽的障碍之一便是误把模块数据当作全局数据而
避免使用它,因为要避免由于使用全局数据而带来的麻烦。但是,如同在6.1 节“模块化:
内聚性与耦合性”中所说的那样,这两种数据是不同的。由于只有在模块中的子程序才可以
对其进行存取,因而由模块数据带来的麻烦要比全局数据小得多。
    如果不使用模块数据,就不会知道了解由模块所带来的巨大收益。如果一个子程序向模
块传递了只有它才能处理的数据的话,那么就不该由模块来承担拥有数据集合并对其进行操
作的罪责。比如,在前面列举的建议利用如下语句来提高抽象程度的例子中:
    CurrentEmployee = NextAvaliableEmployee()
    或使用:
TinKingTZW - 2006-8-22 19:10:00
这两个赋值语句间的区别是:在第一种情形下,NextAvailableEmployee()拥有关于雇员表
和目前表中的入口是哪一个入口的信息,而在第二种情况下,NextAvailableEmployee
(EmployeeList, EmployeeID)只是从向它传递数据的子程序中借用这些信息。当你使用
NextAvailableEmployee()时,为了提供全部的抽象能力,不必担心它所需要的数据,只要记住
它负责自己的问题就可以了。
  全局数据主要会产生两个问题:(1)一个子程序在对其进行操作时并不知道其它子程序也
在对它进行操作;(2)这个子程序知道其它子程序也在对其进行操作,但不知道它们对它干了
什么。而模块数据则不会产生这些问题,因为只有被放在一个单独模块中的有限几个子程序
才被允许对模块数据进行直接存取操作,当一个子程序进行这种操作时,它知道别的子程序
也在进行同样操作,并确切知道这些是哪几个子程序。如果你还不相信的话,试一下,结果
会令你满意的。
误认为会损失性能
信息隐蔽的最后一个障碍是在结构设计和编码两个层次上,都试图避免性能损失。事实
上,在两个层次上你都不必担心这一点。在结构设计层次上,这种担心之所以不必要是因为,
以信息隐蔽为目标进行结构设计,与以性能为目标进行结构设计是不矛盾的,只要你同时考
虑到这两点,那么就可以同时达到这两个目标。更常见的担心是在编码层次上,这种担心主
要是认为间接而不是直接地存取数据结构会带来运行时间上的损失,因为这样做附加了调用
层次。当测试了系统的性能并在瓶颈问题上有所突破时,这种担心是不成熟的。为提高软件
性能做准备的最好手段之一就是模块化设计,这样,在发现了更好的方案之后,不必改变系
统其余部分,就可以对个别子程序进行优化。
6.3    建立模块的理由
    即使不经常使用模块,凭直觉也很可能会对可以放入模块的数据和子程序种类有所了解。
从某种意义来说,模块并不是人们的目标,它只是数据及对数据所进行的操作的集合,并且支
持面向对象的概念——抽象和封装。模块不支持继承性,因而它也并不完全支持面向对象编程,
描述它的这种有限的面向对象特性的词汇是Booch 1991 年提出来的“基于对象”编程。
    以下是一些适合使用模块的域:
  用户接口。可以建立一个模块来把用户接口要素独立起来。这样,不会影响程序其它部分,
你就可以进行改进。在许多情况下,用户接口模块中往往包含有几个模块来进行诸如菜单操作、
窗口管理、系统帮助等。
对硬件有依赖的区域。把对硬件有依赖的区域放入一个或几个模块中。这些区域常见的有:
与屏幕、打印机、绘图机、磁盘驱动器、鼠标等的接口。把这些对硬件有依赖的区域独立起来
可能帮助把程序移植到新环境下运行。设计一个硬件经常变动的程序时,这也是很有帮助的,
可以编写软件表模拟与特定硬件的交互作用,硬件不存在或不稳定时,让接口子程序与这些模
拟软件打交道。然后在硬件稳定时,再让接口子程序与硬件打交道。
输入与输出。把输入/输出封装起来,可以使程序其余部分免受经常变动的文件和报告
格式的影响。把输入/输出放入模块,也使得程序很容易适应输入/输出设备的变动。
  操作系统依赖部分。把对操作系统有依赖的部分放入模块的原因与把对硬件有依赖部分放
入模块的原因是相同的。如果你正在编写一个在Microsoft Windows下运行的软件,为什么要把
它局限于Windows环境下呢?你完全可以把对Windows的调用放在一个Windows接口模块中。
如果以后想把程序移植到Macintosh或者OS/2环境下,你所要做的只是改动一下接口模块而已。
TinKingTZW - 2006-8-22 19:10:00
数据管理。应把数据管理部分放入模块中,让其中的子程序去与那些杂乱的实现细节打交
道。而让模块外的子程序用抽象的方式与数据打交道,这种方式应该尽可能避免实际处理问题,
如果你认为将数据管理模块化是将其放入一个单独模块中,那你就错了。通常,每一种主要的
抽象数据类型,都需要一个单独的模块来管理。
  真实目标与抽象数据类型。在程序中,需要为每个真实目标创立一个模块。把这一目标所
需要的数据放入模块中,然后再在其中建立对目标进行模块化的子程序。这就是所谓抽象数据
类型。
  可再使用的代码。应把计划在其它程序中再用的程序部分进行模块化。建立模块的一个优
点是,重新使用模块要比重新使用面向功能的程序实用得多。在面向对象设计和面向功能设计
方法中,刚开始的项目都不能从以前的项目中借用许多代码,因为以前项目还不够多,无法提
供足够的代码基础。使用面向功能设计方法开发的程序,大约可以从以前的项目中借用35%的
代码:而在使用面向对象设计方法开发的项目中,则大约可以从以前的项目中借用70%的代码。
如果可以通过深思远虑而在以后的项目中避免重写70%的代码,那为什么不这样做呢?
  可能发生变动的相互联系的操作。应该在那些可能发生变动的操作周围修建一道隔墙。这
事实上是容错原则的一种,因为这样可以避免局部的变动影响到程序的其余部分。在6.2 节中,
给出了一些经常发生变动的区域。
  互相联系的操作。最后,应把互相联系的操作放到一起。在绝大多数情况下,都可以发现
把看起来互相联系的子程序和数据放在一起的更强的组织原则。在无法隐蔽信息的情况下,比
如共享数据或计划增强灵活性时,仍然可以把成组操作放在一起,比如,三角函数、统计函数、
字符串操作子程序、图像子程序等。通过精心地成组放置相关操作,还可以在下一个项目中重
新使用它。
6.4  任何语言中实现模块
  有些语言直接支持模块化,但有些语言则需要补充一些编程标准才可以。
6.4.1  模块化所需的语言支持
  模块包括数据、数据类型、数据操作以及公共和局部操作的区分等。为了支持模块化,一
种语言必须支持多种模块。如果没有多模块,其它任何要求都是空谈。
  数据需要在三个层次上可以被存取和隐含,在局部,在模块中及在全局中,绝大多数语言
都支持局部数据和全局数据。如果想要使某些数据仅对模块中的子程序才是可以存取的,那么
就要求语言支持模块数据,即只有某些而不是全部子程序都可以存取的数据。
TinKingTZW - 2006-8-22 19:10:00
含在某一特定模块中,而另一些类型应该是对其它模块开放的。模块需要能够对那些可以知道
其它类型的模块进行控制。
    对模块层次上的子程序的要求也与上述相类似。有些子程序应该只有在模块内部才能调
用,而且模块应该对某一子程序是专用的还是公用的可以进行控制。在模块之外,不应该有其
它模块或子程序知道这个模块中存在专用子程序。如果模块设计得很好,那么其它模块或子程
序不应该有任何理由来关心专用子程序的存在。
6.4.2  语言支持慨述
    在下表中,对几种语言支持信息隐蔽的必要结构进行了总结:
  通用Basic 和通用Pascal 不支持多模块,所以被排在支持模块化的前列,Fortran 和
QuickBasic 不能控制支持模块化的数据类型。只有Ada、C++和C 以及Turbo Pascal才允许模块
限制对某一子程序的调用,从而使这个子程序真正是专用的。
  简而言之,除非使用Ada、C、C++或Turbo Pascal,否则,就不得不通过命名或其它约
定来扩充所使用语言的能力,以便模拟使用模块。以下部分简单论述了直接支持模块化的语
言,并且将告诉你在其它语言中怎样模拟使用模块。
    Ada 与Modula-2 支持
    Ada通过“包”的概念来支持模块化。如果用Ada编过程序,那么就已经知道如何建立包
了。Modula-2 通过模块的概念来支持模块化。虽然Modula-2 并不是本书的特性,它对模块化
的支持仍然是非常直接的,以下给出了一个用Modula-2进行排序的例子:
    definition module Events;
export
EVENT,
EventAvailable,
HighestPriorityEvent,
LowestPriorityEvent;
 
数据数据类型源程序
语言多模块
局部模块全局局部模块全局专用模块/全局
Ada · · · · · · · · ·
C · · · · · +  · · ·
C++ · · · · · · · · ·
Fortran77 + · + · -  - - - ·
通用Basic - - - · - - - - ·
通用Pascal · · · · · · · · ·
Turbo Pascal - · - · · - · - ·
QuickBasic · · · - · - - - ·
这些是公共的
123456
查看完整版本: 【转载】软件开发人员必备工具书 【代码大全】