最近我一直在尝试将长方法拆分为几个短方法。
例如:我有一个
process_url()
函数,该函数将URL拆分为组件,然后通过其方法将其分配给某些对象。我没有准备在一个函数中实现所有这些功能,而是只准备了要在process_url()
中分割的URL,然后将其传递给process_components()
函数,然后将其传递给assign_components()
函数。起初,这似乎为了提高可读性,因为我没有使用较大的“上帝”方法和函数,而是使用具有描述性更小的名称的较小方法和函数。
但是,通过浏览以这种方式编写的一些代码,我发现我现在不知道这些较小的函数是否被其他任何函数或方法调用。
继续前面的示例:看到代码的人可能会认为
process_components()
功能被抽象为一个函数,因为当各种方法和函数调用时,实际上,只有process_url()
才调用它。这似乎有些错误。另一种选择是仍然编写长的方法和函数,但用注释指示它们的部分。
我描述的函数拆分技术是否错误?管理大型函数和方法的首选方法是什么?
UPDATE:我主要担心的是,将代码抽象到一个函数中可能意味着它可以被多个其他函数调用。
还请参见:在/ r / programming上对reddit的讨论(提供了不同的观点,而不是此处的大多数答案)和/ r / readcode。
#1 楼
测试执行很多任务的代码很困难。调试执行很多任务的代码很困难。
解决这两个问题的方法是编写不执行任何操作的代码。做很多事情。编写每个函数,使其仅执行一项操作。这使他们易于使用单元测试进行测试(一个人不需要进行十几个单元测试)。
我的一位同事在判断给定方法是否需要时使用了他的短语。分解成较小的部分:
如果在向另一位程序员描述代码的活动时使用“和”一词,则该方法至少需要分成另外一部分。
您写道:
我有一个process_url()函数,该函数将URL拆分为组件,然后通过其方法将其分配给某些对象。
这应该至少是两种方法。可以将它们包装成一种面向公众的方法,但是工作方式应该是两种不同的方法。
评论
@exizt编写一个带有URL并返回其组件的函数有什么问题?或者,将其标记为受保护或私有(或在语言上等效),以便只能从模块内部调用它。
–user40980
13年4月24日在16:45
@exizt,这个含义有什么问题?如果多个其他函数认为提取的函数很有用,则应绝对调用它们。重构和提取方法可促进代码重用,这不是一件坏事。
–安东尼·佩格拉姆
13年4月24日在16:46
如果它以特定于整个功能的方式处理组件,以致几乎无法重用,该怎么办?即使受保护-它是可调用的函数或方法,这是否意味着可以从该模块内部的多个方法中调用它?
–sbichenko
13-4-24在16:46
@exizt,如果这些功能完全耦合到您的工作流程中,那么其他潜在的调用者将不会发现它们有用。问题几乎可以解决。但是,根据您使用的语言,有一些机制可以通过类文件中的修饰符,包或程序集中的修饰符向调用者隐藏该功能,而这些调用者认为该功能不是特别有用。也许您还有更多的重构和组织工作要做。但总的来说,较小的可重用方法是好东西。
–安东尼·佩格拉姆
13-4-24在16:52
@exizt则比将其变成一个巨大的泥球要容易得多。如果更容易测试,更容易修复,更容易弄清楚-那就这样做。稍后,当您返回到代码并需要弄清楚特定部分的工作原理时,您将节省时间(时间就是金钱)。
–user40980
13年4月24日在16:54
#2 楼
是的,拆分长函数是正常的。罗伯特·C·马丁(Robert C. Martin)在他的《清洁代码》一书中鼓励这种做法。特别是,您应该为函数选择非常具有描述性的名称,作为一种自我记录代码的形式。评论
@exizt:是的,确实如此。所以呢?如果正确编写了该函数的代码,则其他人可以在需要它们的情况下调用它。
– John R. Strohm
13年4月24日在16:49
这是否会使那些阅读我的代码的人误以为它实际上是由其他方法调用的,而实际上它只是由一个人调用的?
–sbichenko
13年4月24日在16:52
@exizt不,不会。
–user40980
13年4月24日在16:55
@exizt:我认为您遇到的一个困难是您可能没有故意命名方法。人员/代码应期望一种方法能够遵守其合同/预期目的。如果您的方法做了非常具体的事情,那么调用者的目的将是显而易见的。因此,通过调用它,他们需要合同中预期的功能。
–史蒂文·埃弗斯(Steven Evers)
13-4-24在21:49
@exizt有多少个地方调用您的main()函数?一个函数只使用一次是可以的。您也不知道那是否会成立,也许您需要实现一个非常相似的功能,然后您将不得不再次调用很多这些功能。或从事类似的项目,只需照原样复制整个功能,而不必重写很多东西。
–半
13年4月25日在16:17
#3 楼
正如人们指出的那样,这提高了可读性。仅仅阅读一些方法名称,阅读process_url()
的人可能会更清楚地看到处理URL的一般过程。问题是,其他人可能会认为这些功能已被其他方法使用。代码,如果其中一些需要更改,他们可以选择保留这些功能并定义新功能。这意味着某些代码变得无法访问。
有几种方法可以解决此问题。首先是代码中的文档和注释。第二,提供覆盖率测试的工具。无论如何,在很大程度上取决于编程语言,以下是您可以根据编程语言应用的一些解决方案:
面向对象的语言可以允许定义一些私有方法,以确保不在其他地方使用它们。
其他语言的模块可以指定从外部可见的函数,等等。
像Python这样的高级语言可能不需要定义多个函数,因为它们无论如何都会很简单
其他语言(例如Prolog)可能需要(或强烈建议)为每个条件跳转定义一个新的谓词。
在某些情况下,通常会在使用该函数的函数中定义辅助函数它们(本地函数),有时是匿名函数(代码闭包),这在Javascript回调函数中很常见。
因此,简而言之,就可读性而言,拆分多个函数通常是一个好主意。如果函数很短并且会产生
goto
效果,或者名称不是真正的描述性,那可能不是很好,在这种情况下,读取某些代码将需要在函数之间跳转,这可能很麻烦。关于您对这些功能的范围和用法的关注,有几种处理方式,这些方式通常取决于语言。通常,最好的建议是使用常识。任何严格的规则在某些情况下很可能是错误的,最终取决于人。我认为这是可读的:
process_url = lambda url: dict(re.findall('([^?=&]*)=([^?=&]*)', url))
我个人更喜欢一种衬板,即使它是稍微复杂一点,而不是跳过并搜索多个代码文件,如果我花了三秒钟多的时间才能找到代码的其他部分,我什至不知道我在检查什么。没有遭受多动症困扰的人们可能会喜欢他们会记住的更多解释性名称,但是最后,您总是要做的是平衡代码,行,段落,函数,文件,模块等不同级别上的复杂性。
所以关键字是balance。拥有一千行的函数对于任何阅读它的人来说都是一个地狱,因为它没有封装并且上下文变得太大。将一个函数拆分为一千个函数,每个函数中只有一行的情况可能会更糟:
您有一些名称(可以在行中作为注释提供)
(希望)消除全局变量,而不必担心状态(具有参照透明性)
,但是您迫使读者来回跳动。
所以没有这里有银弹,但经验和平衡。恕我直言,学习如何执行此操作的最佳方法是阅读许多其他人编写的代码,并分析为什么您很难阅读它以及如何使其更具可读性。这将提供宝贵的经验。
评论
就个人而言,即使它有点复杂,我也喜欢它,但我不喜欢。编写可读代码要比快速写代码更为重要。如果您不经常编写此功能,则很可能会犯错误或愚蠢的类型。另一方面,如果您经常使用它,则您应该已经知道该实用程序函数的存储位置,而应该使用它。
–毛里西
2014年5月24日下午16:26
您的评论是关于快速编写代码或经常编写代码的,您没有把握重点。我不是在谈论编写,而是在阅读代码,这全都与可读性有关。详细的程序不可读。一个衬里可以用作“摘要”,因此即使稍微复杂也可以更具可读性。编写任何东西(特别是代码)时,合成很重要。
– Trylks
2014年5月27日在9:08
我考虑逆转首先是代码中的文档和注释。第二,提供覆盖率测试的工具。我可能会首先关注测试,如果可能的话,可以使用它们来找出确切的代码。然后,我添加所有文档和(很少)关于陷阱的评论。
–迈克尔·杜兰特(Michael Durrant)
15年3月11日在10:55
@MichaelDurrant并不是从最好到最坏的顺序,而是从最基本到更复杂的顺序。为了澄清起见:代码应该是自记录的,可以解决一些问题。最严重的问题可能是注释代码不一致。没有针对注释的测试,因此它们很可能包含错误,因此应尽可能避免注释。 (1)如果需要澄清某些代码,则(2)编写一些注释之前,(3)尝试更好地编写代码,(4)如果仍然需要澄清代码,请转到(1)。
– Trylks
2015年3月11日13:29
#4 楼
我会说这要视情况而定。如果只是为了拆分而将其拆分,并称其名称为
process_url_partN
等,则否,请不要。如果您或其他人需要弄清楚正在发生的事情,这只会使以后很难遵循。如果您要推出具有明确目的的方法,这些方法可以自己进行测试并且很有意义他们自己的(即使没有其他人在使用它们)则为是。
出于您的特定目的,看来您有两个目标。
解析一个URL并返回其组件列表。
对那些组件进行一些操作。
我将第一部分单独编写,让它返回一个相当通用的结果,可以轻松地对其进行测试并可能在以后重用。更好的是,我会寻找一个内置函数,该函数已经在您的语言/框架中执行了此操作,并使用了该函数,除非您的解析非常特殊。如果它非常特殊,我仍将其编写为单独的方法,但可能会将其“捆绑”为处理第二个类的类中的私有/受保护的方法(如果您正在编写面向对象的代码)。
第二部分我将作为自己的组件编写,它使用第一部分进行URL解析。
#5 楼
我从未遇到过其他开发人员将较大的方法拆分为较小的方法的问题,因为这是我自己遵循的模式。 “上帝”方法是一个可怕的陷阱,其他经验不足或根本不在乎的人往往会被更多地抓住。话虽这么说...在较小的方法上使用适当的访问标识符非常重要。找到一个乱七八糟的类,这很令人沮丧,因为那样一来,我就完全找不到对整个应用程序使用该方法的位置/方式的信心。
我住在C#-land上,所以我们有
public
,private
, protected
,internal
,看到这些单词使我毫无疑问地看到了方法的范围以及必须在哪里寻找调用的地方。如果它是私有的,那么我知道该方法仅在一个类中使用,并且在重构时我完全有信心。在Visual Studio世界中,拥有多个解决方案(
.sln
)会加剧此反模式,因为IDE /重新共享“查找用法”助手将不会在开放式解决方案之外找到用法。 #6 楼
如果您的编程语言支持它,则可以在process_url()
函数的范围内定义“辅助”函数,以获得单独函数的可读性。例如,function process_url(url) {
function foo(a) {
// ...
}
function bar(a) {
// ...
}
return [ foo(url), bar(url) ];
}
如果您的编程语言不支持此功能,则可以将
foo()
和bar()
移出process_url()
的范围(以便其他功能/方法)-但您认为这是您的“ hack”,因为您的编程语言不支持此功能。是否将功能分解为子功能可能取决于是否除其他考虑因素外,还有有意义的/有用的名称,以及各个功能的大小。
评论
其他说明:尽管PHP支持函数内部的函数定义,但实际上已定义的函数已添加到全局范围中。
–sbichenko
13年4月25日在14:33
我曾经做过很多次,直到后来才意识到它是多么不可读。即,当您尝试查看process_url的功能时,您很容易阅读foo和bar的实现。并且即使您不这样做,每次阅读process_url的实现时,也不得不跳过它们。如果可以的话,将这些内部方法放在下面会更好。然后,文件在底部变得更加具体,在顶部变得更加抽象。
–crizCraig
13年8月10日在22:19
在JavaScript中,每次调用process_url时,都将重新声明和分配foo和bar,如果它们中的任何一个返回依赖于process_url范围的任何内容(例如,一个函数),它们将保留整个范围。如果需要的话,这是非常有用的-如果不这样做,随着时间的流逝会变成内存消耗。
–桑迪·吉福德(Sandy Gifford)
15年12月16日在19:30
@SandyGifford如果现代JS引擎的各种优化实际上影响了性能bahmutov.calepin.co/detecting-function-optimizations-in-v8.html,则它们应避免重新声明和重新分配。 (例如,通过JIT。)关于内存使用情况,我不确定将较大的函数拆分为较小的函数会如何改变这种情况?
–mjs
15年12月19日在17:45
#7 楼
如果有人对这个问题感兴趣,那么这就是约书亚·克列耶夫斯基(Joshua Kerievsky)在他的“模式重构”(Addison-Wesley)中所说的“合成方法”:转换将逻辑分解成少量细节相同的意图揭示步骤。
我相信,根据方法的“详细程度”正确嵌套方法在这里很重要。 />
在发布者的网站上查看摘录:
我们编写的许多代码并非一开始就很简单。为简单起见,我们必须反思一下不简单的地方,然后不断问:“它怎么会更简单?”我们通常可以通过考虑完全不同的解决方案来简化代码。本章中的重构为简化方法,状态转换和树结构提供了不同的解决方案。
编写方法(123)涉及产生能够有效传达其工作方式和工作方式的方法。组合方法[Beck,SBPP]包含对详细名称相同的命名方法的调用。如果要使系统保持简单,请尝试在任何地方应用Compose Method(123)...
附录:Kent Beck(“实现模式” ”)称为“组合方法”。他建议您:
[c]在对其他方法的调用之外合并方法,每个方法大致处于相同的抽象级别。
组成不佳的方法的迹象中有很多是抽象层次的混合。
再次警告不要混合不同的抽象层次(强调我的)。
评论
这看起来与所谓的“提取方法”模式非常相似(如果不同)
– gna
13年5月5日在6:13
将外部if重构为早期退出测试是好的,内部if块应替换为elements =(Object [])Array.Resize(elements,elements.length * 2);之类的东西,但即使没有这样的东西的变化我宁愿处理第一个问题而不是第二个问题。替换elements [size ++] = element;调用addElement(element)不会抽象出elements.length> size;的前提条件。它只是将其隐藏。
–超级猫
2014年9月2日在22:37
#8 楼
一个好的规则是在相似的级别上具有附近的抽象(由塞巴斯蒂安更好地在上面的答案中提出)。如果您有一个处理低级内容的(大型)函数,但也做了一些较高级别的选择,请尝试排除低级内容:
void foo() {
if(x) {
y = doXformanysmallthings();
}
z = doYforthings(y);
if (z != y && isFullmoon()) {
launchSpacerocket()
}
}
将内容移到较小的函数通常比在其中包含一些概念上的“大”步骤的函数中包含许多循环更好。 (除非您可以将其组合成相对较小的LINQ / foreach / lambda表达式...)
#9 楼
如果您可以设计适合这些功能的类,请将它们设为私有。换句话说,使用合适的类定义,您可以只公开需要公开的内容。#10 楼
我敢肯定这不会成为流行观点,但这是完全可以的。引用的局部性可以极大地帮助您确保您和其他人理解该功能(在这种情况下,我指的是代码,而不是专门针对内存)。与所有内容一样,这是一种平衡。您应该更关心那些告诉您“总是”或“从不”的人。
评论
当您说“完全可以”时,您是说将函数拆分为较小的函数是可以的吗?如果是这样,您为什么会认为这不是流行观点,因为您之前至少有8个答案支持该选择?
– Avner Shahar-Kashtan
13年4月26日在9:44
我的意思是可以将其保留为长函数,这是基于代码本身及其作用的判断调用。没有理由不加思索地自动分解它。
–弗雷德
13年4月26日在19:18
#11 楼
考虑这个简单的功能(我使用的是类似Scala的语法,但我希望这个想法在没有任何Scala知识的情况下就能弄清楚):
def myFun ... {
...
if (condition1) {
...
} else {
...
}
if (condition2) {
...
} else {
...
}
if (condition3) {
...
} else {
...
}
// rest
...
}
此函数具有多达8条可能的路径,取决于这些条件的评估方式,这些代码可以如何执行。
这意味着您将需要8个不同的测试才能测试这部分。此外,很可能某些组合是不可能的,然后您必须仔细分析它们是什么(并确保不要错过某些可能的组合)-很多工作。
很难有关代码及其正确性的原因。由于每个
if
块及其条件都可能取决于某些共享的局部变量,为了知道它们之后发生的情况,使用该代码的每个人都必须分析所有这些代码块和8个可能的执行路径。在这里容易犯错误,很可能有人更新代码会丢失某些东西并引入错误。另一方面,如果将计算结构化为
def myFun ... {
...
val result1 = myHelper1(...);
val result2 = myHelper2(...);
val result3 = myHelper3(...);
// rest
...
}
private def myHelper1(/* arguments */): SomeResult = {
if (condition1) {
...
} else {
...
}
}
// Similarly myHelper2 and myHelper3
您可以:
容易地测试每个助手功能,每个助手功能只有两个可能的执行路径。
检查
myFun
时如果result2
依赖于result1
,则立即显而易见(只需检查对myHelper2(...)
的调用是否使用它来计算参数之一。(假设帮助程序不使用某些全局状态。)很明显,它们是如何依赖的,此外,在这三个调用之后,还很清楚计算状态如何-只需在result1
,result2
和result3
中捕获-无需检查是否/是否需要其他局部变量已被修改。#12 楼
方法具有更具体的责任,代码将更易于测试,读取和维护。尽管没有其他人打电话给他们。如果将来需要在其他地方使用此功能,则可以轻松提取这些方法。
#13 楼
像这样命名您的方法是完全可以接受的:MyProcedure()
MyProcedure_part1()
MyProcedure_part2()
end;
我不明白为什么有人会认为这些方法会被其他进程调用,这是完全清楚它们的用途。
评论
这具有反模式的外观(目前我无法找到其名称),其中每个任务仅部分完成任务,但始终需要依次调用这两个任务。每种方法都只能做一件事和一件事,而不是“一件事的第1部分”和“一件事的第2部分”。如果这不是您想要的,您是否可以修改答案以表明这一点?
–user40980
13年4月25日在17:29
@MichaelT-简单地称为顺序耦合,是的,在大多数情况下,它被认为是有害的。
–CurtainDog
13年4月26日在7:32
@CurtainDog调用工厂对象并在该对象上调用方法之后,没有什么害处。必须按特定顺序进行。问题不是事情需要按顺序发生,而是它们何时需要按顺序发生,而且事实并非如此。在创建对象之前,没有人会合理地尝试在对象上调用方法,因此,该对象的顺序性质不会引起问题。
–弗雷德
13年4月26日在19:25
@Fred工厂方法为某人提供了一个可行的对象。如果foo_part1()和foo_part2()本身是不完整的,只是为了分解代码而将代码分解,那么这很不好。 OP尚未显示这是预期的解释。
–user40980
2013年4月26日19:40在
@CurtainDog实际上,我喜欢这种命名约定,并且自己使用它来破坏一些方法。但是,我使用的后缀不是“ _Part1”,而是“ _Part2”后缀,这些后缀描述了辅助方法的用途,例如“ _Asserts”,“ _ Logging”等。在大多数情况下,这不是反模式。 (请参阅stackoverflow.com/questions/2712131/。)有时,您正在实现一个必定长的算法,而对该方法进行分解会使它更具可读性和可测试性。这是一种简单,有效且易于理解的方法。您也可以使用访问修饰符来防止外部呼叫。
–乔丹·里格(Jordan Rieger)
2015年3月23日在23:28
评论
如果您的语言没有区分这些方法的方法,则可以随时创建一个命名约定。另外,许多IDE都有一些工具,可让您查看对方法的引用。
@KilianFoth:这个问题专门针对一个行函数,这个问题并不意味着较小的函数本身并不是很长。
我不知道哪种语法适合只从一点调用的子例程...;)
不增加理解的函数名称:process,call,doIt。对于值名称也是如此:数据,对象,事物,组件,向量,列表等。