函数何时过长?[关闭]


130

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. 避免深层嵌套的控制结构

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

我现在正在考虑这些想法:)


您的问题有错别字,我想您的意思是“什么时候函数太长?”。
汤姆(Tom)

1
您通过用代码行来摆出一个问题而使这个问题错了。决定因素不是用代码行来衡量的。
dkretz

根据代码和语言,这个问题可能会变得复杂。也许你可以发布它。
2009年

如果遵守单一责任原则,那就去做。我通常感觉需要做一个标题或每20行代码,这标志着我要抽象它,并使用有意义的名称将此片段命名为一个函数,而不是创建章节标题。
Yevgeniy Afanasyev

Answers:


75

没有真正的硬性规定。通常,我喜欢我的方法只是“做一件事”。因此,如果它正在获取数据,然后对该数据进行处理,然后将其写入磁盘,那么我会将捕获内容分开并写入单独的方法,以便我的“ 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();

1
大声笑好吧,我可以删除方法中的所有空格,它只会是很长的一行,而不是长函数。做一件事,那可能是答案,而不是,谢谢

@Movaxes我发布的代码片段只是一个语句,而不是一行上只有很多行..里面没有分号:)我本可以每次扩展GetResources()使其变得更加邪恶:P
史蒂文·罗宾斯

是的,这很有意义。为什么不将整个源文件放在一行中呢?我的意思是,那么您真正成为了Web 2.0“忍者” :)
BobbyShaftoe,2009年

我记得在旧杂志(我说的是BBC Micro的旧杂志)上,他们曾经有“ 10行程序”,每行只有几个语句,直到BBC可以处理的最大长度。.他们总是很痛苦的键入:D
Steven Robbins

6
我喜欢函数的概念只做一件事,但是。如果您有一个函数可以执行10件事,而您将其中9件事移到了单独的函数中,则其余函数仍会调用该函数,这并不是说剩余函数本质上仍在执行10件事!我确实认为像这样分解功能可以使测试变得更加容易。
mtnpaul 2012年

214

以下是红色标志列表(不分先后顺序),它们可能表示功能过长:

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

  2. 状态定义参数太多:通过状态定义参数,我的意思是一个函数参数,它保证通过函数的特定执行路径。这些参数类型太多了,执行路径就会组合爆炸(通常与#1一起发生)。

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

  4. 类之间的过度耦合:这种缺乏适当的封装导致功能与其他类的亲密特性有关,因此加长了它们。

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

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

  7. 你不能立即判断函数的目的:此外,一旦你真正决定的目的,如果你不能概括这个目的在一个句子或碰巧有一个巨大的头痛,这应该是一个线索。

总之,单片功能可能会产生深远的影响,并且通常是主要设计缺陷的征兆。每当我遇到绝对令人愉悦的代码时,它的优雅便立即显现出来。想一想:这些函数的长度通常短。


1
好贴!非常确定性
Chuck Conway

2
确实是@PedroMorteRolo。标准API并不总是优雅的模型。此外,许多Java API都是在对Java编译器和JVM有深入了解的情况下开发的,因此您可能会出于性能方面的考虑而对其进行解释。我承认不能浪费一毫秒的关键代码段可能不得不打破其中一些规则,但是应该始终将其视为特殊情况。预先花费额外的开发时间是一项初始投资,可以避免未来(可能会瘫痪)的技术债务。
瑞安·德卢基

2
顺便说一句..我认为长方法坏启发式也适用于类。恕我直言,长类很糟糕,因为长类倾向于违反单一责任原则。让编译器同时针对长类和方法发出警告会很有趣....
Pedro Rolo

3
@PedroMorteRolo我绝对同意这一点。此外,大型类可能具有更多的可变状态:这导致很难维护的代码。
瑞安·德卢基

1
最佳答案。另一个好的线索是:代码中的注释是什么样的?我偶然发现某人的代码的次数为:// fetch Foo's credentials where Bar is "uncomplete"。那几乎可以肯定是一个函数名,应该解耦。可能希望将其重构为以下内容: Foo.fetchCredentialWhereBarUncomplete()
Jay Edwards

28

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


18

函数只能做一件事。如果要在一个函数中执行许多小操作,请将每个小事情都设为一个函数,然后从long函数中调用这些函数。

你真的想要做的就是复制和每10行的长期功能分为短功能的粘贴(如你的例子说明)。


是的,使用复制和粘贴模式制作许多小功能不是一个好主意,我同意一个功能应始终尝试仅做一件事

“做一件事”可能正确也可能不正确,具体取决于粒度。如果一个函数乘以一个矩阵,那很好。如果某个功能可以构建虚拟汽车,那是“一回事”,但这也是非常重要的。可以使用多个功能逐个组件地构建汽车。
void.pointer 2012年

16

我同意一个函数应该只做一件事,但是那件事在什么水平上呢。

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

分解它并没有真正的好处,除非您可以将其分解为独立的具体碎片。使用的度量标准是功能而不是代码行。

我参与过许多程序,作者将唯一的事情带到了一个极端的水平,而最终所做的只是使它看起来像有人向功能/方法投掷手榴弹,并将其炸裂成数十个未连接的部分。很难遵循。

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

我认为关键是要在该长功能中寻找可重用性,然后将那些零件拉出来。剩下的就是函数,无论它是10、20还是60行。


2
+1“使用的指标是功能而不是代码行”
Cody Piersall

另一个重要指标是块嵌套的级别数。保持最小。将功能分解为较小的部分通常会有所帮助。其他事情也可以提供帮助,例如多次回报。
user2367418'1

10

60行很长,但对于功能而言却不太长。如果它适合编辑器中的一个屏幕,则可以一次看到所有内容。这实际上取决于功能在做什么。

为什么我可能会分解功能:

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

3
我同意,如果您必须将函数命名为DoThisAndThisAndAlsoThis,可能也做得太多。谢谢:)

2
你和这个伴侣不合时宜。60行将永远太多。我想说的是,如果您要关闭10条线,则可能接近极限。
willcodejavaforfood

但是另一个函数仍在调用这些函数,并且本质上是相同的DoThisAndThisAndAlsoThis函数,但是具有很多抽象性,您仍然必须以某种方式命名
Timo Huovinen


5

大小约为您的屏幕尺寸(因此,请打开大的枢轴宽屏并将其打开)... :-)

除了玩笑,每个函数一个逻辑的东西。

积极的一面是,使用执行1项功能的小型逻辑功能,单元测试实际上要容易得多。做很多事情的大型函数更难验证!

/约翰


关于单元测试的要点:)

5

经验法则:如果某个函数包含执行某些功能的代码块,而这些代码块与其余代码有些脱节,则将其放在单独的函数中。例:

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中非常昂贵。我不知道这件事,因为我从未做过任何基准测试。如果是这样的话,如果您的应用程序对性能非常敏感,那么最好不遵循任何问题的答案;


感谢很好的例子:)我将用php

5

看看McCabe的圈数,他将代码分解成一个图形,其中,“图形中的每个节点对应于程序中的代码块,其中流程是顺序的,而圆弧对应于程序中采用的分支。 ”

现在想象您的代码没有功能/方法;它只是图形形式的一大堆代码。

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

为了确定每个方法的块数,方法的大小,您可能会问自己一个问题:为了最小化所有块之间的最大潜在依赖关系(MPE),我应该使用几个方法?

该答案由等式给出。如果r是使系统MPE最小化的方法数,并且n是系统中的块数,则等式为:r = sqrt(n)

可以证明,这使每种方法的块数也为sqrt(n)。


4

请记住,您可能仅出于重构的目的而进行了重构,这可能使代码比起一开始更难以阅读。

我的一位前同事有一个奇怪的规则,即函数/方法只能包含4行代码!他试图如此严格地坚持这一点,以至于他的方法名称经常变得重复而毫无意义,并且调用变得深深地嵌套和令人困惑。

因此,我自己的口号已经变成:如果您无法想到要重构的代码块的恰当的函数/方法名称,请不要打扰。


2

我通常分解一个函数的主要原因是因为它的零碎部分也是我正在编写的另一个附近函数的组成部分,所以共同的部分被排除在外了。另外,如果它正在使用许多其他类中的字段或属性,则很有可能可以将相关的块批发出来,如果可能的话,可以转移到其他类中。

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

您确定其中没有其他地方有用的作品吗?这是什么功能?


该函数基于URL从模板中生成缓存文件,例如来自URL / post / 2009/01/01的post_2009_01_01.html,感谢您的回答

2

我认为答案是:什么时候做太多事情。您的函数应仅执行您希望从函数本身名称中获得的操作。要考虑的另一件事是,是否要在其他部分重用函数的某些部分?在这种情况下,将其拆分可能会很有用。


2

我通常通过放置描述下一个代码块的注释来分解功能。以前在注释中使用的内容现在在新函数名称中使用。这不是硬性规定,但是(对我而言)是一个很好的经验法则。我喜欢代码本身胜于需要注释的代码(因为我了解到注释通常是谎言)


我喜欢评论我的代码,主要是对我自己而不是对其他人来说,这消除了有关$ variable定义位置的许多问题,但我也喜欢该代码可以自我解释。评论在说谎吗?

是的,因为除此以外,还没有维护它们。在撰写本文时,它们可能是正确的,但是一旦引入了错误修正或新功能,就没有人会根据新情况强迫更改注释。IMHO
Olaf Kock

我刚遇到这个答案:stackoverflow.com/questions/406760/…指出“代码中的大多数注释实际上是代码复制的有害形式”。另外-那里的评论很长。
奥拉夫·科克

1

这在某种程度上是一种品味的问题,但是我如何确定呢?我试图将我的功能保持在大约一次只能显示在屏幕上的最大时间(最多)。原因是,如果您可以一次看到整个事件,那么更容易了解正在发生的事情。

当我编写代码时,这是混合编写长函数,然后进行重构以提取可被其他函数重用的位,以及-编写在执行过程中执行离散任务的小函数。

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


好吧,我也想在屏幕上看到我完整的功能:)有时候这意味着Monospace 9字体和黑色背景上的大分辨率,我同意这样更容易理解。

1

在这个主题上已经进行了透彻的研究,如果您想要最少的错误,那么代码就不会太长。但这也不应该太短。

我不同意一种方法应该适合您的显示器,但是如果向下滚动超过一页,则该方法太长。

有关进一步的讨论,请参见 面向对象软件的最佳类大小


感谢您的链接,阅读:)

1

我之前已经编写了500行函数,但是这些只是用于解码和响应消息的大型switch语句。当单个消息的代码比单个if-then-else的代码复杂时,我将其提取出来。

本质上,尽管功能为500行,但独立维护的区域平均为5行。


1

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

如果您的测试足够集中,那么它将导致您编写一个小的集中功能以使测试通过。

这在另一个方向上也起作用。功能必须足够小才能有效测试。因此,在使用遗留代码时,我经常发现我分解了较大的函数以测试它们的不同部分。

我通常会问自己“此功能的职责是什么”,如果我不能用简洁明了的句子陈述职责,然后将其转换为一个小的重点测试,我想知道功能是否太大。


1

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

每个for循环,if语句等在调用方法中均不视为分支。

Cobertura for Java代码(我敢肯定还有其他语言的其他工具)会针对每个函数计算函数中if等的数量,并将其求和,以得出“平均圈复杂度”。

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

有时很难遵循该准则,即验证用户输入。但是,将分支置于不同的方法中不仅有助于开发和维护,还有助于测试,因为可以轻松地分析执行分支的方法的输入,以了解需要将哪些输入添加到测试用例中以覆盖分支。没有被覆盖。

如果所有分支都在一个方法内,则自该方法开始以来就必须跟踪输入,这会影响可测试性。


0

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

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

例如,如果您有一个处理某种字符串输入并返回字符串结果的函数,则可能会根据将字符串分成几部分的逻辑,添加多余字符的逻辑以及放置它的逻辑来分解该函数全部重新格式化在一起。

简而言之,最好的方法是使您的代码干净且易于阅读(无论是通过确保函数具有良好的注释还是将其分解)。


0

假设您正在做件事,则长度取决于:

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

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


我正在用PHP进行缓存,是的,可能60行太多了,正在重构...

0

一件事(从函数名称中应该很明显),但无论如何都不过是一小段代码。并随时增加您的字体大小。如有疑问,请将其重构为两个或多个功能。


0

扩展Bob叔叔的推文精神,您知道当您感到需要在两行代码之间插入空白行时,该函数太长了。这个想法是,如果您需要一个空行来分隔代码,那么此时的职责和范围是分开的。


0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.