《代码大全2》第7章 高质量的子程序

目录

前言

7.1 创建子程序的正当理由

7.1.1 区分函数与过程

7.2 在子程序层上设计

7.3 好的子程序名字

7.4 子程序可以写多长

7.5 如何使用子程序参数

7.6 使用函数时要特别考虑的问题

7.7 宏子程序和内联子程序

核对表:高质量的子程序

大局事项

参数传递事宜

要点


《Code_Complete_2》持续更新中......_@来杯咖啡的博客-CSDN博客这本书有意设计成使你既可以从头到尾阅读,也可以按主题阅读。1. 如果你想从头到尾阅读,那么你可以直接从第2章“用隐喻来更充分地理解软件开发”开始钻研。2. 如果你想学习特定的编程技巧,那么你可以从第6章“可以工作的类”开始,然后根据交叉引用的提示去寻找你感兴趣的主题。3. 如果你不确定哪种阅读方式更适合你,那么你可以从第3章3.2节“辦明你所从事的软件的类型”开始。.....................https://blog.csdn.net/qq_43783527/article/details/126275083

前言

        在讨论高质量的子程序的细节之前,明确下面这两个基本术语会很有帮助。

        首先,什么是“子程序(routine)”?子程序是为实现一个特定的目的而编写的一个可被调用的方法(c++、java)或过程(vb语言)

        那什么又是高质量的子程序呢?这个问题更难回答。也许回答这个问题的最简单的办法,是来看看什么东西不是高质量的子程序。这里举一个低质量的子程序的例子:

这个子程序里有哪些不妥呢?给你一个提示:你应该能够从中发现至少10不同的问题。请你先列出自己发现的问题,然后再来看下面这份清单: 

  • 这个子程序有个很差劲的名字。Handlestuff()一点也没有告诉你这个子程序究竟是做什么的。
  • 这个子程序没有文档(有关文档的话题己经超出了子程序的范畴,因此将在第32章“自说明代码”中讨论)。
  • 这个子程序的布局不好。代码的物理组织形式几乎没有给出任何关于其逻辑组织的提示。布局的使用过于随意,程序内的不同部分使用了不同的布局风格。请比较一下 expenserype==2 和 expenserype==3 这两处的代码风格。(在第31章“布局与风格”中会讨论布局问题。)
  • 这个子程序的输入变量 inputRec 的值被改变了。如果它是一个输入变量,它的值就不应该被修改(而且在 C++中它应该定义为 const)。如果变量的值就是要被修改的,那就不要把它命名为 inputRec。
  • 这个子程序读写了全局变量——它从 corpExpense 中读取数值并将其写入profit。它应该更直接地与其他子程序通信,而不是去读写全局变量。
  • 这个子程序没有一个单一的目的。它初始化了一些变量,向数据库写入数据,又做了一些计算——从这些事情之间看不出任何联系。子程序应该有单一而明确的目的。
  • 这个子程序没有注意防范错误数据 (bad data)。如果 crntQtr 等于0,那么表达式 ytaRevenue*4.0/ (double)crntQtrh 将会导致除零错误。
  • 这个子程序用了若干神密数值(magic number):100,4.0、12、2、3等。神秘数值的问题会在第12.1节“数值概论”中探讨。
  • 这个子程序未使用其中一些参数:screenx 和 screenY 在程序中都没有被引用过。
  • 这个子程序的一个参数传递方式有误:prevcolor 被标为引用参数(&),但在这个子程序内却未对其赋值。
  • 这个子程序的参数太多了。合理的参数个数,其上限大概是了个左右,而这个子程序有11个。这些参数的排布方式也难以理解,估计没人想仔细研究它们、甚至没人想数数有几个参数。
  • 这个子程序的参数顺序混乱且未经注释。(参数的顺序会在本章探讨。而代码注释问题会在第32章中阐述。)

7.1 创建子程序的正当理由

最主要的两个理由:

1、降低复杂度:直接调用子程序而无需了解其内部实现;子程序需要有个好名字

2、避免代码重复:便于多处调用,提高可移植性;

7.1.1 区分函数与过程

  • 函数是指有返回值的子程序;
  • 过程是指没有返回值的子程序。

7.2 在子程序层上设计

什么是高内聚性?

        内聚性源于结构化设计,并且经常与耦合度结合在一起讨论。内聚性指的是类内部的子程序或者子程序内的所有代码在支持一个中心目标上的紧密程度——这个类的目标是否集中。包含一组密切相关的功能的类被称为有着高内聚性,而这种启发式方法的目标就是使内聚性尽可能地高。内聚性是用来管理复杂度的有用工具,因为当一个类的代码越集中在一个中心目标的时候,你就越容易记住这些代码的功能所在。

        功能的内聚性 (functional cohesion)是最强也是最好的一种内聚性,也就是说让一个子程序仅执行一项操作。例如 sin(), GetCustomerName ()、 Fraserile()、CalculateLoanPayment()以及 AgeFromBirthdate() 这样的子程序都是高度内聚的。当然,以这种方式来评估内聚性,前提是子程序所执行的操作与其名字相符-如果它还做了其他的操作,那么它就不够内聚,同时其命名也有问题。

7.3 好的子程序名字

交叉参考:关于给变量命名的更详细探讨,请参考第11章“变量名的力量”。

        好的子程序名字能清晰地描述子程序所做的一切。这里是有效地给子程序命名的一些指导原则。

        1、描述子程序所做的所有事情 。子程序的名字应当描述其所有的输出结果以及副作用 (side effects)。

  • 如果一个子程序的作用是计算报表总额并打开一个输出文件,那么把它命名为 ComputeReportTotals()就还不算完整。ComputeReportTotalsandopenoutputFile()是很完整,但是又太长且显得有点傻。
  • 如果你写的是有一些副作用的子程序,那就会起出很多又长又笨的名字。解决的方法不是使用某个描述性较弱的子程序名,而应该换一种方式编写程序,直截了当地解决问题而不产生副作用。

        2、避免使用无意义的、模糊或表述不清的动词 。有些动词的含义非常灵活,可以延伸到涵盖几乎任何含义。像 Handlecalculation()、 Performservices ()、Outputuser ()、 ProcessInput() 和 Dealwithoutput()这样的子程序名字根本不能说明子程序是做什么的。最多就是告诉你这些子程序所做的事情与计算、服务、用户、输入、输出有关。当然,当动词 “handle(处理)”用做“事件处理 (eventhandling)” 这一特定的技术含义时是个例外。

  •         有时一个子程序中仅有的问题就是其名字表述不清,而子程序本身也许设计得很好,但如果把它的名字由 Handleoutput()改为 FormatAnaprintoutput (),那你就很容易看清楚这个子程序的功能了。
  •         还有另外一些情况,其中的动词之所以含糊,是由于子程序执行的操作就是含糊不清的。。如果是这种情况,那么最佳的解决办法便是重新组织该子程序及任何与之相关的子程序,以便使它们都具有更为明确的目的,进而赋予其能够精确描述这些目的的更为清晰的名字。

        3、不要仅通过数字来形成不同的子程序名字。  有个程序员把所有的代码都写成一个大的函数,然后为每15 行代码创建一个函数,并把它们分别命名为 Partl、Part2 等。在此之后,他又创建了一个高层次的函数来调用这些部分(Part)。这种创建子程序和给子程序命名的做法实在是骇人听闻(我真希望这很少发生)。但程序员们有时会用数字来区分类似于 outputuser、outputUser1 和 Outputuser2这样的子程序。这些名字后面的数字无法显示出子程序所代表的抽象有何不同,因此这些子程序的命名也都很糟糕。

        4、根据需要确定子程序名字的长度。 研究表明,变量名的最佳长度是9到15个字符。子程序通常比变量更为复杂,因此,好的子程序名字通常也会更长一些。另一方面,子程序名字通常是跟在对象名字之后,这实际上为其免费提供了一部分名字。总的来说,给子程序命名的重点是尽可能含义清晰,也就是说,子程序名的长短要视该名字是否清晰易懂而定
        5、给函数命名时要对返回值有所描述函数有返回值,因此,函数的命名要应该针对其返回值进行

  • 比如说,cos()、customerIa. Next()、printer.IsReady()和pen.Currentcolor() 都是不错的函数名,它们精确地表述了该函数将要返回的结果。

        6、给过程起名时使用语气强烈的动词加宾语的形式。 一个具有功能内聚性的过程(procedure)通常是针对一个对象执行一种操作。过程的名字应该能反映该过程所做的事情,而一个针对某对象执行的操作就需要一个动词+宾语(verb-plus-object)形式的名字。如 PrintDocument () 、CalcMonthlyRevenues ()、CheckorderInfo() 和 RepaginateDocument()等,都是很不错的过程名。
        在面向对象语言中,你不用在过程名中加入对象的名字(宾语),因为对象本身己经包含在调用语句中了。
        7、准确使用对仗词命名时遵守对仗词的命名规则有助于保持一致性,从而也提高可读性。像 first/last 这样的对仗词组就很容易理解;而像 Fileopen () 和_1close()这样的组合则不对称,容易使人迷惑。下面列出一些常见的对仗词组:

        8、为常用操作确立命名规则。 在某些系统里,区分不同类别的操作非常重要。而命名规则往往是指示这种区别的最简单也是最可靠的方法。

7.4 子程序可以写多长

        在面向对象的程序中,一大部分子程序都是访问器子程序(accessor routines),它们都非常短小。在任何时候,复杂的算法总会导致更长的子程序。在这种情况下,可以允许子程序的长度有序地增长到 100至 200 行(不算源代码中的注释行和空行)

        这就是说,如果要编写一段超过 200 行代码的子程序,那你就要小心了。对于超过200行代码的子程序来说,没有哪项研究发现它能降低成本和/或降低出错率,而且在超过200 行后,你迟早会在可读性方面遇到问题。

7.5 如何使用子程序参数

        子程序之间的接口是程序中最易出错的部分之一。由 Basili 和 Perricone(1984)所做的一项被广为引用的研究发现,程序中有39% 的错误都是属于内部接口错误——也就是子程序间互相通信时所发生的错误

以下是一些可以减少接口错误的指导原则:
        按照输入-修改-输出的顺序排列参数 。不要随机地或按字母顺序排列参数,而应该先列出仅作为输入用途的参数,然后是既作为输入又作为输出用途的参数,最后才是仅作为输出用途的参数。这种排列方法暗含了子程序的内部操作所发生的顺序——先是输入数据,然后修改数据,最后输出结果。

        如果几个子程序都用了类似的一些参数,应该让这些参数的排列顺序保持一致 。子程序的参数顺序可以产生记忆效应——不一致的顺序会让参数难以记忆。

        不要把子程序的参数用做工作变量。 把传入子程序的參数用做工作变量是很危险的。应该使用局部变量。更好的方法是明确地引入一些工作变量,从而避免当前或日后的麻烦。下面这段代码演示了这一技术:

         在接口中对参数的假定加以说明。 如果你假定了传递给子程序的参数具有某种特征,那就要对这种假定加以说明。在子程序内部调用子程序的地方同时对所做的假定进行说明是值得的。不要等到把子程序写完之后再回过头去写注释——你是不会记住所有这些假定的。一种比用注释还好的方法,是在代码中使用断言(assertions)。

        把子程序的参数个数限制在大约7 个以内。对于人的理解力来说,7是一个神奇的数字。心理学研究发现,通常人类很难同时记住超过7个单位的信息(Miler1956)。这一发现已经用手各种领域之中,因此,假定人不能同时记住超过约7个的子程序参数,也是合适的。

        为子程序传递用以维持其接口抽象的变量或对象。 关于如何把对象的成员传给子程序这一问题,存在着两种互不相让的观点。比如说你有一个对象,它通过10个访问器子程序(access routine)暴露其中的数据,被调用的子程序只需要其中的3项数据就能进行操作。那么是应该选择直接传递这个对象还是只传递3项数据呢?这两种情况,都没有错,你自己根据场景来决定。

7.6 使用函数时要特别考虑的问题

        现代的编程语言,如 C++、Java、 Visual Basic 等,都同时支持西数和过程。

  • 函数是指有返回值的子程序;
  • 过程是指没有返回值的子程序。

在C++中,通常把所有子程序都称为“函数”,然而,那些返回值类型为 void 的函数在语义上其实就是过程。函数与过程的区别更多的是语义的区别,而不是语法的区别,你还是要以语义为准。

略。

7.7 宏子程序和内联子程序

略。

核对表:高质量的子程序

大局事项

  • 创建子程序的理由充分吗?
  • 一个子程序中所有适于单独提出的部分是不是已经被提出到单独的子程序中了?
  • 过程的名字中是否用了强烈、清晰的“动词十宾语”词组?函数的名字是否描述了其返回值?
  • 子程序的名字是否描述了它所做的全部事情?
  • 是否给常用的操作建立了命名规则?
  • 子程序是否具有强烈的功能上的内聚性?即它是否做且只做一件事,并且把它做得很好?
  • 子程序之间是否有较松的耦合?子程序与其他子程序之间的连接是否是小的(small)、明确的(intimate)、可见的(viaible)和灵活的(flexible)?
  • 子程序的长度是否是由其功能和逻辑自然确定,而非遊循任何人为的编码标准?

参数传递事宜

  • 整体来看,子程序的参数表是否表现出一种具有整体性且一致的接口抽象?
  • 子程序参数的排列顺序是否合理?是否与类似的子程序的参数排列顺序相符?
  • 接口假定是否已在文档中说明?
  • 子程序的参数个数是否没超过7个?
  • 是否用到了每一个输入参数?
  • 是否用到了每一个输出参数?
  • 子程序是否避免了把输入参数用做工作变量?
  • 如果子程序是一个函数,那么它是否在所有可能的情况下都能返回一个合法的值?

要点

  • 创建子程序最主要的目的是提高程序的可管理性,当然也有其他一些好的理由。其中,节省代码空间只是一个次要原因;提高可读性、可靠性和可修改性等原因都更重要一些。
  • 有时候,把一些简单的操作写成独立的子程序也非常有价值。
  • 子程序可以按照其内聚性分为很多类,而你应该让大多数子程序具有功能上的内聚性,这是最佳的一种内聚性。
  • 子程序的名字是它的质量的指示器。如果名字糟糕但恰如其分,那就说明这个子程序设计得很差劲。如果名字糟糕而且又不准确,那么它就反映不出程序是干什么的。不管怎样,糟糕的名字都意味着程序需要修改。
  • 只有在某个子程序的主要目的是返回由其名字所描述的特定结果时,才应该使用函数。
  • 细心的程序员会非常谨慎地使用宏,而且只在万不得已时才用。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@来杯咖啡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值