• 沒有找到結果。

生成子程序的原因

在文檔中 Code Complete 代码大全 (頁 59-63)

第五章 高质量子程序特点

5.1 生成子程序的原因

以下是关于为什么要生成子程序的一些合理原因,其中有些原因之间可能有互相重叠的地 方。

降低复杂性。使用子程序的最首要原因是为了降低程序的复杂性,可以使用子程序来隐含 信息,从而使你不必再考虑这些信息。当然,在编写子程序时,你还需要考虑这些信息。但是,

一旦写好子程序,就可能不必再考虑它的内部工作细节,只要调用它就可以了。创建子程序的 另外一个原因是尽量减小代码段的篇幅,改进可维护性和正确性。这也是一个不错的解释,但 若没有子程序的抽象功能,将不可能对复杂程序进行明智的管理。

一个子程序需要从另一个子程序中脱离出来的原因之一是,过多重数的内部循环和条件判 断。这时,可以把这部分循环和判断从子程序中脱离出来,使其成为一个独立的子程序,以降 低原有子程序的复杂性。

避免代码段重复。无可置疑,生成子程序最普遍的原因是为了避免代码段重复。事实上,

如果在两个不同子程序中的代码很相似,这往往意味着分解工作有误。这时,应该把两个子程 序中重复的代码都取出来,把公共代码放入一个新的通用子程序中,然后再让这两个子程序调

用新的通用子程序。通过使公共代码只出现一次,可以节约许多空间。这时改动也很方便,因 为只要在一个地方改动代码就可以了。这时代码也更可靠了,因为只需在一个地方检查代码。

而且,这也使得改动更加可靠,因为,不必进行不断地、非常类似地改动,而这种改动往往又 是认为自己编写了相同的代码这一错误假设下进行的。

限制了改动带来的影响。由于在独立区域进行改动,因此,由此带来的影响也只限于一个 或最多几个区域中。要把最可能改动的区域设计成最容易改动的区域。最可能被改动的区域包 括:硬件依赖部分、输入输出部分、复杂的数据结构和商务规则。

隐含顺序。把处理事件的非特定顺序隐含起来是一个很好的想法。比如,如果程序通常先 从用户那里读取数据,然后再从一个文件中读取辅助数据,那么,无论是读取用户数据的子程 序,还是读取文件中数据的子程序,都不应该对另一个子程序是否读取数据有所依赖。如果利 用两行代码来读取堆栈顶的数据,并减少一个 Stacktop 变量,应把它们放入一个 PopStack()子程 序中,在设计系统时,使哪一个都可以首先执行,然后编写一个子程序,隐含哪一个首先执行 的信息。

改进性能。通过使用子程序,可以只在一个地方,而不是同时几个地方优化代码段。把相 同代码段放在子程序中,可以通过优化这一个子程序而使得其余调用这个子程序的子程序全部 受益。把代码段放入子程序也使得用更快的算法或执行更快的语言(如汇编)来改进这段代码 的工作变得容易些。

进行集中控制。在一个地方对所有任务进行控制是一个很好的想法。控制可能有许多形式。

知道一个表格中的入口数目便是其中一种形式,对硬件系统的控制,如对磁盘、磁带、打印机、

绘图机的控制则是其中另外一种形式。使用子程序从一个文件中进行读操作,而使用另一个子 程序对文件进行写操作便是一种形式的集中控制。当需要把这个文件转化成一个驻留内存的数 据结构时,这一点是非常有用的,因为这一变动仅改变了存取子程序。专门化的子程序去读取 和改变内部数据内容,也是一种集中的控制形式。集中控制的思想与信息隐含是类似的,但是 它有独特的启发能力,因此,值得把它放进你的工具箱中。

隐含数据结构。可以把数据结构的实现细节隐含起来,这样,绝大部分程序都不必担心这 种杂乱的计算机科学结构,而可以从问题域中数据是如何使用的角度来处理数据。隐含实现细 节的子程序可以提供相当高的抽象价值,从而降低程序的复杂程度。这些子程序把数据结构、

操作集中在一个地方,降低了在处理数据结构时出错的可能性。同时,它们也使得在不改变绝 大多数程序的条件下,改变数据结构成为可能。

隐含全局变量。如果需要使用全局变量,也可以像前述那样把它隐含起来、通过存取子程 序来使用全局变量有如下优点:不必改变程序就改变数据结构;监视对数据的访问;使用存取 子程序的约束还可以鼓励你考虑一下这个数据是不是全局的;很可能会把它处理成针对在一个 模块中某几个子程序的局部数据,或处理成某一个抽象数据的一部分。

隐含指针操作。指针操作可读性很差,而且很容易引发错误。通过把它们独立在子程序中,

可以把注意力集中到操作意图而不是机械的指针操作本身。而且,如果操作只在一处进行,也 更容易确保代码是正确的。如果找到了比指针更好的数据结构,可以不影响本应使用指针的子 程序就对程序作改动。

重新使用代码段。放进模块化子程序中的代码段重新使用,要比在一个大型号程序中的代 码段重新使用起来容易得多。

计划开发一个程序族。如果想改进一个程序,最好把将要改动的那部分放进子程序中,将 其独立。这样,就可以改动这个子程序而不致影响程序的其余部分,或者干脆用一个全新的子 程序代替它。几年前,我曾经负责一个替保险推销员编写系列软件的小组,我们不得不根据每 一个推销员的保险率、报价单格式等等来完成一个特定的程序。但这些程序的绝大部分又都是 相同的:输入潜在客户的子程序,客户数据库中存储的信息、查看、计算价格等等。这个小组 对程序进行了模块化,这样,随推销员而变化的部分都放在自己的模块中。最初的程序可能要 用三个月的时间来开发,但是,在此之后,每来一个推销员,我们只改写其中屈指可数的几个 模块就可以了。两三天就可能写完一个要求的程序,这简直是一种享受!

提高部分代码的可读性。把一段代码放入一个精心命名的子程序,是说明其功能的最好办 法。这样就不必阅读这样一段语句:

if ( Node <> nil )

while ( Node.Next <> nil ) do Node = Node.Next LeafName = Node.Name else

LeafName = " "

代替它的是:

LeafName = GetLeafName(Node)

这个程序是如此简短,它所需要的注释仅仅是一个恰当的名字而已。用一个函数调用来代 替一个有六行的代码段,使得含有这段代码的子程序复杂性大为降低,并且其功能也自动得到 了注释。

提高可移植性。可以使用子程序来把不可移植部分、明确性分析和将来的移植性工作分隔 开来,不可移植的部分包括:非标准语言特性、硬件的依赖性和操作系统的依赖性等。

分隔复杂操作。复杂操作包括:繁杂的算法、通信协议、棘手的布尔测试、对复杂数据的 操作等等。这些操作都很容易引发错误。如果真的有错误,那么如果这个错误是在某个子程序 中,而不是隐藏在整个程序中的话,查找起来要容易得多。这个错误不会影响到其它子程序,

因为为了修正错误只要改动一个子程序就可以了。如果发现了一个更为简单迅速的算法,那么 用它来代替一个被独立在子程序中的算法是非常容易的。在开发阶段,尝试几种方案并选择其 中一个最好的是非常容易的。

独立非标准语言函数的使用。绝大多数实现语言都含有一些非标准的但却方便的扩展。使 用这种扩展的影响是两面性的,因为在另外一个环境下它可能无法使用。这个运行环境的差异 可能是由于硬件不同、语言的生产商不同、或者虽然生产商相同、但版本不同而产生的。如果 使用了某种扩展,可以建立一个作为进入这种扩展大门的子程序。然后,在需要时,可以用订 做的扩展来代替这一非标准扩展。

简化复杂的布尔测试。很少有必要为理解程序流程而去理解复杂的布尔测试。把这种测试 放入函数中可以提高代码的可读性,因为:

(1) 测试的细节已经被隐含了。

(2) 清楚的函数名称已经概括了测试目的。

赋予这种测试一个函数,该函数强调了它的意义,而且这也鼓励了在函数内部增强其可读

性的努力。结果是主程序流和测试本身都显得更加清楚了。

是出于模块化的考虑吗?绝不是。有了这么些代码放入子程序的理由,这个理由是不必要 的。事实上,有些工作更适合放在一个大的子程序中完成(关于程序最佳长度的讨论见 5.5 节“子 程序长度”)。

5.1.1 简单而没有写入子程序的操作

编写子程序的最大心理障碍是不情愿为了一个简单的目的而去编写一个简单的子程序。写 一个只有两或三行代码的子程序看起来是完全没有必要的。但经验表明,小的子程序也同样是 很有帮助的。

小型子程序有许多优点,其中之一是改进了可读性。我曾在程序中采用过如下这样一个仅 有一行的代码段,它在程序中出现了十几次:

Points = DeviceUnits * ( POINTS_PER_INCH / DeviceUnitsPerInch ( ) )

这决不是你所读过的最复杂的一行代码。很多人都明白它是用来转换的。他们也会明白程 序中的每行这个代码都在作同一件事,但是,它还可以变得更清楚些,所以,我创建了一个恰

这决不是你所读过的最复杂的一行代码。很多人都明白它是用来转换的。他们也会明白程 序中的每行这个代码都在作同一件事,但是,它还可以变得更清楚些,所以,我创建了一个恰

在文檔中 Code Complete 代码大全 (頁 59-63)