第一篇:优美的程序需要优美的子程序
〖返回〗〖转发〗
我见过那种一个main函数写了1000多行的程序员,而且为数还不在少数。难道说他们不懂得用函数吗?不见得,但可以肯定的一点是他们没有编写优美代码的意识。那么我们为什么需要子程序呢?我们来看看STEVE McCONNELL是怎么说的。
以下是关于为什么要生成于程序的一些合理原因,其中有些原因之间可能有互相重叠的地方。
降低复杂性。使用子程序的最首要原因是为了降低程序的复杂性,可以使用子程序来隐含信息,从而使你不必再考虑这些信息。当然,在编写子程序时,你还需要考虑这些信息。但是,一旦写好子程序,就可能不必再考虑它的内部工作细节,只要调用它就可以了。创建子程序的另外一个原因是尽量减小代码段的篇幅,改进可维护性和正确性。这也是一个不错的解释,但若没有子程序的抽象功能,将不可能对复杂程序进行明智的管理。
一个子程序需要从另一个子程序中脱离出来的原因之一是,过多重数的内部循环和条件判断。这时,可以把这部分循环和判断从子程序中脱离出来,使其成为一个独立的子程序,以降低原有子程序的复杂性。
避免代码段重复。无可置疑,生成子程序最普遍的原因是为了避免代码段重复。事实上,如果在两个不同子程序中的代码很相似,这往往意味着分解工作有误。这时,应该把两个子程序中重复的代码都取出来,把公共代码放入一个新的通用子程序中,然后再让这两个子程序调用新的通用子程序。通过使公共代码只出现一次,可以节约许多空间。这时改动也很方便,因为只要在一个地方改动代码就可以了。这时代码也更可靠了,因为只需在一个地方检查代码。而且,这也使得改动更加可靠,因为,不必进行不断地、非常类似地改动,而这种改动往往又是认为自己编写了相同的代码这一错误假设下进行的。
限制了改动带来的影响。由于在独立区域进行改动,因此,由此带来的影响也只限于一个或最多几个区域中。要把最可能改动的区域设计成最容易改动的区域。最可能被改动的区域包括:硬件依赖部分、输入输出部分、复杂的数据结构和商务规则。
隐含顺序。把处理事件的非特定顺序隐含起来是一个很好的想法。比如,如果程序通常先从用户那里读取数据,然后再从一个文件中读取辅助数据,那么,无论是读取用户数据的子程序,还是读取文件中数据的子程序,都不应该对另一个子程序是否读取数据有所依赖。如果利用两行代码来读取堆栈顶的数据,并减少一个Stacktop变量,应把它们放入一个PopStack()子程序中,在设计系统时,使哪一个都可以首先执行,然后编写一个子程序,隐含哪一个首先执行的信息。
改进性能。通过使用子程序,可以只在一个地方,而不是同时几个地方优化代码段。把相同代码段放在子程序中,可以通过优化这一个子程序而使得其余调用这个子程序的子程序全部受益。把代码段放入子程序也使得用更快的算法或执行更快的语言(如汇编)来改进这段代码的工作变得容易些。
进行集中控制。在一个地方对所有任务进行控制是一个很好的想法。控制可能有许多形式。知道一个表格中的入口数目便是其中一种形式,对硬件系统的控制,如对磁盘、磁带、打印机、绘图机的控制则是其中另外一种形式。使用子程序从一个文件中进行读操作,而使用另一个子程序对文件进行写操作便是一种形式的集中控制。当需要把这个文件转化成一个驻留内存的数据结构时,这一点是非常有用的,因为这一变动仅改变了存取子程序。专门化的子程序去读取和改变内部数据内容,也是一种集中的控制形式。集中控制的思想与信息隐含是类似的,但是它有独特的启发能力,因此,值得把它放进你的工具箱中。
隐含数据结构。可以把数据结构的实现细节隐含起来,这样,绝大部分程序都不必担心这种杂乱的计算机科学结构,而可以从问题域中数据是如何使用的角度来处理数据。隐含实现细节的子程序可以提供相当高的抽象价值,从而降低程序的复杂程度。这些子程序把数据结构、操作集中在一个地方,降低了在处理数据结构时出错的可能性。同时,它们也使得在不改变绝大多数程序的条件下,改变数据结构成为可能。
隐含全局变量。如果需要使用全局变量,也可以像前述那样把它隐含起来、通过存取子程序来使用全局变量有如下优点:不必改变程序就改变数据结构;监视对数据的访问;使用存取子程序的约束还可以鼓励你考虑一下这个数据是不是全局的;很可能会把它处理成针对在一个模块中某几个子程序的局部数据,或处理成某一个抽象数据的一部分。
隐含指针控作。指针操作可读性很差,而且很容易引发错误。通过把它们独立在子程序中,可以把注意力集中到操作意图而不是机械的指针操作本身。而且,如果操作只在一处进行,也更容易确保代码是正确的。如果找到了比指针更好的数据结构,可以不影响本应使用指针的子程序就对程序作改动。
重新使用代码段。放进模块化子程序中的代码段重新使用,要比在一个大型号程序中的代码段重新使用起来容易得多。计划开发一个程序族。如果想改进一个程序,最好把将要改动的那部分放进子程序中,将其独立。这样,就可以改动这个子程序而不致影响程序的其余部分,或者干脆用一个全新的子程序代替它。几年前,我曾经负责一个替保险推销员编写系列软件的小组,我们不得不根据每一个推销员的保险率、报价单格式等等来完成一个特定的程序。但这些程序的绝大部分又都是相同的:输入潜在客户的子程序,客户数据库中存储的信息、查看、计算价格等等。这个小组对程序进行了模块化,这样,随推销员而变化的部分都放在自己的模块中。最初的程序可能要用三个月的时间来开发,但是,在此之后,每来一个推销员,我们只改写其中屈指可数的几个模块就可以了。两三天就可能写完一个要求的程序,这简直是一种享受!
提高部分代码的可读性。把一段代码放入一个精心命名的子程序,是说明其功能的最好办法。这样就不必阅读这样一段语句:
if ( Node <> NULL )
while ( Node.Next <> NULL ) do
Node = Node.Next
LeafName = Node.Name
else
LeafName = " "
代替它的是:
LeafName = GetleafName(Node)
这个程序是如此简短,它所需要的注释仅仅是一个恰当的名字而已。用一个函数调用来代替一个有六行的代码段,使得含有这段代码的子程序复杂性大为降低,并且其功能也自动得到了注释。
提高可移植性。可以使用子程序来把不可移植部分、明确性分析和将来的移植性工作分隔开来,不可移植的部分包括:非标准语言特性、硬件的依赖性和操作系统的依赖性等。
分隔复杂操作。复杂操作包括:繁杂的算法、通信协议、棘手的布尔测试、对复杂数据的操作等等。这些操作都很容易引发错误。如果真的有错误,那么如果这个错误是在某个子程序中,而不是隐藏在整个程序中的话,查找起来要容易得多。这个错误不会影响到其它子程序,因为为了修正错误只要改动一个子程序就可以了。如果发现了一个更为简单迅速的算法,那么用它来代替一个被独立在子程序中的算法是非常容易的。在开发阶段,尝试几种方案并选择其
中一个最好的是非常容易的。
独立非标准语言函数的使用。绝大多数实现语言都含有一些非标准的但却方便的扩展。使用这种扩展的影响是两面性的,因为在另外一个环境下它可能无法使用。这个运行环境的差异可能是由于硬件不同、语言的生产商不同、或者虽然生产商相同、但版本不同而产生的。如果使用了某种扩展,可以建立一个作为进入这种扩展大门的子程序。然后,在需要时,可以用订做的扩展来代替这一非标准扩展。
简化复杂的布尔测试。很少有必要为理解程序流程而去理解复杂的布尔测试。把这种测试放入函数中可以提高代码的可读性,因为:
(1) 测试的细节已经被隐含了。
(2) 清楚的函数名称已经概括了测试目的。
赋予这种测试一个函数,该函数强调了它的意义,而且这也鼓励了在函数内部增强其可读性的努力。结果是主程序流和测试本身都显得更加清楚了。是出于模块化的考虑吗吗?绝不是。有了这么些代码放入子程序的理由,这个理由是不必要的。事实上,有些工作更适合放在一个大的子程序中完成。
看完了这一段对于使用子程序的说明,是不是觉得很神奇,以前从来没有想过使用子程序有如此多的好处。其实总结来说子程序的最大好处就是提高了系统的复用性(reusability)和可读性。这对于现代软件工程来说是至关重要的一点。复用性是什么样一个概念呢,我举一个书上的例子:
在建房子中,你不会去建造那些你可以现成买来的东西,比如洗衣机、烘干机,电冰箱、吸尘器等,除非你是个机械迷。同时,你也会去购买已经做好的地毯、门、窗和浴室用品,而不是自己动手建。如果你正在建造一个软件,你也会这样做。你会推广使用高级语言的特点,而不是去编写操作系统一级的代码。你也会利用已经存在的显示控制和数据库处理系统,利用已经通过的子程序。如果样样都自己动手是很不明智的。
如果你想修建一幢陈设一流的别墅,情况就不同了,你可能定做全套家具,因为希望洗碗机、冰箱等与你的协调一致,同时你还会定做别具风格的门和窗户。这种定做化的方式与一流软件开发也是非常类似的。为了这一目的,你可能创建精度更高、速度更快的科学公式。你也会设计自己的显示控制、数据库处理系统和自己的子程序,以使整个软件给人以一气呵成,天衣无缝的感觉。
了解复用的好处了吗,有人描绘过未来软件的景象,你可以去软件市场上购买你需要的软件模块,然后加以组装就成为你所需要的软件产品了,相信吗?不信吗?我也是,不过也许真会有那么一天。
在这一节里,我们看到了子程序的未来是一片光明,在接下来的的章节中,我们会对子程序的优美程度做一个讨论。