如果长函数具有内部结构,是否可以接受?


23

当使用支持嵌套函数(例如Python和D)的语言处理复杂算法时,我经常编写大型函数(因为算法很复杂),但是通过使用嵌套函数来构造复杂代码来减轻这种情况。即使大型功能(超过100行)仍通过使用嵌套功能在内部进行了结构合理,仍然被认为是邪恶的?

编辑:对于那些不熟悉Python或D的人,这些语言中的嵌套函数还允许访问外部函数范围。在D中,此访问权限允许外部范围内的变量发生突变。在Python中,它仅允许读取。在D中,可以通过声明显式禁用对嵌套函数中的外部范围的访问static


5
我定期编写400多种行函数/方法。一定要把那500个案例的转换声明放在某处:)
卡勒姆·罗杰斯

7
@Callum Rogers:在Python中,您将使用查找字典/映射,而不是使用500个大小写转换语句。我相信它们比具有500左右的switch-case语句优越。您仍然需要500行字典定义(或者,在Python中,您可以使用反射来动态枚举它们),但是字典定义是数据,而不是代码。与具有较大的功能定义相比,具有较大的数据定义的烦恼要少得多。此外,数据比代码更健壮。
Lie Ryan 2010年

每当看到具有大量LOC的功能时,我都会感到畏缩,这使我想知道模块化的目的是什么。它更易于遵循逻辑并使用较小的功能进行调试。
Aditya P

古老而熟悉的Pascal语言还允许访问外部作用域,GNU C中的嵌套函数扩展也是如此。(这些语言仅允许向下的funargs,但是:作为携带范围的函数的参数只能是传递,而不返回,这将需要完整的词法关闭支持)。
卡兹(Kaz)2013年

函数式编程的一个很好的例子是响应式编程时编写的组合器,这些函数往往很长。它们很容易命中三十行,但是如果分开,则会丢失两倍大小,因此会丢失。
Craig Gidney 2014年

Answers:


21

永远记住规则,函数可以做一件事,并且做得很好!如果可以,请避免使用嵌套函数。

它阻碍了可读性和测试。


9
我一直讨厌这个规则,因为在较高的抽象级别查看时执行“一件事情”的函数在较低的级别查看时可能会执行“很多事情”。如果应用不正确,“一件事”规则可能会导致API的粒度过细。
dsimcha's

1
@dsimcha:不能误用什么规则并导致问题?当某人在有用的地方应用“规则” /陈词滥调时,只需提出另一条规则:所有概括都是错误的。

2
@Roger:一个合理的抱怨,除了恕我直言,该规则在实践中经常被错误地使用以证明过度细粒度,过度设计的API是合理的。这会带来具体的成本,因为对于简单的常见用例而言,API变得更难,更冗长。
dsimcha's

2
“是的,该规则被错误地应用,但是该规则被错误地应用了太多!”?:P

1
我的功能做一件事,并且做得很好:即占据27个屏幕。:)
Kaz

12

有人认为,短函数比长函数更容易出错

Card and Glass(1990)指出,设计复杂性实际上涉及两个方面:每个组件内部的复杂性和组件之间关系的复杂性。

就个人而言,我发现注释良好的直线代码(将其分解成多个在其他地方从未使用过的功能)更容易遵循(尤其是当您不是最初编写它的人时)。但这确实取决于情况。

我认为主要的收获是,当您分割一块代码时,您正在用一种复杂性交换另一种。中间某处可能有一个最佳位置。


您阅读过“清洁代码”吗?它详细讨论了这一点。amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/...
马丁威克曼

9

理想情况下,无需滚动即可查看整个功能。有时,这是不可能的。但是,如果您可以将其分解成碎片,那么它将使阅读代码变得更加容易。

我知道,只要按上一页或下一页或移到代码的不同部分,我就只记得前一页的7 +/- 2件事。不幸的是,其中一些位置将在阅读新代码时使用。

我总是喜欢考虑我的短期记忆,例如计算机的寄存器(CISC,而不是RISC)。如果您在同一页面上拥有全部功能,则可以转到缓存以从程序的另一部分获取所需的信息。如果整个功能不能放在一个页面上,则相当于在每次操作后始终将任何内存推入磁盘。


3
您只需要在矩阵打印机上将其打印出-一次看到10米长的代码即可:)

或获得分辨率更高的显示器
frogstarr78

并以垂直方向使用它。
Calmarius 2013年

4

为什么要使用嵌套函数,而不是普通的外部函数?

即使仅在您以前使用的大型函数中使用了外部函数,它仍然使整个混乱更容易阅读:

DoThing(int x){
    x += ;1
    int y = FirstThing(x);
    x = SecondThing(x, y);
    while(spoo > fleem){
        x = ThirdThing(x+y);
    }
}

FirstThing(int x){...}
SecondThing(int x, int y){...}
ThirdThing(int z){...}

2
两个可能的原因:混乱的名称空间,以及我称之为“词法邻近”的词法。这些可能是好是坏的原因,具体取决于您的语言。
Frank Shearar 2010年

但是嵌套函数的效率可能较低,因为它们需要支持向下的funarg,并且可能需要完整的词法闭包。如果语言的优化不佳,则可能会做额外的工作来创建动态环境以传递给子函数。
卡兹(Kaz)2013年

3

目前我还没有这本书(引用),但是根据Code Complete的说法,根据他的研究,函数长度的“最佳点”约为25至50行代码。

有时候可以使用长函数:

  • 当函数的圈复杂度较低时。如果您的开发人员不得不查看包含巨大if语句和针对if的else语句的函数同时不在屏幕上,他们可能会感到有些沮丧。

不能使用长函数的时候:

  • 您有一个带有深层嵌套条件的函数。请您的其他代码阅读者帮忙,通过破坏功能来提高可读性。一个函数向它的读者提供一个指示,即“这是一块完成某件事的代码”。也要问问自己,函数的长度是否表明它做得太多,是否需要将其分解为另一个类。

最重要的是,可维护性应该是列表中的最高优先事项之一。如果另一个开发人员看不到您的代码并在不到5秒钟的时间内“了解”代码的运行情况,那么您的代码就无法提供足够的“元数据”来说明其运行情况。其他开发人员应该仅通过查看所选IDE中的对象浏览器就能知道您的类正在做什么,而不必阅读100多行代码。

较小的功能具有以下优点:

  • 可移植性:迁移功能要容易得多(可以在类中重构)。
  • 调试:当您查看具有25行代码而不是100行代码的函数时,查看stacktrace可以更快地查明错误。
  • 可读性-函数的名称表明整个代码块在做什么。如果您的团队中的开发人员不使用它,则可能不想阅读该块。另外,在大多数现代IDE中,另一个开发人员可以通过在对象浏览器中读取函数名称来更好地了解您的类在做什么。
  • 导航-大多数IDE都可以让您搜索功能名称。另外,大多数现代IDE都具有在另一个窗口中查看功能源的功能,这使其他开发人员可以在2个屏幕上查看您的长功能(如果有多个显示器),而无需滚动它们。

清单继续.....



2

我不喜欢大多数嵌套函数。Lambda属于该类别,但除非它们的字符数超过30至40,否则通常不会标记我。

根本原因是它成为具有内部语义递归的高度局部密集的函数,这意味着我很难动脑筋,并且将某些内容推送到一个不会使代码空间混乱的辅助函数会更容易。

我认为功能应该发挥作用。做其他事情就是其他功能。因此,如果您有200行功能“做事情”,而且一切顺利,那就可以了。


有时,您必须将大量上下文传递给外部函数(可以方便地将其作为本地函数访问),以使函数调用方式比函数调用方式复杂得多。
卡兹(Kaz)

1

可以接受吗?这只是一个您只能回答的问题。该功能是否达到了所需?它可以维护吗?团队的其他成员“可以接受”吗?如果是这样,那才是真正重要的。

编辑:我没有看到有关嵌套函数的事情。就个人而言,我不会使用它们。我会改用常规函数。


1

较长的“直线”功能可以是一种明确的方法,用于指定总是以特定顺序出现的冗长的步骤序列。

但是,正如其他人提到的那样,这种形式倾向于具有局部复杂性,其中总体流程不太明显。将局部复杂性放入嵌套函数(即:在long函数中的其他位置定义,可能在顶部或底部)可以使主线流程恢复清晰。

第二个重要的考虑因素是控制仅在长函数的局部范围内使用的变量范围。需要保持警惕,以避免在一个代码段中引入一个变量而在其他地方无意中引用了该变量(例如,在编辑循环之后),因为这种错误不会显示为编译或运行时错误。

在某些语言中,此问题很容易避免:可以将本地代码段包装在其自己的块中,例如使用“ {...}”,在其中,任何新引入的变量仅对该块可见。某些语言(例如Python)缺少此功能,在这种情况下,本地函数对于强制使用较小的作用域区域很有用。


1

不可以,多页功能是不可取的,并且不应通过代码审查。 我也曾经写过长函数,但是在阅读了Martin Fowler的Refactoring之后,我停了下来。长函数难以正确编写,难以理解且难以测试。我从来没有见过即使只有50行的功能,如果将其拆分为一组较小的功能,也不会更容易理解和测试。在多页函数中,几乎可以肯定应该排除整个类。很难具体说明。也许您应该将长期功能之一发布到Code Review上,然后有人(也许是我)可以向您展示如何改进它。


0

当我用python编程时,我想在编写函数后退后一步,并问自己是否遵守“ Python禅”(在python解释器中键入“ import this”):

美丽胜于丑陋。
显式胜于隐式。
简单胜于复杂。
复杂胜于复杂。
扁平比嵌套更好。
稀疏胜于密集。
可读性很重要。
特殊情况还不足以打破规则。
尽管实用性胜过纯度。
错误绝不能默默传递。
除非明确地保持沉默。
面对模棱两可的想法,拒绝猜测的诱惑。
应该有一种-最好只有一种-显而易见的方法。
尽管除非您是荷兰人,否则一开始这种方式可能并不明显。
现在总比没有好。
虽然永远不会总比正确要好现在。
如果实现难以解释,那是个坏主意。
如果实现易于解释,则可能是个好主意。
命名空间是一个很棒的主意-让我们做更多这些吧!


1
如果显式优于隐式,我们应该使用机器语言进行编码。即使在汇编器中也不是;它会执行令人讨厌的隐式操作,例如自动用指令填充分支延迟槽,计算相对分支偏移以及解析全局变量之间的符号引用。伙计,这些愚蠢的Python用户和他们的小宗教...
Kaz 2013年

0

将它们放在单独的模块中。

假设您的解决方案没有需要太多的选择而不是肿。您已经将功能拆分为不同的子功能,因此问题是应该将其放在哪里:

  1. 将它们放在只有一个“公共”功能的模块中。
  2. 将它们放在只有一个“公共”(静态)功能的类中。
  3. 将它们嵌套在一个函数中(如前所述)。

现在,第二个和第三个选择在某种意义上都是嵌套的,但是第二个选择对于某些程序员来说似乎还不错。如果您不排除第二种选择,那么我看不出有太多理由排除第三种选择。

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.