高质量的子程序
在讨论高质量的子程序的细节之前,明确下面这两个基本术语会很有帮助。首先,什么是“子程序(routine)”? 子程序是为实现一个特定的目的而编写的一个可被调用的方法(method)或过程(procedure)。
抛开计算机本身,子程序也算得上是计算机科学中一项最重大的发明了。子程序的使用使得程序变得更加易读,更易于理解,比在任何编程语言的任何功能特性都更容易。
子程序也是迄今为止发明出来的用以节约空间和提高性能的最重要手段。
3.1 创建子程序的正当理由
下面概况了创建子程序的一些理由
- 降低复杂度;
- 引入中间的、易懂从抽象;
- 避免代码代码重复;
- 支持子类化;
- 隐藏顺序;
- 隐藏指针操作;
- 提高可移植性;
- 简化复杂的逻辑判断;
- 改善性能;
除此之外,创建类的很多理由也是创建子程序的理由:
- 隔离复杂度;
- 隐藏实现细节;
- 限制变化所带来的影响;
- 隐藏全局数据;
- 形成中央控制点;
- 促成可重用的代码;
- 达到特定的重构目的;
3.2 在子程序层上设计
首先来理解一个概念,内聚性。对子程序而言,内聚性是指子程序中各种操作之间联系的紧密程度。我们的目标是让每一个子程序只把一件事情做好,不再做任何其他事情。这样做的好处是得到更高的可靠性。
关于内聚性的讨论一般会涉及到内聚性的几个层次。理解一些概念要比记住一些特定的术语更重要。这些概念可以帮助你思考如何让子程序尽可能地内聚。
功能的内聚性:是最强也是最好的一种内聚性,也就是说让一个子程序仅执行一项操作。
当然,以这种方式来评估内聚性,前提是子程序所执行的操作与其名字相符——如果它还做了其他的操作,那么它就不够内聚,同时其命名也有问题。
除此之外,还有其他一些种类的内聚性人们却通常认为是不够理想的。比如:顺序上的内聚性、通信上的内聚性、临时的内聚性。
为了得到更好的内聚性,可以把不同的操作纳入各自的子程序中。让调用方的子程序具有单一而完整的功能。为了让所有的子程序都具有功能上的内聚性,对两个或更多的原有子程序进行修改是很常见的。
这些术语中没有哪个是神秘的或者圣神不可侵犯的。需要理解的是其中的想法,而不是那些术语。编写具有功能上的内聚性的子程序几乎总是可能的,因此把注意力集中于功能上的内聚性,从而得到最大的收获。
3.3 好的子程序名字
好的子程序名字能清晰地描述子程序所做的一切。这里是有效地给子程序命名的一些指导原则。
- 描述子程序所做的所有事情;
- 避免使用无意义的、模糊或表述不清的动词;
- 不要仅通过数字来形成不同的子程序名字;
- 根据需要确定子程序名字的长度;
- 给函数命名时要返回值有所描述
- 给过程起名时使用语气强烈的动词加宾语的形式;
- 准确使用对仗词;
- 为常用操作确立命名规则。
3.4 如何使用子程序参数
- 按照输入-修改-输出的顺序排列参数。不要随机地或按字母顺序排列参数,而应该先列出仅作为输入用途的参数,然后是既作为输入又作为输出用途的参数,最后才是仅作为输出用途的参数。这种排列方法暗含了子程序的内部操作所发生的顺序——先是输入数据,然后修改数据,最后输出结果。
- 如果几个子程序都用了类似的一些参数,应该让这些参数的排列顺序保持一致。
- 使用所有的参数。
- 把状态或出错变量放在最后。按照习惯做法,状态变量和那些用于指示发生错误的变量应该放在参数表的最后。它们只是附属于程序的主要功能,而且它们是仅用于输出的参数,因此这是一种很有道理的规则。
- 不要把子程序的参数用做工作变量。把传入子程序的参数用做工作变量是很危险的。应该使用局部变量。
- 在接口中对参数的假定加以说明。如果你假定了传递给子程序的参数具有某种特征,那就要对这种假定加以说明。在子程序内部和调用子程序的地方同时对所做的假定进行说是值得的。不要等到把子程序写完了之后再回过头去写注释——你是不会记住所有这些假定的。一种比用注释还好的方法,是在代码中使用断言(assertions)。
应该对这些接口参数的假定进行说明:参数是仅用于输入的、要被修改的、还是仅用于输出的;表示数量的参数的单位;如果没有枚举类型的话,应该说明状态代码和错误值的含义;所能接受的数值的范围;不该出现的特定数值。
- 把子程序的参数个数限制在大约7个以内。在实践中,子程序中参数的个数到底应该限制在多少,取决于你所使用的编程语言如何支持复杂的数据类型。如果你使用的是一种支持结构化数据的现代编程语言,你就可以传递一个含有13个成员的合成数据类型,并将它看作一个大数据块。如果你使用的是一种更为原始的编程语言,那你可能需要分别传递全部13个成员。
如果你发现自己一直需要传递很多参数,这就说明子程序之间的耦合太过紧密了。应该重新设计这个或这组子程序,降低其间的耦合度。如果你向很多不同的子程序传递相同的数据,就请把这些子程序组成一个类,并把那些经常使用的数据用作类的内部数据。
- 考虑对参数采用某种表示输入、修改、输出的命名规则。
- 为子程序传递用以维持其接口抽象的变量或对象。
- 确保实际参数与形式参数相匹配。
核对表: 高质量的子程序
大局事项
- 创建子程序的理由充分吗?
- 一个子程序中所有适于单独提出的部分是不是已经被提出到单独的子程序中了?
- 过程的名字中是否用了强烈、清晰的“动词+宾语”词组?函数的名字是否描述了其返回值?
- 子程序的名字是否描述了它所做的全部事情?
- 是否给常用的操作建立了命名规则?
- 子程序是否具有强烈的功能上的内聚性?即它是否做且只做一件事情,并且把它做得很好?
- 子程序之间是否有较松的耦合?子程序与其他子程序之间的连接是否是小的、明确的、可见的和灵活的?
- 子程序的长度是否是由其功能和逻辑自热确定,而非遵循任何人为的编码标准?
参数传递适宜
- 整体来看,子程序的参数表是否表现出一种具有整体性且一致的接口抽象?
- 子程序参数的排列顺序是否合理?是否与类似的子程序的参数排列顺序相符?
- 接口假定是否已在文档中说明?
- 子程序的参数个数是否没超过7个?
- 是否用到了每一个输入参数?是否用到了每一个输出参数?
- 子程序是否避免了把输入参数用做工作变量?
- 如果子程序是一个函数,那么它是否在所有可能的情况下都能返回一个合法的值?
要点
- 创建子程序最主要的目的是提高程序的可管理性,当然也有其他一些好的理由。其中,节省代码空间只是一个次要的原因;提高可读性、可靠性和可修改性等原因都更重一些。
- 有时候,把一些简单的操作写成独立的子程序也非常有价值。
- 子程序可以按照其内聚性分为很多类,而你应该让大多数子程序具有功能上的内聚性,这是最佳的一种内聚性。
- 子程序的名字是它的质量的指示器。如果名字糟糕但恰如其分,那就说明这个子程序设计得很差劲。如果名字糟糕且又不准确,那么它就反映不出程序是干什么的。不管怎样,糟糕的名字都意味着程序需要修改。
- 只有在某个子程序的主要目的是返回有其名字所描述的特定结果时,才应该使用函数。
- 细心的程序员会非常谨慎地使用宏,而且只在万不得已时才用。
网友评论