[CodeComplete]创建一个函数需要理由吗
以下为<<代码大全2>>[第七章 高质量的子程序]的摘录
编程中什么是标准,相信大家都没有办法给出一套成系统的理论,而《代码大全》的作者就是在为我们描述从设计到实现诸多大家或意识到而没有深究,又或者还没有意识到的问题,通过系统的方式为大家展开了软件开发中诸多细节。希望对大家能都所帮助!
本章探讨了以下问题:
创建子程序的正当理由
在子程序层上展开设计
起个好名字
子程序可以写多长
如何使用子程序的参数
使用函数时要特别考虑的问题
什么时候使用函数,什么时候使用过程
宏子程序和内联子程序 (内容不错)
这里的子程序指的是完成一个特定目的的方法或过程,大家通常在什么状况下决定创建子程序?这个答案可能很难清楚。但在<<编码大全>>中有完整的分析.
1.创建子程序的正当理由
a.降低复杂度
可以通过创建子程序来隐藏一些信息,使得在实现时不必过多的考虑这些信息。当程序写好后甚至可以忘记这些细节,只要还记得如何使用。
b.引入中间、易懂的抽象
把一段代码放入一个命名恰当的子程序中,是说明这段代码的最好方法之一。如:
if(node <> NULL) then
while (node.next <> NULL ) do
node = node.next
leafName = node.name
end while
else
leafName=""
endif
如果改成这条语就会更容易理解:
leafName = GetLeafName( node )
这个函数名GetLeafName提供更高层次的抽象,从而使代码更具可读性,也更易于理解,同时降低原来包含上面代码的子程序的复杂度。
c.避免代码重复
如果在两段子程序内编写相似的代码,就意味着代码分解(decomposition),这就是错误。此时应当将两段子程序中的重复代码提取出来,将其中相同部分放入一个基类,然后将差异代码放入派生类中。还有另一种方法(如C语言开发),将相同的代码放入新的子程序中,然后让其余的代码来调用这个子程序。 这样的代码更加可靠,因为对于验证代码的正确性,你只需要检查一处代码,同时这样也会使改动更为可靠,因为你可以避免需要做相同的修改时,却做了一些略有不同的修改。
d.支持子类化(subclassing)
覆盖(override)简短而规整的子程序所需新代码的数量,要比覆盖冗长而混乱的子程序更少。如果你能让可覆盖的子程序保持简单,你在实现派生类的时候也会减少犯错的机会。
e.隐藏顺序
如一个程序通常是从先从用户那里读取数据,然后再从一个文件中读取辅助数据,那么,无论是从用户那里读取数据的子程序还是从文件中读取数据的子程序,都不应该依赖另一个子程序是否已执行。 一个明确的例子是先读取栈顶的数据,然后减少stackTop变量的值,这时就应当把两行代码放到一个叫PopStack()的子程序中。把这种信息隐藏起来,总比让它们在系统内到处散布要好很多。
f. 隐藏指针操作
指针操作的可读性通常都很差,而且也容易出错。通过把这些操作隔离在子程序内部,你就可以把精力集中于操作的意图本身,而不是指针操作的细节。同时如果此类和都能在一个位置完成,那么你对代码的正确性就会更有把握。如果你发现了比指针更合适的数据类型,也可以对程序做出修改,而不用担心会破坏了那些原本要使用指针的代码。
g.提高可移植性
可以用子程序来隔离程序中不可移植的部分,从而明确识别和隔离示来的移植工作。
h.简化复杂的布尔判断
为了理解程序的流程,通常并没有必要去研究那些得杂的布尔判断的细节,应当把这些判断放入函数中,以提高代码的可读性,因为:(1)这样就把判断的细节放到一边了; (2)一个具有描述性的函数名字可以概括出该判断的目的。
i. 改善性能
通过使用子程序,你可以只在一个地方优化代码。把代码集中在一处可以更方便地查出哪些代码的运行效率低下。同时在一处优化,就能使用到(无论直接或间接)该子程序的所有代码都从中受益。
j.确保所有的子程序都小 (NO)
如果有这么多好的理由来把代码写成子程序,这一点就没有必要了。
2.似乎过于简单而没必要写成子程序的操作
编写有效的子程序时,一个最大的心理障碍是不情愿为一个简单的目的而编写一个简单的子程序。写一个只有两三行代码的子程序可能看来有些大材小用,但经验表明,一个很好而又小巧的子程序会十分有用。
小的子程序有许多优点。其一便是它们能够提高其可读性。我曾在一个程序的十多处写了下面的代码:
points = deivceUnits * (POINTS_PER_INCH / DeviceUnitsPerInch())
多数人都能看懂这是从设备单位(device unit)到磅数(point)的转换。但是它还可以更清楚些,所以我创建了一个程序:
Function DeviceUnitsToPoints(deviceUnits:Integer):Integer
DeviceUnitsToPoints = deivceUnits *
(POINTS_PER_INCH / DeviceUnitsPerInch())
End Function
在用这个子程序取代了那些直接嵌入计算的代码(inline code)之后,程序中那十几行代码就差不多成了下面:
points = DeviceUnitsToPoints( deviceUnits)
这行代码更具可读性--甚至已经达到自我注解的地步。
这个例子还暗示出把简单操作写成函数的另一个原因:简单的操作常常会变成复杂操作。如在某些情况下,当某个设备使DeviceUnitPerInch()返回0,就意味着必须考到除零的情况,为此就需再多写3行代码。而在以子程序实现前则需要付出数十倍的工作量。
3.起个好名字
注:函数名不要用拼音,那样会有方言发音的问题。
a.描述子程序所做的所有事情
子程序的名字应当描述其所有的输出结果以及副作用(side effects).如果一个子程序的作用是计算报表总额并打开一个输出文件,那么把它命名为CompuseReportTotals就还不算完整。ComputeReportTotalAndOpenOutputFile()是很完整,但是又太长且显得有点傻。如果你写的是有一些副作用的子程序,那就会起出很多又长又傻的名字。解决的方法不是改成其它名字,而应该换一种方式编写程序,直接解决问题。
b.避免使用无意义的,模糊或表述不清的动词
有些动词的含义非常灵活,可以涵盖几乎任何含义。像HandleCalculation(), PerformServices(0,OutputUser(),ProcessInput()和DealWithOutput()这样的子程序名字根本不能说明子程序在做什么。当然,当handle用做事件处理这一特定的技术含义时是个例外。
如果将HandleOuput()改为FormatAndPrintOutput()就更容易看清这个子程序的功能。
c.不要仅通过数字来形成不同的子程序名字
有个程序员把所有的代码都写成一个大的函数,然后为每15行代码创建一个函数,并把它们分别命名为Part1,Part2等。 这种创建子程序和给子程序命的做法实在是骇人听闻。
d.根据需要确定子程序名字的长度
研究表明,变量名的最佳长度是9到15个字符。子程序通常比变量更为复杂,因此,好的子程序名字通常也会更长一些。另一方面,子程序名字通常是跟在对象名字后,这实际上为其免费提供了一部分名字。总之,子程序名的长短要视该名字是否清晰易懂而定。
e.给函数命名时要对返回值有所描述
函数有返回值,因此函数的命名要应该针对其返回值进行。如比cos(),customerID.Next(),和pen.CurrentColor()都是不错的函数名,它人精确地表述了该函数将要返回的结果。
f.给过程起名时使用语气强烈的动词加宾语的形式
一个具有功能内聚性的过程通常是针对一个对象执行一种操作。过程的名字应当能反映该过程所做的事情,而一个针对某对象执行的操作就需要一个动词+宾语形式的名字,如PrintDocument() CalcMonthlyRevenues(),CheckOrderInfo()和RepaginateDocument()等。
g准确使用对仗词
命名时使用对仗词的命名规则有助于保持一致性,从而也提高可读性。像first/last这样的词组就容易理解;而像FileOpen和_lclose这样的组合就容易使人迷惑。下面列出一些常见的对仗词组:
add/remove increment/decrement open/close
beign/end insert/delete show/hide
create/destroy lock/unlock source/target
first/last min/max start/stop
get/put next/previous up/down
get/set old/new
h.为常用操作确立命名规则
在某些系统里,区分不同类别的操作非常重要。而命名规则往往是指示这种区别的最简单也是最可靠的方法。
在我做过的一个项目的代码里,每个对象都被分配了一个唯一标识。我们忽视了为返回这种对象标识的子程序建立一个命名规则,以至了有了下面这些子程序名字:
employee.id.Get()
dependent.GetId()
supervisio()
candidate.id()
其中Employee类提供了其id对象,而该对象以进而提供了Get()方法;Dependent类提供了GetId方法; Supervisor类则把id和为它的默认返回值;到了项目中期,已经没人能记住哪个对象应该用哪些子程序了,但此时已经撰写了太多的代码。这样一来项目组中每个人都不得不花费不必要的精力,去记住每个对象上采用的获取id的语法细节。而这些问题完全可以通过建立获取id的命名规则而避免。
4.子程序可以写多长
注:书中首先列举历年对子程序代码长度对质量影响的研究成果!
如果要编写一段超过200行代码的子程序,那你就要小心了。对于超过200行代码的子程序来说,没有哪项研究发现它能降低成本或降低出错率,而且超过200行后,你迟早会在可读性方面遇到问题。
*关于函数复杂度,可以参考另一篇文章。
高质量的子程序的Check List:
大局事项
Y/N 创建子程序的理由充分吗?
Y/N 一个子程序中所有适于单独提出的部分是不是已经被提出到单独的子程序中了?
Y/N 过程的名字中是否使用了强烈、清晰的"动词+宾语"词组?函数的名字是否描述了其返回值?
Y/N 子程序的名字是否描述了它所做的全部事情?
Y/N 是否给常用的操作建立了命名规则?
Y/N 子程序之间是否有较松的耦合?子程序与其他子程序之间的连接是否是小的(small)、明确的(intimate)、可见的(viaible)和灵活的(flexible)?
Y/N 子程序的长度是否是由其功能和逻辑自然确定,而非是依照任何人为的编码标准?
参数传递事宜:
Y/N 整体来看,子程序的参数表是否表现出一种具有整体性且一致的接口抽象?
Y/N 子程序参数的排列顺序是否合理? 是否与类似的子程序的参数排列顺序相符?
Y/N 接口假定是否已在文档中说明?
Y/N 子程序的参数个数是否没超过7个?
Y/N 是否用到了每个参数?
Y/N 子程序是否避免了把输入参数用做工作变量?
Y/N 如果子程序是一个函数,那么它是否在所有可能的情况下都能返回一个合法的值?