函数什么时候太长了?

35行,55行,100行,300行?你什么时候应该开始拆散它?我这样问是因为我有一个包含60行(包括注释)的函数,并且正在考虑将它分开。

long_function(){ ... }

变成:

small_function_1(){...}
small_function_2(){...}
small_function_3(){...}

函数不会在 long _ function 之外使用,使用更小的函数意味着更多的函数调用,等等。

你什么时候会把一个函数分解成更小的函数? 为什么?

  1. 方法应该只做一件逻辑事情(考虑功能性)
  2. 你应该能够用一句话解释这个方法
  3. 它应该适合您的显示器的高度
  4. 避免不必要的开销(指出显而易见... 的注释)
  5. 对于较小的逻辑函数,单元测试更容易
  6. 检查函数的一部分是否可以被其他类或方法重用
  7. 避免过多的类间耦合
  8. 避免深度嵌套的控制结构

感谢大家的回答 ,编辑列表并投票选出正确的答案,我会选择那一个;)

我现在正带着这些想法进行重构:)

69685 次浏览

这在一定程度上取决于品味,但我如何确定这一点呢? 我尽量保持我的功能大致只有在一次(最大限度)适合我的屏幕的情况下才能保持。原因是,如果你能同时看到整个事情,就更容易理解正在发生的事情。

当我编写代码时,需要混合编写长函数,然后通过重构来提取可以被其他函数重用的代码——同时编写小函数来完成离散的任务。

我不知道这个问题有没有正确或错误的答案(例如,你可以选择67行作为你的最大值,但是有时候多加一些是有意义的)。

我经常分解函数的主要原因是因为函数的零碎部分也是我正在编写的另一个函数的组成部分,所以公共部分被分解了。此外,如果它使用了其他类中的大量字段或属性,那么很有可能将相关块整体提取出来,并且如果可能的话将其移动到其他类中。

如果你有一段代码,在顶部有一个注释,考虑把它拉到一个函数中,函数和参数名称说明它的用途,并保留注释作为代码的基本原理。

你确定里面没有其他有用的东西吗?这是什么功能?

函数应该只做一件事。如果你在一个函数中做很多小事情,把每个小事情变成一个函数,然后从 long 函数中调用这些函数。

不要真正想做的是将长函数的每10行内容复制粘贴到短函数中(正如您的示例所示)。

没有什么严格的规定。我通常喜欢我的方法只是 “做一件事”。所以如果它抓取数据,然后用这些数据做些什么,然后把它写到磁盘上,然后我把抓取和写入分割成不同的方法,这样我的“ main”方法就包含了“做些什么”。

但是 “做点什么”仍然可能有很多行,所以我不确定 “行数”指标是否是正确的使用方法:)

编辑: 这是我上周在工作中邮寄的一行代码(为了证明一个观点)。.这绝对不是我养成的习惯:)-我当然不希望在我的方法中有50-60个这样的坏男孩: D

return level4 != null ? GetResources().Where(r => (r.Level2 == (int)level2) && (r.Level3 == (int)level3) && (r.Level4 == (int)level4)).ToList() : level3 != null ? GetResources().Where(r => (r.Level2 == (int)level2) && (r.Level3 == (int)level3)).ToList() : level2 != null ? GetResources().Where(r => (r.Level2 == (int)level2)).ToList() : GetAllResourceList();

我想你会找到很多答案。

我可能会根据函数中执行的逻辑任务将其分解。如果你认为你的短篇小说正在变成一部小说,我建议你找到并提炼出不同的步骤。

例如,如果你有一个处理某种字符串输入并返回字符串结果的函数,你可能会根据将字符串分成几部分的逻辑、添加额外字符的逻辑以及将所有字符重新组合成格式化结果的逻辑将该函数分解。

简而言之,只要能使代码清晰易读(无论是通过简单地确保函数具有良好的注释还是将其分解) ,就是最好的方法。

对于一个函数来说,60行代码很大,但不会太长。如果它适合在一个编辑器中的一个屏幕上,您可以一次看到所有内容。它实际上取决于函数正在做什么。

为什么我可以分解一个函数:

  • 太长了
  • 通过分解代码并为新函数使用有意义的名称,它使代码更易于维护
  • 这个功能没有内聚性
  • 函数的某些部分本身是有用的。
  • 当很难为函数想出一个有意义的名称时(它可能做得太多了)

在我看来,答案是: 当它做了太多的事情。 函数应该只执行从函数本身的名称所期望的操作。 另一件需要考虑的事情是,如果您希望在其他函数中重用某些部分,在这种情况下,将其分割可能会很有用。

假设你正在做 的事情,长度将取决于:

  • 你在做什么
  • 你在使用什么语言
  • 代码中需要处理多少抽象层次

60行可能太长,也可能刚刚好。不过我怀疑时间可能太长了。

我通常会因为需要放置描述下一个代码块的注释而将函数分解。之前进入注释的内容现在进入新函数名。这不是一条硬性规则,但(对我来说)是一条很好的经验法则。比起需要注释的代码,我更喜欢代码自我表达(因为我知道注释通常是撒谎的)

大小与屏幕大小相近(所以去找一个大的轴心宽屏并转动它) ... : -)

不开玩笑,每个函数只有一个逻辑。

积极的一面是,单元测试对于只做一件事情的小逻辑函数来说真的容易得多。做很多事情的大函数更难验证!

乔安

有一件事(从函数名来看,这件事应该是显而易见的) ,但不管怎样,也不过是一屏代码而已。随时可以增加你的字体大小。如果有疑问,将其重构为两个或多个函数。

经验法则: 如果一个函数包含执行某些操作的代码块,而这些代码块与其余代码有所分离,则将其放在一个单独的函数中。例如:

function build_address_list_for_zip($zip) {


$query = "SELECT * FROM ADDRESS WHERE zip = $zip";
$results = perform_query($query);
$addresses = array();
while ($address = fetch_query_result($results)) {
$addresses[] = $address;
}


// now create a nice looking list of
// addresses for the user
return $html_content;
}

好多了:

function fetch_addresses_for_zip($zip) {
$query = "SELECT * FROM ADDRESS WHERE zip = $zip";
$results = perform_query($query);
$addresses = array();
while ($address = fetch_query_result($results)) {
$addresses[] = $address;
}
return $addresses;
}


function build_address_list_for_zip($zip) {


$addresses = fetch_addresses_for_zip($zip);


// now create a nice looking list of
// addresses for the user
return $html_content;
}

这种方法有两个优点:

  1. 当您需要获取某个邮政编码的地址时,可以使用现成的函数。

  2. 当您需要再次读取函数 build_address_list_for_zip()时,您知道第一个代码块将执行什么操作(它获取特定邮政编码的地址,至少您可以从函数名中获取这些地址)。如果将查询代码留在内联中,则首先需要分析该代码。

另一方面(我会否认我告诉过你这些,即使是在严刑逼供的情况下) : 如果你读了很多关于 PHP 优化的文章,你可能会想到尽可能地减少函数的数量,因为函数调用在 PHP 中是非常非常昂贵的。我不知道,因为我从来没有做过任何基准。如果是这种情况,如果您的应用程序对性能非常敏感,那么您最好不要遵循任何问题的答案; ——]

下面是一个红色标志的列表(没有特定的顺序) ,它可能表明一个函数太长:

  1. 深度嵌套的控制结构 : 例如 for-loop 深度为3级,甚至仅为2级,嵌套的 if-语句具有复杂的条件。

  2. 太多的 国家定义参数 : 在 状态定义参数状态定义参数中,我指的是一个函数参数,它保证函数的特定执行路径。如果这类参数太多,你就会有一个执行路径的组合爆炸(这通常与 # 1同时发生)。

  3. 在其他方法 中重复的逻辑: 糟糕的代码重用是对单一过程代码的巨大贡献。许多这样的 逻辑复制可以非常微妙,但一旦重构,最终的结果可以是一个更优雅的设计。

  4. 过多的类间耦合 : 这种缺乏适当封装的情况导致函数关注其他类的亲密特征,从而延长了它们。

  5. 不必要的开销 : 注释指出明显的、深度嵌套的类、私有嵌套类变量的多余的 getter 和 setter,以及异常长的函数/变量名都会在相关函数中产生语法噪音,最终增加它们的长度。

  6. 您的大型开发级显示器不足以显示它 : 实际上,今天的显示器足够大,以至于一个接近其高度的函数可能太长了。但是,如果它是 更大,这是一个确凿的证据,有些事情是错误的。

  7. 你不能立即确定函数的目的 : 此外,一旦你确定了 的目的,如果你不能用一句话概括这个目的或者碰巧有一个巨大的头痛,这应该是一个线索。

总之,整体功能可以产生深远的影响,往往是重大设计缺陷的症状。每当我遇到需要阅读的绝对 快乐代码时,它的优雅立即显现出来。你猜怎么着: 函数的长度通常是 非常短的。

我个人的启发是,如果我不能看到整个事情不滚动太长。

我同意一个函数应该只做一件事,但在什么层次上是一件事。

如果你的60行代码正在完成一件事(从程序的角度来看) ,而组成这60行代码的部分不会被其他任何东西使用,那么60行代码就可以了。

除非你能把它们分解成独立的混凝土块,否则分解它们没有任何实际的好处。要使用的度量标准是功能性,而不是代码行。

我参与过很多项目,其中作者把唯一的一件事情做到了极致,最终做到的只是让它看起来像是有人拿着手榴弹炸了一个函数/方法,然后把它炸成了几十个无关的部分,很难理解。

在提取该函数的各个部分时,还需要考虑是否会增加任何不必要的开销,并避免传递大量数据。

我相信关键点是在这么长的功能中寻找可重用性,并将这些部分提取出来。剩下的就是函数了,不管它是10行、20行还是60行。

请记住,您最终可能只是为了重构而重构,这可能会使代码比最初更加不可读。

我以前的一个同事有一个奇怪的规则: 一个函数/方法必须只包含4行代码!他试图严格地坚持这一点,以至于他的方法名经常变得重复和无意义,加上调用变得深度嵌套和混乱。

所以我自己的口头禅就是: 如果你不能为你正在重构的代码块想出一个像样的函数/方法名,那就别费心了。

我认为这里有一个巨大的警告“只做一件事”的咒语在这一页。有时候做一件事就会变化很多。如果较小的函数最终拥有较长的参数列表,不要将一个较长的函数分解成一堆较小的函数。这样做只会把一个函数变成一组高度耦合的函数,而且没有真正的单个值。

关于这个主题已经有一些深入的研究,如果你想要最少的错误,你的代码不应该太长。但也不能太短。

我不同意一个方法应该放在一个显示器上,但是如果你向下滚动超过一个页面,那么这个方法就太长了。

最佳班级规模 面向对象软件供进一步讨论

我以前写过500行函数,但是这些只是用于解码和响应消息的大开关语句。当单个消息的代码变得比单个 if-then-else 更复杂时,我将其提取出来。

本质上,虽然功能是500行,独立维护的区域平均5行。

让我们来看一下 McCabe 的圈,他把他的代码分解成一个图,“图中的每个节点对应于程序中的一个代码块,其中流是连续的,弧对应于程序中采用的分支。”

现在假设您的代码没有函数/方法; 它只是一个图形形式的巨大代码蔓延。

您需要将这个扩展分解为方法。考虑到,当您这样做时,每个方法中将有一定数量的块。每个方法只有一个块对所有其他方法可见: 第一个块(我们假设您只能在一个点跳入一个方法: 第一个块)。每个方法中的所有其他块都是隐藏在该方法中的信息,但方法中的每个块可能会跳转到该方法中的任何其他块。

要根据每个方法的块数来确定方法的大小,您可能要问自己的一个问题是: 需要使所有块之间的依赖项(MPE)的最大潜在数量最小化到多少个方法?

这个答案是由一个等式给出的。如果 r 是使系统 MPE 最小的方法的数目,n 是系统中块的数目,那么方程是: R = sqrt (n)

可以看出,这给出了每个方法的块数,也是 sqrt (n)。

我通常使用测试驱动的方法来编写代码。在这种方法中,函数大小通常与测试的粒度有关。

如果测试的焦点足够集中,那么它将导致您编写一个小的、集中的函数来使测试通过。

这也适用于另一个方向。函数需要小到足以进行有效测试。因此,在处理遗留代码时,我经常发现要分解较大的函数,以便测试它们的不同部分。

我通常会问自己“这个函数的责任是什么”,如果我不能用一个简洁明了的句子来说明这个责任,然后把它转化成一个小型的集中测试,我会怀疑这个函数是否太大了。

前一阵子 Bob 叔叔的 tweet 精神得到了延伸,当您觉得需要在两行代码之间放置一个空行时,您就知道一个函数变得太长了。其思想是,如果您需要一个空行来分隔代码,那么它的责任和范围就在这一点上分开了。

如果它有三个以上的分支,通常这意味着应该将一个函数或方法分开,以便将分支逻辑封装在不同的方法中。

每个 for 循环、 if 语句等都不会被视为调用方法中的一个分支。

Cobertura for Java 代码(我相信其他语言也有其他工具)计算每个函数的 if 等的数量,然后求和为“平均循环复杂度”。

如果一个函数/方法只有三个分支,那么它将得到该度量的三个分支,这非常好。

有时很难遵循这个准则,即验证用户输入。尽管如此,将分支放在不同的方法中不仅有助于开发和维护,而且还有助于测试,因为执行分支的方法的输入可以很容易地进行分析,以查看需要向测试用例添加哪些输入,以覆盖未覆盖的分支。

如果所有的分支都在一个方法中,那么从方法开始就必须跟踪输入,这会妨碍可测试性。

我的想法是,如果我不得不问自己,它是否太长,它可能太长。它有助于在这个领域制作更小的函数,因为它可以在应用程序生命周期的后期提供帮助。