函数长度
2016年11月30日
在我的职业生涯中,我听到过很多关于函数应该有多长的争论。这是一个更重要问题的替代方案——我们什么时候应该将代码封装到自己的函数中?其中一些指南是基于长度的,例如函数不应超过屏幕大小[1]。有些是基于重用的——任何使用超过一次的代码都应该放在自己的函数中,但只使用一次的代码应该保留在内联。然而,对我来说最有意义的论点是意图和实现之间的分离。如果你必须花费精力查看一段代码来弄清楚它在做什么,那么你应该把它提取到一个函数中,并用这个“做什么”来命名函数。这样,当你再次阅读它时,函数的目的就会立即显现出来,而且大多数时候你不需要关心函数如何实现其目的——也就是函数体。
一旦我接受了这个原则,我就养成了编写非常小函数的习惯——通常只有几行代码[2]。任何超过六行代码的函数对我来说都开始变得难闻,而且我经常会写只有一行代码的函数[3]。肯特·贝克向我展示了最初 Smalltalk 系统中的一个例子,这让我意识到大小并不重要。当时的 Smalltalk 运行在黑白系统上。如果你想突出显示一些文本或图形,你会反转视频。Smalltalk 的图形类有一个名为“highlight”的方法,它的实现只是对“reverse”方法的调用[4]。方法的名称比它的实现更长——但这并不重要,因为代码的意图和它的实现之间存在很大差距。
有些人担心短函数,因为他们担心函数调用的性能成本。在我年轻的时候,这偶尔是一个因素,但现在这种情况非常罕见。优化编译器通常在较短的函数上运行得更好,这些函数可以更容易地缓存。一如既往,关于性能优化的通用指南才是最重要的。有时,你可能需要在以后内联函数,但通常较小的函数会建议其他提高速度的方法。我记得人们反对为列表使用isEmpty
方法,而常见的习惯用法是使用aList.length == 0
。但是,在这里,在函数上使用揭示意图的名称也可能支持更好的性能,如果判断集合是否为空比确定其长度更快。
只有当名称很好时,这样的短函数才有效,因此你需要认真注意命名。这需要练习,但一旦你精通了它,这种方法可以使代码变得非常自文档化。更大规模的函数可以像故事一样阅读,读者可以根据需要选择哪些函数深入了解更多细节。
致谢
Brandon Byars、Karthik Krishnan、Kevin Yeung、Luciano Ramalho、Pat Kua、Rebecca Parsons、Serge Gebhardt、Srikanth Venugopalan 和 Steven Lowe 在我们内部邮件列表中讨论了这篇文章的草稿。
Christian Pekeler 提醒我,嵌套函数不符合我的大小观察结果。
注释
1: 或者在我的第一份编程工作中:两页行式打印机纸——大约 130 行 Fortran IV 代码
2: 许多语言允许你使用函数来包含其他函数。这通常用作范围缩减机制,例如使用函数作为对象模式来实现类。这样的函数自然要大得多。
3: 我的函数的长度
最近,我对构建这个网站的工具链中的函数长度感到好奇。它主要是 Ruby,大约有 15 KLOC。以下是方法体长度的累积频率图
如你所见,这里有很多小方法——我代码库中的一半方法只有两行或更少。(这里的行是指非注释、非空白行,不包括def
和end
行。)
以下是数据的粗略表格形式(我感觉太懒了,不想把它变成合适的 HTML 表格)。
lines.freq lines.cumfreq lines.cumrelfreq [1,2) 875 875 0.4498715 [2,3) 264 1139 0.5856041 [3,4) 195 1334 0.6858612 [4,5) 120 1454 0.7475578 [5,6) 116 1570 0.8071979 [6,7) 69 1639 0.8426735 [7,8) 75 1714 0.8812339 [8,9) 46 1760 0.9048843 [9,10) 50 1810 0.9305913 [10,15) 98 1908 0.9809769 [15,20) 24 1932 0.9933162 [20,50) 12 1944 0.9994859
4: 这个例子来自肯特出色的Smalltalk 最佳实践模式中的揭示意图的消息