什么时候功能太长了?

时间:2009-01-24 07:13:40

标签: function refactoring coding-style

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. 避免深层嵌套控制结构
  9. 感谢大家的答案,编辑列表并投票选出正确的答案我会选择那个;)

    我现在正在重构这些想法:)

24 个答案:

答案 0 :(得分:199)

这是一个红色标记列表(没有特别的顺序),表明函数太长了:

  1. 深层嵌套控制结构:例如对于具有复杂条件的嵌套if语句,for循环3级深度甚至只有2级深度。

  2. 状态定义参数太多:通过状态定义参数,我的意思是一个保证特定执行的函数参数通过功能的路径。获取太多这些类型的参数,并且你有一个组合的执行路径爆炸(这通常与#1一起发生)。

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

  4. 过多的类间耦合:缺乏适当的封装会导致函数关注其他类的密切特性,从而延长它们。

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

  6. 您的大量开发人员级别的显示器还不足以显示它:实际上,今天的显示器足够大,以至于任何接近其高度的功能都可能也是如此长。但是,如果它更大,这就是冒烟的问题。

  7. 您无法立即确定该功能的用途:此外,一旦您实际 确定其目的,如果您无法总结此目的单句或碰巧有一个巨大的头痛,这应该是一个线索。

  8. 总之,整体功能可能会产生深远的影响,并且通常是主要设计缺陷的症状。每当我遇到一个绝对 joy 的代码时,它的优雅就会立刻显现出来。猜猜看:这些函数通常非常很短。

答案 1 :(得分:74)

没有真正的硬性规则。一般来说,我喜欢我的方法只是“做一件事”。因此,如果它正在抓取数据,然后对该数据执行某些操作,然后将其写入磁盘,那么我将拆分并写入单独的方法,因此我的“主要”方法只包含“做某事”。

虽然“做某事”仍然可能只有几行,所以我不确定多少行是正确使用的指标:)

编辑:这是我上周在工作中邮寄的一行代码(为了证明一点......这不是我养成的习惯:)) - 我当然不希望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();

答案 2 :(得分:26)

我认为这页上的“只做一件事”的口头禅是一个巨大的警告。有时候做一件事就是玩弄很多变数。如果较小的函数最终具有长参数列表,请不要将长函数分解为一堆较小的函数。这样做只会将一个功能转变为一组高度耦合的功能而没有真正的个人价值。

答案 3 :(得分:19)

一个函数应该只做一件事。如果你在一个函数中做了很多小事,那就把每个小事都做成一个函数,并从long函数中调用这些函数。

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

答案 4 :(得分:15)

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

如果你的60行正在完成一件事(从程序的角度来看),那些构成这60行的作品将不会被其他任何东西使用,那么60行是可以的。

除非你能把它分解成独立的具体部分,否则分解它并没有什么好处。要使用的指标是功能而不是代码行。

我曾经参与过很多项目,其中作者把唯一的一件东西推到了一个极端的水平,所有它最终做的就是让它看起来像有人拿了手榴弹到一个功能/方法并把它吹成几十个未连接的那些难以理解的作品。

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

我认为关键是要在那个长函数中寻找可重用性并将这些部分拉出来。剩下的就是功能,无论是10行,20行还是60行。

答案 5 :(得分:11)

60行很大但功能不太长。如果它适合编辑器中的一个屏幕,您可以一次看到它。这实际上取决于功能的作用。

为什么我可能会分手:

  • 太长了
  • 通过分解代码并为新功能使用有意义的名称,使代码更易于维护
  • 功能不具有凝聚力
  • 部分功能本身很有用。
  • 当难以为该功能提出有意义的名称时(可能做得太多)

答案 6 :(得分:6)

看看McCabe的圈外音,他将代码分解成一个图形,其中“图中的每个节点对应于程序中的一个代码块,其中流程是顺序的,弧线对应于分支中的分支该计划。“

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

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

要根据每个方法的块数确定方法的大小,您可能会问自己的一个问题是:我应该使用多少方法来最小化所有块之间的最大潜在依赖关系数(MPE)? / p>

答案由一个等式给出。如果r是最小化系统MPE的方法的数量,并且n是系统中的块数,那么等式是: r = sqrt(n)

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

答案 7 :(得分:6)

大小约为你的屏幕尺寸(所以去获得一个大转轴宽屏并转动它)......: - )

开玩笑说,每个功能都是合乎逻辑的。

积极的一点是,对于做一件事的小型逻辑功能来说,单元测试真的要容易得多。做很多事情的大功能很难验证!

/约翰

答案 8 :(得分:6)

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

答案 9 :(得分:6)

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

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()时,你知道第一个代码块将要做什么(它获取某个邮政编码的地址,至少这是你可以从中得到的功能名称)。如果您将查询代码保留为内联,则首先需要分析该代码。

  3. [另一方面(我会否认我告诉过你,即使是在折磨下):如果你阅读了很多关于PHP优化的内容,你可以想到保持函数数量尽可能小,因为函数调用在PHP中非常非常昂贵。我不知道,因为我从未做过任何基准测试。如果是这种情况,如果您的应用程序非常“性能敏感”,您可能会更好地不遵循您的问题的任何答案;-)]

答案 10 :(得分:4)

请记住,为了重新考虑因素,您最终可能会重新进行重新分解,这可能会使代码比以前更难以理解。

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

所以我自己的口头禅已成为:如果你不能想到你正在重新分解的代码块的一个不错的函数/方法名称,那就不用费心了。

答案 11 :(得分:2)

我通常打破一个功能的主要原因是因为它的一些部分也是我正在编写的另一个附近函数的成分,因此公共部分被排除在外。此外,如果它正在使用其他类中的许多字段或属性,那么很有可能相关的块可以被批量提取,如果可能的话移动到另一个类中。

如果您的代码块顶部有注释,请考虑将其拉出到函数中,函数和参数名称说明其用途,并保留代码基本原理的注释。

你确定那里没有其他地方有用的东西吗?它有什么功能?

答案 12 :(得分:2)

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

如果您的测试足够集中,那么它将引导您编写一个小型聚焦函数以使测试通过。

这也适用于另一个方向。功能需要足够小才能有效测试。因此,在使用遗留代码时,我经常会发现我为了测试它们的不同部分而分解更大的函数。

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

答案 13 :(得分:2)

我通常需要放置描述下一个代码块的注释来破坏函数。之前进入评论的内容现在进入新的函数名称。这不是一个硬性规则,但(对我来说)是一个很好的经验法则。我喜欢代码说自己比需要评论的代码更好(因为我已经知道评论通常是谎言)

答案 14 :(得分:2)

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

答案 15 :(得分:1)

对这个主题进行了一些深入的研究,如果你想要最少的bug,你的代码不应该太长。但它也不应该太短。

我不同意一个方法应该适合你的显示器,但如果你向下滚动超过一页,那么该方法太长了。

请参阅 The Optimal Class Size for Object-Oriented Software进一步讨论。

答案 16 :(得分:1)

我之前编写过500行函数,但这些只是用于解码和响应消息的大转换语句。当单个消息的代码比单个if-then-else更复杂时,我将其解压缩出来。

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

答案 17 :(得分:1)

这部分是一个品味的问题,但我如何判断这是我尝试保持我的功能大致只在我的屏幕上一次(最大)。原因是,如果你能够立刻看到整件事情,那么更容易理解发生了什么。

当我编写代码时,它可以编写长函数,然后重构以提取可以被其他函数重用的位 - 并且编写执行离散任务的小函数。

我不知道对此有任何正确或错误的答案(例如,您可能会以67行作为最大值,但有时可能会添加更多一行)。

答案 18 :(得分:0)

有一件事(那个东西应该从函数名称中显而易见),但不过是一屏代码。并随意增加您的字体大小。如果有疑问,请将其重构为两个或更多功能。

答案 19 :(得分:0)

假设您正在做一个事物,长度将取决于:

  • 你在做什么
  • 您正在使用的语言
  • 您需要在代码中处理多少抽象级别

60行可能太长或者可能恰到好处。我怀疑它可能太长了。

答案 20 :(得分:0)

我怀疑你会在此找到很多答案。

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

例如,如果你有一个处理某种字符串输入并返回字符串结果的函数,你可能会根据将字符串拆分成部分的逻辑,添加额外字符和逻辑的逻辑来破坏函数。将它们全部重新组合在一起作为格式化结果。

简而言之,无论是什么让你的代码干净易读(无论是通过简单地确保你的功能有好的评论或分解它)都是最好的方法。

答案 21 :(得分:0)

一段时间后,从Bob叔叔那里扩展推文的精神,你知道当你觉得需要在两行代码之间加一个空行时,函数会变得太长。我们的想法是,如果您需要一个空白行来分隔代码,那么它的责任和范围就会分开。

答案 22 :(得分:0)

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

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

Cobertura for Java代码(我确信还有其他语言的其他工具)计算每个函数的函数中if等的数量,并将其与“平均圈复杂度”相加。

如果一个函数/方法只有三个分支,它将在该指标上得到三个,这非常好。

有时很难遵循本指南,即验证用户输入。尽管如此,在不同方法中设置分支不仅有助于开发和维护,还有助于测试,因为可以轻松分析执行分支的方法的输入,以查看需要将哪些输入添加到测试用例以覆盖分支没有被覆盖。

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

答案 23 :(得分:0)

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