第七章 高质量的子程序
子程序指代方法、过程或者函数。
7.1 创建子程序的正当理由
降低复杂度
通过调用子程序来实现功能而无需了解其内部工作细节
引入中间、易懂的抽象
可以利用子程序的命名来简要清晰地介绍代码作用
避免代码重复
隐藏顺序
把需要按照顺序执行的代码包装为一个子程序,以将顺序信息隐藏起来,避免它们在系统内四处散布。
改善性能
子程序可以方便查找运行效率低下的代码。同时由于规模较小也更易于优化。
有时候我们会因为一个小功能只需要两三行代码实现而不愿意额外编写一个子程序,但是这其实是有益处的,比如提高可读性——帮助实现自我注解。
7.2 在子程序层上设计
内聚性指子程序中各种操作之间联系的紧密程度。
功能的内聚性指一个子程序仅执行一个功能,比如sin,getName()等。这是最好的一种内聚性。
顺序的内聚性指子程序内存在需要按特定顺序执行的操作,并且这些步骤需要共享数据。比如一个计算苹果售价的子程序,先得到苹果的重量,再基于重量,乘以单价计算价格。后一步是基于前一步的才被称为具有顺序的内聚性。这样的情况可以将其拆分为两个子程序,如GetWeight和GetSalePrice,其中GetSalePrice调用GetWeight。这样两者就都具有功能上的内聚性了。
如果按特定顺序执行的操作之间并不存在依赖关系,仅仅只是一种人为规定的顺序——比如注册时先输入账号,再输入密码,最后输入验证码,那么这称为过程上的内聚性,一种可取的方法是把它们集合在一个子程序里,使其单一且完整(因为其中的某一个步骤没有单独存在的意义)
通信上的内聚性指子程序中的不同操作使用了同样的数据,但不存在其他联系。比如子程序接收出生年份这一数据后先打印出了年龄,接着用出生年份计算并打印出生肖,这就是通信上的内聚性。同样的,我们可以用三个程序来对其进行拆分,即A子程序打印年龄,B子程序打印生肖,C子程序按照顺序调用A、B子程序。
逻辑上的内聚性,这是很常见的一种情况。比如我写了一个计算器子程序,它接收三个参数——两个数值以及一个运算符。按照传入的运算符进行不同逻辑操作,比如除法时要验证除数不能为0等。在这个例子下,这样的设计还可以,因为一个简单的计算器程序只涉及了四种基本运算符号,用if else就能很好的区分开并且代码量很小。但是这不是最优的,如果未来这个计算器需要扩展,引入指数运算,对数运算等,那么代码或许就会逐渐变得冗杂。比较好的处理方法是把每一则运算写成一个子程序,每一个if else中不包含具体的实现而是调用这些子程序。这种设计的术语是“事件处理器”。
7.3 好的子程序名字
子程序的名字应该尽可能完整地描述所有的输出结果以及副作用(返回值或修改了某些数值等)。如果某个子程序的功能过多导致难以用简洁的名称将其命名,那么应该考虑对该子程序进行拆分,而不是用模糊的概括性子程序名。
避免使用模糊的动词,如果子程序本身做的事情就是模糊的,那么应该做的是重新组织子程序。
不要用数字来作为区分子程序的名字,比如method1\method2
准确使用对仗词,即功能相反的子程序的命名最好也是对仗的。
建立统一的命名规则,例如Java当前基本统一的get\set方法等。
7.4 子程序可以写多长
虽然许多研究在探寻子程序长度和出现bug的相关性时得出的结论不尽相同(有的截然相反),但是一般来说控制子程序不超过200行即可。
7.5 如何使用子程序参数
以下是一些可以减少接口错误的指导原则
按照输入-修改-输出的顺序排列参数
即,仅输入到子程序中的参数、传入到子程序中进行修改的参数,以及仅输出的参数(matlab等语言可以在参数中定义返回值)
如果有不止一个子程序使用了接近的参数,让它们的顺序保持一致
不传递任何无用参数
把状态变量放在最后 比如指示子程序应该按照何种模式工作的变量等。
不要把子程序的参数用于工作变量
后者的功能虽然与前者完全一致,并且代码量增多了,但是其可读性要高得多。
参数个数限制在大约七个 如果你发现参数过多了,考虑一下子程序之间的耦合度是否太过紧密了。
传递对象还是对象中的有用参数?
传递对象中的特定数据,可以降低耦合度,更便于重用等。但是由于拆分了对象,而破坏了封装性。
传递整个对象,可以让被调用子程序能更灵活地使用对象的其余成员(当未来需要用到时就不需要修改代码了),从而让接口更稳定。但是由于潜在地把对象中的所有访问器子程序都暴露了,所以也破坏了封装性。
这两种方法各自都有适合采用的场景。如果调用程序的参数和对象有一定关系,同时不保证未来不会改变这些参数(但是都和该对象有关),那么就最好传递整个对象。如果只是特定的需要这些个参数,和对象本身关联不大,那么传递这些特定参数就可以了。
当难以判断时,还有一种更简单的方法,如果现有的是参数,就直接传递参数,否则传递对象。
7.6 使用函数时要特别考虑的问题
前者代码密度较大,不容易让人知道返回值是什么,可读性比较差,比较推荐后者。不过有的时候为了让代码看起来更简洁,同时返回值也很简单明显的,用前者也未尝不可。
CHECKLIST:Hign-Quality Routines
大局事项
创建子程序的理由充分吗?
一个子程序中所有适于单独提出的部分是不是已经被提出到单独的子程 序中了?
过程的名字中是否用了强烈、清晰的“动词+宾语”词组?
函数的名字是否描述了其返回值?
子程序的名字是否描述了它所做的全部事情?
是否给常用的操作建立了命名规则?
子程序是否具有强烈的功能上的内聚性?即它是否做且只做一件事,并且把它做得很好?
子程序之间是否有较松的耦合?子程序与其他子程序之间的连接是否是小的、明确的、可见的和灵活的?
子程序的长度是否是由其功能和逻辑自然确定,而非遵循任何人为的编 团样准?
参数传递事宜
整体来看,子程序的参数表是否表现出一种具有整体性且一致的接口抽象?
子程序参数的排列顺序是否合理?是否与类似的子程序的参数排列顺序 相符?
接口假定是否已在文档中说明?
子程序的参数个数是否没超过7个?
是否用到了每一个输入参数?
是否用到了每一个输出参数?
子程序是否避免了把输入参数用做工作变量?
如果子程序是一个函数,那么它是否在所有可能的情况下都能返回一个合法的值?
Key Points
-
创建子程序最主要的目的是提高程序的可管理性,当然也有其他-一些好的理由。其中,节省代码空间只是一个次要原因;提高可读性、可靠性和可修改性等原因都更重要一些。
-
有时候,把一些简单的操作写成独立的子程序也非常有价值。
-
子程序可以按照其内聚性分为很多类,而你应该让大多数子程序具有功能上的内聚性,这是最佳的一种内聚性。
-
子程序的名字是它的质量的指示器。如果名字糟糕但恰如其分,那就说明这个子程序设计得很差劲。如果名字糟糕而且又不准确,那么它就反映不出程序是干什么的。不管怎样,糟糕的名字都意味着程序需要修改。
-
只有在某个子程序的主要目的是返回由其名字所描述的特定结果时,才应该使用函数。
-
细心的程序员会非常谨慎地使用宏,而且只在万不得已时才用。