瑞星卡卡安全论坛技术交流区系统软件 【转载】软件开发人员必备工具书 【代码大全】

«4567891011»   8  /  16  页   跳转

【转载】软件开发人员必备工具书 【代码大全】

仅有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;
);
gototop
 

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 个单独参数分别传送。
如果你发现自己总是在传递比较多的变元,则说明程序之间的耦合就有些过于紧密了。这
gototop
 

时应重新设计子程序或子程序群,来降低耦合的紧密性。如果你把同一数据传给不同的子程序,
应当把这些子程序组织成一个模块,并把那些经常使用的数据当做模块数据。
考虑建一个关于输入、修改和输出参数的命名约定。如果发现区分输入,修改和输出参数
是非常重要的,则你可以建立一个关于命名的约定,以便区分它们,比如可以用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 ...
在这个例子中,从它输出参数的角度来看,是一个过程。但是从纯技术角度来说,因为程
序返回一个值,它又是一个函数。这是使用函数的合法方式吗?从保护这个方法的角度出发,
你可以认为这个函数返回一个值与这个子程序的主要目的——格式化输击无关。从这个观点来
看,虽然它名义上是一个函数,但它运行起来更像是过程。如果一贯使用这种技术的话,那么
用返回值来表示这个过程的成功与否并不会使人感到困惑。
一个替换的方案是建立一个用状态变量作为显式参数的子程序,从而产生了如下所示的
gototop
 

代码段:
    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 ++)
gototop
 

用子程序的命名方法来给扩展为代码的宏命名,以便在必要时用子程序代替它。在C 语言
中给宏命名的规定是应该使用大写字母来命名,如果可以使用子程序来代替它,那么就使用子
程序命名规定来代替C 中的宏命名规定。这样,你只要改变所涉及的子程序,就可以非常容易
地对宏和子程序进行互相替换。
    采纳这种建议也会带来一些危险,如果你一直使用++和一,当你误把宏当作子程序来用
时就会产生副作用,考虑到副作用带来的问题。如果你采纳这个建议避免副作用的话,你就可
以干得更好。
5.8.2  检查表
高质量的子程序
    总体问题
    ·  创建子程序的理由充分吗?
    ·  如果把一个子程序中的某些部分独立成另一个子程序会更好的话,你这样做了吗?
    ·  是否用了明显而清楚的动宾词组对过程进行命名?是否是用返回值的描述来命名函
数?
    ·  子程序的名称是否描述了它做的所有工作?
    ·  子程序的内聚性是不是很强的功能内聚性?它只做一件工作并做得很好吗?
    ·  子程序的耦合是不是松散的?两个子程序之间的联系是不是小规模、密切、可见和灵
活的?
    ·  子程序的长度是不是它的功能和逻辑自然地决定的:而不是由人为标准决定的?
    防错性编程
    ·  断言是否用于验证假设?
    ·  子程序对于非法输入数据进行防护了吗?
    ·  子程序是否能很好地进行程序终止?
    ·  子程序是否能很好地处理修改情况?
    ·  是否不用很麻烦地启用或去掉调试帮助?
    ·  是否信息隐蔽、松散耦合,以及使用“防火墙”数据检查,以使得它不影响子程序之
外的代码?
gototop
 

·  子程序是否检查返回值?
    ·  产品代码中的防错性代码是否帮助用户,而不是程序员?
    参数传递问题
    ·  形式参数与实际参数匹配吗?
    ·  子程序中参数的排列合理吗?与相似子程序中的参数排列顺序匹配吗?
    ·  接口假设说明了吗?
    ·  子程序中参数个数是不是7 个或者更少,
    ·  是否只传递了结构化变量中另一个子程序用得到的部分?
    ·  是否用到了每一个输入参数?
    ·  是否用到了每一个输出参数?
    ·  如果子程序是一函数,是否在所有情况下它都会返回一个值?
5.10  小  结
    ·  建立子程序的最重要原因是加强可管理性(即降低复杂性),其它原因还有节省空间、
改进正确性、可靠性、可修改性等等。
    ·  强调强内聚性和松散耦合的首要原因是它们提供了较高层次的抽象性,你可以认为一
个具备这种特性的子程序运行是独立的,这可以使你集中精力完成其它任务。
    ·  有些情况下,放入子程序而带来巨大收益的操作可能是非常简单的。
    ·  子程序的名称表明了它的质量,如果名称不好但却是精确的,那么说明它的设计也是
非常令人遗憾的。如果一个子程序的名称既不好又不精确,那它根本就无法告诉你程
序作了些什么。无论哪种情况,都说明程序需要改进。
·  防错性编程可以使错误更容易被发现和修复,对最终软件的危害性显著减小。
gototop
 

学生
gototop
 

第六章  模块化设计
目录
    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)。
    模块化设计的目标是使每个子程序都成为一个“黑盒子”,你知道进入盒子和从盒子里出来
的是什么,却不知道里边发生什么。它的接口非常简单,功能明确,对任何一个特定的输入,
gototop
 

你都可以精确地预测它相应的输出结果。如果你的子程序像一个黑盒子,那么它将是高度模块
化的,其功能明确,接口简单,使用也灵活。
    使用单独一个子程序是很难达到这一目的的,这也正是引入模块的原因。一组子程序常常
要使用一套公用的数据,在这种情况下,由于子程序间要共享数据,因而它们不是高度模块化
的,作为一个单个的子程序,它们的接口也不简单。但是,作为一个整体,这组子程序则完全
有可能为程序的其它部分提供一个简单的接口,也完全有可能达到高度模块化这一目标。
6.1.1  模块内聚性
    模块的内聚性准则,与单个子程序的内聚性准则一样,都是十分简单的。一个模块应该提
供一组相互联系的服务。
    比如一个进行驾驶控制模拟的模块,其中应含有描述汽车目前的控制设置和目前速度的数
据。它可以提供像设定速度、恢复到刚才的速度、刹车等功能。在其内部,可能还有附加的子
程序和数据来支持这些功能,但是,模块外的子程序则不需对它们有任何了解。如果这样的话,
那么这个模块的内聚性将是非常强的,因为模块中的每个子程序都是为提供驾驶控制模拟服务
的。
    再比如一个进行三角函数计算的子程序,模块中可能含有Sin()、Cos()、Tan()、Arcsin()等
全部密切相关的三角函数子程序。如果这些子程序都是标准的三角函数,那么它们无须共享数
据,但这些子程序间仍然是有联系的,因此这个模块的内聚性仍然是非常强的。
    下面是一个内聚性不好的模块例子,设想一个模块中含有几个子程序为实现一个堆栈:
init_stack()、push()和pop();模块中同时还含有格式化报告数据和定义子程序中用到的所有全局
数据的子程序。很难看出堆栈与报告子程序或全局数据部分有什么联系,因此模块的内聚性是
很差的。这些子程序应该按照模块中心的原则进行重新组织。
    在上例中,对模块内聚性的估计是以模块数据和功能为基础进行的。它是在把模块作为一
个整体的层次上进行的。因而,模块中的子程序并不会因为模块内聚性好而一定具有良好的内
聚性。所以模块中的每个子程序设计,也要以保证良好内聚性为准则。关于这方面的问题,见
5.3 节“强内聚性”。
6.1.2  模块耦合
    模块与程序其它部分间的耦合标准与子程序间的耦合标准也是类似的。模块应被设计成可
以提供一整套功能,以便程序的其它部分与它清楚地相互作用。
    在上述的驾驶控制例子中,模块担任了如下功能:SetSpeed()、GetCurrentSettings()、
ResumeFormerSpeed()和Deactivate()。这是一套完整的功能,因而程序的其它部分与它的相互作
用完全是通过规定的公用接口进行的。
如果模块所提供的功能是不完善的,其它子程序可能被迫对其内部数据进行读写操作。这
就打开了黑盒子盖而使其成为透明的了,这实际上破坏了模块化。结构化设计的先驱Larry
Constantine指出,模块提供的功能必须是完整的,以便它的调用者们可以各取所需。
为了设计出强内聚而又松散耦合的模块,必须在设计模块和设计单个子程序的标准之间进
行平衡与折衷。降低子程序之间耦合性的重要措施之一,就是尽可能减少使用全局变量。而创
建模块的原因之一则是为了让子程序可以共享数据;你若想使同一模块中的子程序不必通过参
gototop
 

数表进行传递,可以采用对其中所有数据进行直接存取来实现。
    从所有模块中的子程序可以对它进行存取的角度来说,模块中数据很像是全局数据。但从
不是程序中所有的子程序都可以对它进行存取的角度来说,它又不像是全局数据,它只对模块
中的子程序来说,才是可以存取的。因此,在模块设计中的最重要决定之一,便是决定哪个子
程序需要对模块中数据进行直接存取。如果某个子程序仅仅是由于可以对模块中数据进行存取
的原因才留在模块中的,那么,它应该被从模块中去掉。
6.2  信息隐蔽
      如果你阅读了书中所有推荐参阅文献的注释,你就会发现其中有400 多个是关于信息隐
蔽的。拥有这么多参考文献的内容一定是非常重要的吧?是的,它的确非常重要。
    进行信息隐蔽的设计思想贯穿了软件开发的每一个层次,从使用命名的常量而不是使用自
由常量到子程序设计、模块设计和整个程序设计。由于这一思想往往是在模块这一层次得到最
充分体现的。因此,我们在本章详细讨论它。
    信息隐蔽是为数不多的几个在实践中无可辩驳地证明了自己价值的理论之一(Boehm
1987)。研究发现,应用信息隐蔽进行设计的大型程序容易更改指数要比没采用这一技术的高4
倍。同时,信息隐蔽也是结构化设计和面向对象设什的基础之一。在结构化设计中,黑盒子思
想便来源于信息隐蔽。在面向对象设计中,也是信息隐蔽引发了抽象化和封装化的设计思想。
6.2.1  保密
    信息隐蔽中的关键概念是“保密”。每一个模块的最大特点都是通过设计和实现,使它对其
它模块保密。这个秘密或许是可能被改动的区域、某个文件的格式化、一个数据结构的实现方
式、或是一个需要与程序其它部分隔离开来,以便其中的错误产生的危害最小的区域。模块的
作用是将自己的信息隐蔽起来以保卫自己的隐私权。信息隐蔽的另一个称谓是“封装”,其意思
是一个外表与内容不一样的盒子。
    无论管它叫什么,信息隐蔽都是设计子程序和模块的一种方法,它对模块的意义更重要些。
当你隐藏秘密时,你就设计了一组存取同一套数据的子程序。对一个系统的改动可能涉及到几
个子程序,但是,它只应涉及一个模块。
    在设计模块时,一项重要任务就是决定哪些特性应该是对模块外部公开的,哪些应该是作
为秘密隐藏起来的,一个模块中可能使用25 个子程序而只暴露出其中的5 个,其余20 个都只
在内部使用。模块中也可能用到了几个数据结构,但却把它们全部隐藏起来。它可能会也可能
不会提供把数据结构信息通知给程序其余部分的子程序。模块设计的这一方面一般被称作“可
见性”,因为它主要涉及了模块的功能特性是否是对外部分开或暴露的。
模块的接口应该尽可能少地暴露它的内部内容。一个模块应该像是一座冰山,你只看到它
的一角,而它其余7/8 的部分则藏在水面下。
与设计的其它方面一样,设计模块的接口也是一个逐渐的过程,如果接口在第一次是不正
确的,可以再试几次直到它稳定下来;如果它稳定不下来,那么就需要重新设计它。
可以用各种不同的图形来代表模块。模块表示图的关键是,它应该区分开仅供模块内部使
用的功能和对外开放的功能。这种图形通常称之为“积木图”,是由Erody Boock 在开发Ada
gototop
 
«4567891011»   8  /  16  页   跳转
页面顶部
Powered by Discuz!NT