长方法总是不好的吗?[关闭]


64

因此,环顾四周,我注意到一些关于长方法是不好的做法的评论。

我不确定我是否总是同意长方法是不好的(并希望别人提出意见)。

例如,我有一些Django视图,在将对象发送到视图之前会对其进行一些处理,一个长方法是350行代码。我编写了代码,以便处理参数-对查询集进行排序/过滤,然后对查询返回的对象进行一点点处理。

因此,处理主要是条件聚合,它具有足够复杂的规则,无法在数据库中轻松完成,因此我在主循环外部声明了一些变量,然后在循环期间对其进行了更改。

variable_1 = 0
variable_2 = 0
for object in queryset :
     if object.condition_condition_a and variable_2 > 0 :
     variable 1+= 1
     .....
     ...    
     . 
      more conditions to alter the variables

return queryset, and context 

因此,根据理论,我应该将所有代码分解为较小的方法,以使视图方法的最大长度为一页。

但是,过去曾经在各种代码基础上工作过,当您需要不断地从一种方法跳转到另一种方法来查明代码的所有部分,同时又将最外层的方法放在脑海中时,有时会发现它使代码的可读性降低。

我发现拥有一个格式合理的长方法,您可以更轻松地看到逻辑,因为它不会被内部方法隐藏。

我可以将代码分解为较小的方法,但是通常会有一个内部循环用于两三件事,因此这会导致代码更复杂,或者方法只会做两三件事而不做一件事(或者我可以为每个任务重复内部循环,但是这样会影响性能。

那么是否存在长方法并不总是不好的情况?当只在一个地方使用这些方法时,总会有一种情况吗?

更新:好像一年多以前我问了这个问题。

因此,在这里(混合)响应之后,我重构了代码,将其拆分为方法。这是一个Django应用,可从数据库中检索复杂的相关对象集,因此测试参数不可用(可能一年中的大部分时间都需要为测试用例创建相关对象。我有一个“昨天需要做的事情”类型)工作环境,然后再抱怨)。现在,修复该代码部分中的错误现在稍微容易一些,但并不是那么容易。

之前:

#comment 1 
bit of (uncomplicated) code 1a  
bit of code 2a

#comment 2 
bit of code 2a
bit of code 2b
bit of code 2c

#comment 3
bit of code 3

现在:

method_call_1
method_call_2
method_call_3

def method_1 
    bit of (uncomplicated) code 1a  
    bit of code 2a

def method_2 
    bit of code 2a
    bit of code 2b
    bit of code 2c

def method_3
    bit of code 3

156
所有的绝对都是不好的。总是。
约阿希姆·绍尔

30
我经常(以这种形式或类似形式)看到“提取方法,如果您可以重复使用它们”的说法,但我不赞成这样做:如果该方法做的事情不止一件事,则应该从中提取方法以提高可读性/ 即使仅从代码中的一个位置调用这些新方法,也具有可维护性。
约阿希姆·绍尔

4
@gnat:哇,在构建步骤中某种形式的手动内联(通过预处理器)是不是更好的解决方案?
约阿希姆·绍尔

11
我认识的最好的程序员之一评论说,如果您真的想要一种度量,则局部变量的数量比实际长度要好。他正在研究一种复杂的路径优化器,其中,胆量是一种长达数百行的方法,但是保留的状态(局部变量)的数量却很少。
Chuu 2012年

2
长方法并不总是坏的。但是您应该经常看这些东西,然后问自己“这不好吗?”
Carson63000

Answers:


80

不,长方法并不总是坏的。

在《代码完整》一书中,据测算,长方法有时会更快,更容易编写,并且不会导致维护问题。

实际上,真正重要的是保持DRY并尊重关注点分离。有时,计算时间很长,但实际上将来不会引起问题。

但是,从我的个人经验来看,大多数较长的方法往往缺乏关注点分离。实际上,长方法是一种检测代码中可能存在问题的简便方法,并且在进行代码审查时需要特别注意。

编辑:做评论时,我在答案中添加了一个有趣的观点。实际上,我还将检查功能的复杂性指标(NPATH,循环复杂性或什至更好的CRAP)。

实际上,我建议不要在长功能上检查此类指标,而应在每个功能上都使用自动工具(例如Java的checkstyle)对它们进行警告。


41
+1:“不,长方法并不总是不好”,但它们几乎总是不好
Binary Worrier 2012年

67
长方法主体是一种经典的代码味道:它本身并不是问题,但这表明那里可能存在问题。
约阿希姆·绍尔

6
+1,但我仍然建议您检查long方法的圈复杂度。较高的值表示实际上无法进行单元测试的方法(而较长的方法很少缺少控制流逻辑)。
Daniel B

11
我使用方法名称来最小化注释。有时会导致诸如“ getSelectedNodeWithChildren”之类的事情,但是我的同事不断告诉我,我的代码可读性强。我也尽量避免缩写,它们写的很好,但阅读却不太好。
K..

4
@ da_b0uncer这也是我遵循的政策。读取代码比编写代码更难,因此在编写时要付出更多的努力才能使代码更易读。
deadalnix 2012年

55

这里的大多数焦点似乎总是围绕这个词。是的,绝对是不好的,软件工程几乎和科学一样是艺术,而所有这些……但是我不得不说,对于您给出的示例,如果将其拆分,该方法会更好。起来。这些是我通常用来证明拆分方法合理性的参数:

可读性:我不确定其他人,但是我无法快速阅读350行代码。是的,如果它是我自己的代码,并且我可以对它做很多假设,那么我可以非常快速地浏览它,但这不重要。考虑一下,如果该方法包含10个对其他方法的调用(每个方法都有一个描述性名称),那么该方法将更容易阅读。为此,您已经在代码中引入了分层,高级方法为读者提供了一个简短的,甜美的概述,以了解发生了什么事情。

编辑-换种方式考虑一下,就像这样:您将如何向新团队成员解释该方法?当然,它具有某种结构,您可以按照“嗯,它先是先做A,然后是B,然后是C,等等”的方式进行总结。使用简短的“概述”方法调用其他方法可以使此结构显而易见。很难找到350行没有用的代码。人脑并不意味着要处理100多个项目,我们将它们分组。

可重用性:长方法往往具有较低的内聚性-它们经常做的不只一件事。凝聚力低是重用的敌人;如果将多个任务组合为一种方法,那么最终它会在比原本应该少的地方重复使用。

可测试性和内聚性:我在上面的评论中提到了圈复杂度 -这是一种很好的方法来衡量您的方法有多复杂。它代表代码中唯一路径数量的下限,具体取决于输入(编辑:根据MichaelT的注释更正)。这也意味着要正确测试您的方法,您至少需要有与循环复杂度数一样多的测试用例。不幸的是,当您将不真正依赖于彼此的代码放在一起时,就无法确定是否缺少依赖关系,并且复杂性往往会加在一起。您可以认为此度量表示您要尝试做的不同事情的数量。如果太高,是时候分而治之了。

重构和结构:长方法通常表明代码中缺少某些结构。通常,开发人员无法弄清楚该方法不同部分之间的共通之处,以及它们之间的界限。意识到长方法是一个问题,并试图将其拆分为较小的方法,这是在较长的路上真正为整个事物确定更好的结构的第一步。也许您需要创建一两个类。最终不一定会变得更复杂!

我还认为,在这种情况下,使用长方法的借口是“ ...在主循环之外声明的某些变量然后在循环期间被更改”。我不是Python方面的专家,但是我可以肯定,可以通过某种形式的引用传递来解决此问题。


13
+1表示不赞成问题的始终,并着眼于肉:长方法是否不好。我认为OP正在寻求证明,就好像他的情况是一个极端情况,尽管通常当我听到人们解释不常见情况所必需的不良做法时,这仅仅是因为他们并没有非常努力地尝试使用良好做法。罕见的情况确实非常罕见,可悲的是长方法却很普遍。
吉米·霍法

2
好了,看一下上面的列表:从过去的经验来看,我会说可读性的提高是因为该方法更长,包含很多注释并且格式正确,而不必在方法之间跳转代码,尽管这可能是非常主观的。我不希望代码的某些部分被重用。目前,大多数代码重用都是通过继承实现的。
wobbily_col 2012年

1
@wobbily_col顺便说一句,如果您正在寻找一个写得很好的文本,它具有使用较短方法的基本原理,请阅读Clean Code的前几章,这对解释非常有用。
丹尼尔·B

5
@wobbily_col您说必须跳太多以了解许多方法的代码令人困惑,我认为这里的缺失点在于命名的重要性。如果一个方法的名字很好,则无需查看它的作用,调用方法应该是完全可理解的,而无需任何基础知识即可知道调用的方法在做什么,例如您曾经使用过someVar.toString()并感觉到过您需要查看toString的代码才能知道它在做什么?由于良好的方法命名,您刚刚读完它就可以了。
吉米·霍法

4
附带说明一下,拥有需要n个参数的方法也是一种代码味道,它表明该方法可能做的不只一件事情。对于难以命名的方法也是如此。如果确实需要所有这些参数,它们通常是一个更大概念的一部分,可以并且应该将其包含在其自己的类中。当然,我们可以举个例子,那就是最好不要使用此规则-我的观点是,如果您看到这样一种方法,请对其进行彻底研究,那么它可能在某种程度上是不好的。
KL 2012年

28

长方法总是不好的,但有时比其他方法更好。


5
如果没有任何解释,那么如果有人发表相反的意见,此答案可能会毫无用处。例如,如果有人发表这样的声明:“长方法永远不会坏,但有时会比其他方法差。” ,这个答案将如何帮助读者挑出两个相反的观点?考虑将其编辑为更好的形状

9

长方法是代码气味。它们通常表明出了点问题,但这不是一成不变的规则。通常,合理的情况涉及很多状态和相当复杂的业务规则(如您所发现的)。

至于您的另一个问题,将逻辑块隔离到单独的方法中通常是有帮助的,即使它们仅被调用一次。这样可以更轻松地查看高级逻辑,并使异常处理更加简洁。只要您不必传入二十个参数即可表示处理状态!


7

长方法并不总是坏的。它们通常表明可能存在问题。

在我正在使用的系统上,我们有六种左右的方法,它们的长度超过10,000行。其中一条目前为54,830行。没关系

这些荒谬的长函数非常简单,并且会自动生成。那个大的54,830行长的怪物包含从1962年1月1日到2012年1月10日(我们的最新版本)的每日极运动数据。我们还发布了一个过程,用户可以通过该过程来更新该自动生成的文件。该源文件包含来自http://data.iers.org/products/214/14443/orig/eopc04_08_IAU2000.62-now的极地运动数据,该数据已自动翻译为C ++。

在安全安装中,无法即时读取该网站。与外界没有任何联系。也不能选择将网站下载为本地副本并使用C ++进行解析。解析很,而且必须快。下载,自动转换为C ++并进行编译:现在您有了快速的东西。(只是不要对它进行优化编译。令人惊讶的是,优化编译器要花多长时间才能编译50,000行极其简单的直线代码。在我的计算机上,要花半个多小时来编译经过优化的一个文件。而优化完全没有完成任何事情。没有什么可优化的。它是简单的直线代码,一个接一个的声明语句。)


6
“一个接一个的赋值语句”……我称其为“数据文件”。为什么要编码?
约阿希姆·绍尔

3
@JoachimSauer-因为在运行时使用Monte Carlo设置解析大型数据文件是一个坏主意。一个非常非常糟糕的主意。
大卫·哈门

4
@DavidHammen:然后在初始化时执行此操作,就像您要强制链接器/加载器执行该操作一样。或将数据文件作为C结构而不是C代码写入头文件中。至少加载器随后将作为数据块加载。
哈维尔2012年

3
@Javier:即使在初始化时,这也是一个非常糟糕的主意,至少在蒙特卡洛设置中。初始化需要几分钟的时间,而运行仅需几秒钟的模拟与在一夜之间获得成千上万次运行的事实背道而驰。将关键的初始化时间任务更改为编译时间任务可以解决该问题。我们尝试了多种技术,包括编译数据结构方法。在某些情况下(例如,巨大的重力模型),它只是行不通或很难做。直线代码方法易于自动生成,易于验证。这只是丑陋的代码。
大卫·哈门

7
+1有趣的故事。生成的代码当然不是真正的源代码,因此有人可能会争辩说这不会“违反”规则。有人认为代码生成器本身有不错的简短方法
jk。

7

我们只是说有好方法和坏方法来破坏长方法。必须“将最外层的方法保留在脑海中”,这表明您没有以最佳方式分解它,或者您的子方法命名不正确。从理论上讲,在某些情况下长方法会更好。实际上,这种情况非常罕见。如果您不知道如何使一个较短的方法可读,请让某人查看您的代码,并专门询问他们有关缩短方法的想法。

至于导致预期性能下降的多个循环,如果不进行测量就无法知道。如果多个较小的循环意味着所需的所有内容都可以保留在缓存中,则可以大大提高速度。即使性能受到影响,在可读性方面通常也可以忽略不计。

我要说的是,尽管长的方法更难,但它们通常更容易编写。这就是为什么即使没有人喜欢它们的情况下它们也会激增的原因。从一开始就计划到重构,在签入之前没有任何问题。


1
“从一开始就计划到重构,在签入之前没有任何问题。” +1。如今,大多数IDE都具有重构工具,这些工具也使此操作非常容易。但是有一种反向方法,您可以将事物委派给不存在的函数,然后再去填写这些方法,但是我从来没有像我尝试过的那样那样编写过代码。
Tjaart

+1表示“必须“ [保持]您脑海中最外层的方法”,这表明您没有以最佳方式分解它”
迈克尔·肖

4

长方法可以提高计算效率和空间效率,可以更轻松地了解逻辑并轻松调试它们。但是,只有当一个程序员接触该代码时,这些规则才真正适用。如果代码不是原子的,那么将很难扩展,基本上,下一个人将不得不从头开始,然后调试和测试此代码将永远花费,因为它没有使用任何已知的好的代码。


34
总有至少两个程序员参与其中:“您”和“您,从现在起三周”。
约阿希姆·绍尔

3

我们称之为“ 功能分解”,意为在可能的情况下将较长的方法分解为较小的方法。正如您提到的那样,您的方法涉及排序/过滤,因此最好为这些任务使用单独的方法或函数。

准确地说,您的方法应仅专注于perforimng 1任务。

而且,如果由于某种原因需要调用另一种方法,则可以这样做,否则继续使用已经编写的方法。同样出于可读性考虑,您可以添加注释。按照惯例,程序员使用多行注释(在C,C ++,C#和Java中为/ ** /)进行方法描述,并使用单行注释(在C,C ++,C#和Java中为//)。也有很好的文档工具可用于提高代码可读性(例如JavaDoc)。如果您是.Net开发人员,也可以研究基于XML的注释

循环确实会影响程序性能,如果使用不当,可能会导致应用程序开销。想法是设计算法时应尽可能少地使用嵌套循环。


也称为“函数应做的一件事情”。
lorddev

3

编写冗长的函数完全没问题。但这是否取决于实际情况而定。例如,一些最好的算法表现得最好。另一方面,面向对象程序中的很大一部分例程将是访问器例程,这将非常短。如果可以通过表驱动的方法来优化条件,则某些冗长的处理例程中有较长的切换情况。

在Code Complete 2中,有一段关于程序长度的简短讨论。

理论上最好的497最大长度通常描述为节目清单的一页或两页,从66到132行。现代程序倾向于将大量的极短的例程与一些较长的例程混合在一起。

数十个证据表明,这种长度的例程比较短的例程更不容易出错。让诸如嵌套深度,变量数量以及其他与复杂度相关的考虑之类的问题决定例程的长度而不是施加长度(535)

如果要编写的例程长度超过200行,请小心。没有一项研究报告了降低成本,降低错误率或者两者都使用较大例程在大于200行的行之间进行区分的研究,并且在传递200行代码时,您必然会遇到可理解性的上限。本身有536个限制。


2

另一票几乎总是错误的。不过,我发现了两种基本情况,它们是正确的答案:

1)一种方法,该方法基本上只是调用其他方法,并且本身并不做任何实际工作。您有一个过程需要50个步骤才能完成,您将获得一个包含50个调用的方法。试图将其分解通常不会有任何收获。

2)调度员。OOP设计已经摆脱了大多数这样的方法,但是从本质上来说,传入的数据源只是数据,因此不能遵循OOP原理。在处理数据的代码中包含某种调度程序例程并非完全不寻常。

我还要说的是,在处理自动生成的东西时甚至不应该考虑这个问题。没有人试图了解自动生成的代码的作用,对于人类是否易于理解并不重要。


1
50个步骤的过程大概可以归纳为几个阶段。步骤1-9是参数检查,因此请创建一个称为参数检查的新方法。(我肯定有一些不可能做到这一点的例子。我很想看到一个例子)。
sixtyfootersdude

@sixtyfootersdude:当然,您可以将其分解。我并不是说这是不可能的,我说过将其分解没有任何收获。虽然还不到50步,但我却遇到了类似的事情:创建游戏世界。#1创建了一个空的世界,#2创建了一个地形,然后以一种或另一种方式按摩了整个步骤。
罗伦·佩希特尔

&sixtyfootersdude:令人惊讶的是,您如何知道从未见过的代码以及如何对其进行改进。
gnasher729

2

我想解决您提供的示例:

例如,我有一些Django视图,在将对象发送到视图之前会对其进行一些处理,一个长方法是350行代码。我编写了代码,以便处理参数-对查询集进行排序/过滤,然后对查询返回的对象进行一点点处理。

在我公司,我们最大的项目是建立在Django之上的,我们也具有长视图功能(许多功能超过350行)。我认为我们的时间不必那么长,它们正在伤害我们。

这些视图函数正在执行许多松散相关的工作,应将其提取到模型,帮助程序类或帮助程序函数中。同样,我们最终会重用视图来做不同的事情,而应该将其划分为更具凝聚力的视图。

我怀疑您的观点具有相似的特征。就我而言,我知道这会导致问题,并且我正在努力进行更改。如果您不同意它引起的问题,则无需修复它。


2

我不知道是否有人已经提到了这一点,但是长方法不好的原因之一是因为它们通常涉及多个不同级别的抽象。您有循环变量,各种事情都在发生。考虑虚拟功能:

function nextSlide() {
  var content = getNextSlideContent();
  hideCurrentSlide();
  var newSlide = createSlide(content);
  setNextSlide(newSlide);
  showNextSlide();
}

如果您在执行该功能中的所有动画,计算,数据访问等操作,那将是一团糟。nextSlide()函数保持一致的抽象层(幻灯片状态系统),并忽略其他图层。这使代码可读。

如果您必须不断地采用较小的方法来查看它们的作用,那么划分函数的工作就失败了。仅仅因为您正在阅读的代码没有在子方法中做明显的事情,并不意味着子方法不是一个好主意,只是它做错了。

当我创建方法时,我通常最终将它们分成较小的方法,作为一种分而治之的策略。像

   if (hasMoreRecords()) { ... }

当然比

if (file.isOpen() && i < recordLimit && currentRecord != null) { ... } 

对?

我同意绝对语句是不好的,也同意通常长方法是不好的。


1

真实的故事。我曾经遇到一种超过两千行的方法。该方法的区域描述了在这些区域中正在执行的操作。读完一个区域后,我决定执行一种自动提取方法,并根据区域名称对其进行命名。到我完成时,该方法不过是40个方法调用,每个方法调用约五十行,并且全部工作相同。

太大是主观的。有时,一种方法无法比目前更深入地细分。就像写书一样。大多数人都同意,通常应将较长的段落分开。但是有时候,只有一个主意,将其拆分会比该段的长度引起更多的混乱。


0

方法的重点是帮助减少代码返工。方法应具有其负责的特定功能。如果最终在许多地方重新哈希代码,则如果更改了旨在解决该软件的规范,则冒着产生意外结果的风险。

对于具有350行的方法,建议将其执行的许多任务复制到其他位置,因为通常需要大量代码来执行专门的任务是不寻常的。


帮助减少代码是什么
Avner Shahar-Kashtan

@ AvnerShahar -卡斯坦,他大概的意思是“复制” :-)
彼得Török

0

并不是很长的方法是不好的做法,更多的是让它们那样做是不好的。

我的意思是重构样本的实际行为来自:

varaible_1 = 0
variable_2 = 0
for object in queryset :
     if object.condition_condition_a and variable_2 > 0 :
     variable 1+= 1
     .....
     ...    
     . 
      more conditions to alter the variables

return queryset, and context 

Status status = new Status();
status.variable1 = 0;
status.variable2 = 0;
for object in queryset :
     if object.condition_condition_a and status.variable2 > 0 :
     status.variable1 += 1
     .....
     ...    
     . 
      more conditions to alter the variables (status)

return queryset, and context 

然后到

class Status {
    variable1 = 0;
    variable2 = 0;

    void update(object) {
        if object.condition_condition_a and variable2 > 0 {
            variable1 += 1
        }
    }
};

Status status = new Status();
for object in queryset :
     status.update(object);
     .....
     ...    
     . 
      more conditions to alter the variables (status)

return queryset, and context 

您现在正在朝着不仅是一种更短的方法,而且是一种更加有用和易于理解的方法迈进。


0

我认为该方法很长的事实需要检查,但绝对不是即时的反模式。在大型方法中要寻找的最大问题是大量的嵌套。如果你有

foreach(var x in y)
{
    //do ALL the things
    //....
}

并且循环的主体不是非常本地化的(即,您可以向其发送少于4个参数),那么将其转换为:

foreach(var x in y)
{
    DoAllTheThings(x);
}
...
void DoAllTheThings(object x)
{
    //do ALL the things
    //....
}

反过来,这可以大大减少功能的长度。另外,请确保在函数中查找重复的代码并将其移至单独的函数中

最后,有些方法冗长而复杂,您无能为力。有些问题需要不易于编码的解决方案。例如,对一个非常复杂的语法进行解析会产生很长的方法,而如果不加倍加重,您将无法做很多事情。


0

事实是,这取决于。如前所述,如果代码没有将关注点分开并且试图用一种方法来完成所有事情,那么这就是一个问题。将代码分为多个模块可以使代码的读取和写入(由多个程序员)更加容易。首先,每个源文件都遵循一个模块(类)是一个好主意。

其次,关于功能/程序:

void setDataValueAndCheckForRange(Data *data) {/*code*/} 

如果它仅检查“数据”的范围,则是一种好方法。当相同范围适用于多种功能时(错误代码示例),这是一种BAD方法:

void setDataValueAndCheckForRange(Data *data){ /*code */}
void addDataValuesAndCheckForRange(Data *result, Data *d1, Data *d2){ /*code*/}
void subDataValuesAndCheckForRange(Data *result, Data *d1, Data *d2){ /*code*/}
void mulDataValuesAndCheckForRange(Data *result, Data *d1, Data *d2){ /*code*/}

这必须重构为:

bool isWithinRange(Data *d){ /*code*/ }
void setDataValue(Data *d) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/} 
void addDataValue(Data *d, Data *d1, Data *d2) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/} 
void subDataValue(Data *d, Data *d1, Data *d2) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/} 
void mulDataValue(Data *d, Data *d1, Data *d2) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/} 

尽可能重用代码。当程序的每个功能都足够简单(不一定很简单)时,这是可能的。

语录:该山由细小的地球颗粒组成。海洋是由微小的水滴组成的。..-(Sivananda)


0

较长的方法倾向于命令性语言中的“坏”,它们倾向于语句,副作用和可变性,这恰恰是因为这些功能增加了复杂性并因此增加了错误。

在支持表达式,纯净和不变性的函数式编程语言中,很少有理由担心。

在函数式和命令式语言中,总是最好将可重用的代码块分解为常见的顶层例程,但是在支持带有嵌套函数等词法作用域的函数式语言中,实际上最好封装以将子例程隐藏在顶部级功能(方法),而不是将它们分解为其他顶级功能。


但是,在函数式语言中,长方法并不常见。
itsbruce 2015年
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.