第6章描述了创建类的个中细节,而本章则将转向子程序(routines),关注那些关乎子程序质量好坏的特征。如果你希望在进入细节之前阅读有关能够影响子程序设计的事项,那么请先阅读第5章“软件构建中的设计”,然后再回到本章。第8章“防御式编程”也阐述了高质量的子程序的一些重要属性。如果你对创建子程序和类的步骤更感兴趣,那么从第9章“伪代码编程过程”开始可能会更好。
在讨论高质量的子程序的细节之前,明确下面这两个基本术语会很有帮助。首先,什么是“子程序(routine)”?子程序是为实现一个特定的目的而编写的一个可被调用的方法(method)或过程(procedure)。例如C++中的函数(function),Java中的方法(method),或Microsoft Visual Basic中的函数过程(function procedure)或子过程(sub procedure)。对于某些使用方式,C和C++中的宏(macro)也可认为是子程序。你可以把创建高质量子程序的很多技术应用到所有的这些情况中。
那什么又是高质量的子程序呢?这个问题更难回答。也许回答这个问题的最简单的办法,是来看看什么东西不是高质量的子程序。这里举一个低质量的子程序的例子:
C++示例:低质量的子程序
void HandleStuff( CORP_DATA & inputRec, int crntQtr, EMP_DATA empRec,
double & estimRevenue, double ytdRevenue, int screenX, int screenY,
COLOR_TYPE & newColor, COLOR_TYPE & prevColor, StatusType & status,
int expenseType )
{
int i;
for ( i = 0; i < 100; i++ ) {
inputRec.revenue[i] = 0;
inputRec.expense[i] = corpExpense[ crntQtr ][ i ];
}
UpdateCorpDatabase( empRec );
estimRevenue = ytdRevenue * 4.0 / (double) crntQtr;
newColor = prevColor;
status = SUCCESS;
if ( expenseType == 1 ) {
for ( i = 0; i < 12; i++ )
profit[i] = revenue[i] - expense.type1[i];
}
else if ( expenseType == 2 ) {
profit[i] = revenue[i] - expense.type2[i];
}
else if ( expenseType == 3 )
profit[i] = revenue[i] - expense.type3[i];
}
这个子程序里有哪些不妥呢?给你一个提示:你应该能够从中发现至少10个不同的问题。请你先列出自己发现的问题,然后再来看下面这份清单:
■ 这个子程序有个很差劲的名字。HandleStuff()一点也没有告诉你这个子程序究竟是做什么的。
■ 这个子程序没有文档(有关文档的话题已经超出了子程序的范畴,因此将在第32章“自我注解的代码”讨论)。
■ 这个子程序的布局不好。代码的物理组织形式几乎没有给出任何关于其逻辑组织的提示。布局的使用过于随意,程序内的不同部分使用了不同的布局风格。请比较一下expenseType==2和expenseType==3这两处的代码风格(在第31章“布局与风格”中会讨论布局问题。)
■ 这个子程序的输入变量inputRec的值被改变了。如果它是一个输入变量,它的值就不应该被修改(而且在C++中它应该定义为const)。如果变量的值就是要被修改的,那就不要把它命名为inputRec。
■ 这个子程序读写了全局变量——它从 corpExpense 中读取数值并将其写入profit。它应该更直接地与其他子程序通讯,而不是去读写全局变量。
■ 这个子程序没有一个单一的目的。它初始化了一些变量,向数据库写入数据,又做了一些计算——从这些事情之间看不出任何联系。子程序应该有单一而明确的目的。
■ 这个子程序没有注意防范错误数据(bad data)。如果crntQtr等于0,那么表达式ytdRevenue*4.0/(double)crntQtrh将会导致除零错误。
■ 这个子程序用了若干神密数值(magic number):100、4.0、12、2、3等。神秘数值的问题会在第12.1节“数字概述”中探讨。
■ 这个子程序未使用其中一些参数:screenX和screenY在程序中都没有被引用过。
■ 这个子程序的一个参数传递方式有误:prevColor被标为引用参数(&),但在这个子程序内却未对其赋值。
■ 这个子程序的参数太多了。合理的参数个数,其上限大概是7个左右,而这个子程序有11个。这些参数的排布方式也难以理解,估计没人想仔细研究它们、甚至没人想数数有几个参数。
■ 这个子程序的参数顺序混乱且未经注释。(参数的顺序会在本章探讨。而代码注释问题会在第32章中阐述)
抛开计算机本身,子程序也算得上是计算机科学中一项最为重大的发明了。子程序的使用使得程序变得更加易读,更易于理解,比任何编程语言的任何功能特性都更容易。像上例那样滥用这一计算机科学中最为重要的特性,简直就是一种犯罪。
子程序也是迄今为止发明出来的用以节约空间和提高性能的最重要手段。设想,如果对每个子程序调用都重复写出代码,而不是转到相应的子程序,那么代码会变得多么臃肿。再设想一下,如果要对一段代码的性能进行改善,但这些代码反复出现在不同地方,而不是被纳入了一个子程序,那该有多困难。正是子程序使得现代化的编程成为可能。
“好吧,”你可能会说,“我早就知道子程序很重要,而且我在编程时也一直在用它们。这里的讨论好像是要纠正什么,那么你想让我做些什么呢?”
我希望你能够理解,创建一个子程序可以有很多合理的原因,但完成它的方式却有对错之分。当我还是一名在读的计算机系大学生时,我曾认为创建子程序的主要原因不过是为了避免重复代码。我所用过的那本入门教程也告诉我说,使用子程序的好处就是因为它避免了重复的代码,从而使程序更易于开发、调试、编档和维护等等。除了还讲了一些如何使用参数和局部变量等语法细节之外,这就是那本教程所涵盖的全部内容了。它对子程序理论和实践的解释既不够好,也不够完整。下面的几节将给你一些更好的解释。
Valid Reasons to Create a Routine
创建子程序的正当理由
这里列出的是一些创建子程序的正当理由。有些理由互有重叠,因为本来也未打算让它们形成一个正交的集合。
降低复杂度 创建子程序的一个最重要的原因,就是为了降低程序的复杂度。可以通过创建子程序来隐藏一些信息,这样你就不必再去考虑这些信息了。当然,在你要编写这个子程序的时候肯定是要考虑它们的。不过一旦程序写好了,你就应该能忘记这些细节,可以直接调用该子程序而无须了解其内部工作细节。创建子程序还有其他一些原因——如缩小代码规模、改善可维护性、提高正确性等——也都是很不错的,但如果没有子程序的抽象能力,我们的智力将根本无法管理复杂的程序。
当内部循环或条件判断的嵌套层次很深时,就意味着需要从子程序中提取出新的子程序了。把嵌套的部分提取出来形成一个独立的子程序,可以降低外围子程序的复杂度。
引入中间、易懂的抽象 把一段代码放入一个命名恰当的子程序内,是说明这段代码用意最好的方法之一。与读下面这一串语句相比:
if ( node <> NULL ) then
while ( node.next <> NULL ) do
node = node.next
leafName = node.name
end while
else
leafName = ""
end if
读懂下面这条语句就更容易:
leafName = GetLeafName( node )
这段新程序如此之短,只要给它取个好的名字就足够说明它的用意了。与上面的8行代码相比,这个名字提供了更高层次的抽象,从而使代码更具可读性,也更容易理解,同时也降低了原来包含着上面那段代码的子程序的复杂度。
避免代码重复 毋庸置疑,创建子程序最普遍的原因是为了避免代码重复。事实上,如果在两段子程序内编写了相似的代码,就意味着代码分解(decomposition)出现了差错。这时,应该把两段子程序中的重复代码提取出来,将其中的相同部分放入一个基类,然后再把两段程序中的差异代码放入派生类中。还有另一种办法,你也可以把相同的代码放入新的子程序中,再让其余的代码来调用这个子程序。与代码的重复出现相比,让相同的代码只出现一次可以节约空间。代码改动起来也更方便,因为你只需要在一处修改即可。这时的代码也会更加可靠,因为为了验证代码的正确性,你只需要检查一处代码。同时,这样做也会使改动更加可靠,因为你可以避免需要做相同的修改时,却做了一些略有不同的修改。
支持子类化(subclassing) 覆盖(override)简短而规整的子程序所需新代码的数量,要比覆盖冗长而邋遢的子程序更少。如果你能让可覆盖的子程序保持简单,那你在实现派生类的时候也会减少犯错的几率。
隐藏顺序 把处理事件的顺序隐藏起来是一个好主意。比如,如果一个程序通常都是先从用户那里读取数据,然后再从一个文件中读取辅助数据,那么,无论是从用户那里读取数据的子程序还是从文件中读取数据的子程序,都不应该依赖另一个子程序是否已执行。再举一个有关顺序的例子,假设你写了两行代码,先读取栈顶的数据,然后减少stackTop变量的值。你应该把这两行代码放到一个叫PopStack()的子程序中。从而把这两行代码所必须执行的顺序隐藏起来。把这种信息隐藏起来,比让它们在系统内到处散布要好很多。
隐藏指针操作 指针操作的可读性通常都很差,而且也容易出错。通过把这些操作隔离在子程序内部,你就可以把精力集中于操作的意图本身,而不是指针操作机制的细节。同时,如果此类操作都能在一个位置完成,那么你对代码的正确性就会更有把握。如果你发现了比指针更合适的数据类型,也可以对程序做出修改,而不用担心会破坏了那些原本要使用指针的代码。
提高可移植性 可以用子程序来隔离程序中不可移植的部分,从而明确识别和隔离未来的移植工作。不可移植的部分包括编程语言所提供的非标准功能,对硬件的依赖,以及对操作系统的依赖等。
简化复杂的布尔判断 为了理解程序的流程,通常并没有必要去研究那些复杂的布尔判断的细节。应该把这些判断放入函数中,以提高代码的可读性,因为:(1)这样就把判断的细节放到一边了;(2)一个具有描述性的函数名字可以概括出该判断的目的。
把布尔判断的逻辑放入单独的函数中,也强调了它的重要性。这样做也会激励人们在函数内部做出更多的努力,提高判断代码的可读性。最终,代码的主流程和判断代码都变得更加清晰。简化布尔判断也是降低复杂度的一个例子,这一点在前面就已经讨论过了。
改善性能 通过使用子程序,你可以只在一个地方优化代码。把代码集中在一处可以更方便地查出哪些代码的运行效率低下。同时,在一处进行的优化,就能使用到(无论是直接调用还是间接使用)该子程序的所有代码都从中受益。把代码集中在一处之后,想用更高效的算法或更快速高效的语言来重写代码也更容易做了。
确保所有的子程序都很小? 不是的。既然有这么多好的理由来把代码写成子程序,这一点就没有必要了。事实上,有些事情写一个大的子程序来完成还会更好。(有关子程序的最佳长度,请参见第7.4节“子程序可以写多长”)
Operations That Seem Too Simple to Put Into Routines
似乎过于简单而没必要写成子程序的操作
编写有效的子程序时,一个最大的心理障碍是不情愿为一个简单的目的而编写一个简单子程序。写一个只有两三行代码的子程序可能看起来有些大才小用,但经验可以表明,一个很好而又小巧的子程序会多有用。
小的子程序有许多优点。其一便是它们能够提高其可读性。我曾在一个程序的十多处地方写了下面这行代码:
伪代码示例:某种计算
points = deviceUnits * ( POINTS_PER_INCH / DeviceUnitsPerInch() )
这肯定不是你所读过的最复杂的代码。多数人最终都能看懂,它进行的是从设备单位(device unit)到磅数(point)的转换计算。人们也会看出这十几处代码都在做着同样的事情。但是,它们原本可以更清楚些,所以我创建了一个子程序,并给它起了个好的名字,使这一转换可以只在一个地方进行:
伪代码示例:用函数来完成计算
Function DeviceUnitsToPoints ( deviceUnits Integer ): Integer
DeviceUnitsToPoints = deviceUnits *
( POINTS_PER_INCH / DeviceUnitsPerInch() )
End Function
在用这个子程序取代了那些直接嵌入计算的代码(inline code)之后,程序中的那十几行代码就差不多都成了下面这样:
伪代码示例:调用计算函数的
points = DeviceUnitsToPoints( deviceUnits )
这行代码更具可读性——甚至已经达到自我注解的地步。
这个例子还暗示出把简单操作写成函数的另一个原因:简单的操作常常会变成复杂操作。写这段子程序时我还没有认识到这一点,但在某些情况下,当某个设备激活(active)时,DeviceUnitPerInch()会返回0。这意味着我必须考虑到除以零的情况,为此需要再多写3行代码:
伪代码示例:维护代码时扩展了的函数
Function DeviceUnitsToPoints( deviceUnits: Integer ) Integer;
if ( DeviceUnitsPerInch() <> 0 )
DeviceUnitsToPoints = deviceUnits *
( POINTS_PER_INCH / DeviceUnitsPerInch() )
else
DeviceUnitsToPoints = 0
end if
End Function
如果还是在程序中的十几处地方出现原来那样的代码行,那么这一测试就也要重复十几次,这样就需要增加总共几十行代码。用一个简单的子程序,就把那几十行代码减到了3行。
Summary of Reasons to Create a Routine
总结:创建子程序的理由
下面概括了创建子程序的一些理由:
■ 降低复杂度
■ 引入中间的、易懂的抽象
■ 避免代码重复
■ 支持子类化
■ 隐藏顺序
■ 隐藏指针操作
■ 提高可移植性
■ 简化复杂的逻辑判断
■ 改善性能
除此之外,创建类的很多理由也是创建子程序的理由:
■ 隔离复杂度
■ 隐藏实现细节
■ 限制变化所带来的影响
■ 隐藏全局数据
■ 形成中央控制点
■ 促成可重用的代码
■ 达到特定的重构目的







