我注意到我使用的一些函数有6个或更多个参数,而在大多数我使用的库中,很少会发现一个函数需要3个以上的参数。是更改功能行为的二进制选项。我认为其中一些参数设置的函数可能应该重构。有多少个数字的指导原则吗?

评论

@Ominus:这个想法是您想让您的课程集中注意力。重点关注的类通常没有那么多的依赖项/属性,因此构造函数的参数更少。人们此时提出的一些流行语是高凝聚力和单一责任原则。如果您认为没有违反这些规则并且仍然需要大量参数,请考虑使用Builder模式。

绝对不要遵循带有12个参数的MPI_Sendrecv()示例!

我当前正在从事的项目使用某个框架,在该框架中,具有10个以上参数的方法很常见。我在几个地方调用一个带有27个参数的特定方法。每次看到它,我都会在里面死一些。

切勿添加布尔开关来更改功能行为。拆分功能。将常见行为分解为新功能。

@Ominus什么?只有10个参数?没什么,还需要更多。 :D

#1 楼

我从未见过指导原则,但是根据我的经验,一个带有三个或四个以上参数的函数表示两个问题之一:应该将其拆分为几个较小的函数,每个函数都具有较小的参数集。
那里还有另一个对象。您可能需要创建另一个包含这些参数的对象或数据结构。有关更多信息,请参见Parameter Object模式上的本文。将函数拆分为较小的函数是您需要做的重构,这取决于当前传递给函数的那些标志,从父函数调用这些函数。通过执行以下操作:


它使您的代码更易于阅读。我个人发现,阅读由if结构构成的“规则列表”要比用一个方法完成所有操作的结构容易得多。您已将问题分解为几个较小的任务,这些任务分别非常简单。然后,单元测试集合将由一个行为测试套件组成,该套件将检查通过master方法的路径以及每个单独过程的较小测试的集合。

评论


参数抽象已经变成设计模式了吗?如果您有3个参数类,会发生什么。是否还要添加9个方法重载来处理可能的参数组合?这听起来像一个讨厌的O(n ^ 2)参数声明问题。哦,等等,您只能继承Java / C#中的1个类,因此需要更多的biolerplate(也许更多的子类化)才能使其在实践中起作用。抱歉,我不相信。忽略一种语言可能会提供的支持复杂性的更具表现力的方法,这感觉是错误的。

–伊文·普莱斯
2012年4月18日19:10在

除非您使用Pattern Object模式将变量打包在对象实例中并将其作为参数传递。这对于打包有效,但是可能只是为了简化方法定义而创建不相似变量的类。

–伊文·普莱斯
2012年4月18日在19:14

@EvanPlaice我并不是说只要有多个参数就必须使用该模式-绝对正确,它比第一个列表还差。在某些情况下,您确实确实需要大量参数,但是将它们包装在一个对象中根本行不通。我还没有遇到一个企业发展中的案例,但这个案例并没有落入我在回答中提到的两个桶中的一个。这并不是说一个不存在。

– Michael K
2012年4月18日在19:14

@MichaelK如果您从未使用过它们,请尝试使用Google搜索“对象初始值设定项”。这是一种相当新颖的方法,可显着减少清晰度。从理论上讲,您可以一次性消除类的构造函数,参数和重载。不过实际上,最好保留一个通用的构造函数,并在其余的晦涩/利基属性中依靠“对象初始化器”语法。恕我直言,它是您最接近动态类型语言在静态类型语言中的表现力的方法。

–伊文·普莱斯
2012年4月18日在20:20

@Evain Plaice:因为什么时候动态类型的语言可以表达?

– ThomasX
2012年4月25日在8:24

#2 楼

根据“清洁代码:敏捷软件技巧手册”,理想情况是零,一个或两个是可以接受的,在特殊情况下,三个或四个或更多,绝不可以!

作者的话:


函数的理想参数个数为零(尼拉度)。接下来是一个(单声道),紧接着是两个(双声道)。在可能的情况下,应避免使用三个参数(三重性)。超过三个(polyadic)需要非常特殊的理由,因此无论如何都不应该使用。我认为这本书可以作为您需要多少参数的很好的指南。 >
例如,我认为第二个选择更好,因为它更清楚地说明了该方法正在处理的内容:

br />
LangDetector detector = new LangDetector(someText);
//lots of lines
String language = detector.detectLanguage();


关于许多参数,这可能表明某些变量可以分组为单个对象,或者在这种情况下,很多布尔值可以表示函数/方法要做的事情不止一件,在这种情况下,最好在不同的函数中重构这些行为中的每一个。

评论


“在特殊情况下,三个,四个或四个以上,永不!” BS。 Matrix.Create(x1,x2,x3,x4,x5,x6,x7,x8,x9)怎么样? ?

–卢卡斯·麦登(Lukasz Madon)
2012年4月19日在2:26

零是理想的吗?函数如何获取信息?全局/实例/静态/任何变量? UCK

– Peter C
2012年4月19日,下午3:13

那是一个不好的例子。答案显然是:字符串语言= detectLanguage(someText);。在任何一种情况下,您传递的参数数量都完全相同,只是由于语言不佳而将函数执行分为两部分。

– Matthieu M.
2012年4月19日在6:20

@lukas,在支持像数组或(gasp!)列表这样的奇特构造的语言中,Matrix.Create(input)怎么样?输入在哪里,例如.NET IEnumerable ?这样,当您要创建一个包含10个元素而不是9个元素的矩阵时,也不需要单独的重载。

–用户
2012年4月19日在7:49

零参数是“理想”,这是一个麻烦,也是我认为Clean Code被高估的原因之一。

–user949300
17-9-28 17:35

#3 楼

如果应用程序中的域类设计正确,则会自动减少传递给函数的参数数量-因为这些类知道如何执行其工作,并且它们具有足够的数据来执行其工作。

例如,假设您有一个经理班级,要求3年级的班级完成作业。

如果建模正确,请

br />这很简单。

如果没有正确的模型,方法将是这样的

3rdGradeClass.finishHomework(int lessonId) {
    result = students.assignHomework(lessonId, dueDate);
    teacher.verifyHomeWork(result);
}


正确的模型始终会减少方法调用之间的函数参数,因为将正确的函数委派给了自己的类(单一职责),并且它们具有足够的数据来完成其工作。 ,请检查模型以查看是否正确设计了应用程序模型。

但是有一些例外:当我需要创建传输对象或配置对象时,我会我先使用构建器模式来生成小型构建对象,然后再构建大型配置对象。

评论


在该示例中,您仅需要将学生,老师,dueDate作为第一个函数的参数,然后它才能工作3rdGradeClass.finishHomework(int lessonId,students,teacher,dueDate){},只有在正确的示例中,以防传递更多参数

– Shersha Fn
20年7月15日在9:35

#4 楼

其他答案不能解决的一个方面就是性能。某些体系结构,尤其是在多次调用该函数的情况下。

例如,在ARM中进行函数调用时,前四个参数放在寄存器r0r3中,其余参数必须被推入堆栈。对于关键函数,将参数数目保持在五个以下可能会产生很大的不同。

对于经常调用的函数,即使程序必须在每次调用之前设置参数的事实也会影响性能。 (r0r3可能会被调用的函数覆盖,并且必须在下一次调用之前被替换),因此在这方面零参数是最佳的。提出了有趣的内联主题。内联将以某种方式减轻这种情况,因为内联将使编译器执行与纯汇编编写时相同的优化。换句话说,编译器可以看到被调用函数使用了哪些参数和变量,并且可以优化寄存器使用率,从而最大程度地减少了堆栈的读/写。 />

内联会导致编译后的二进制数增加,因为如果从多个位置调用相同的代码,则会以二进制形式复制相同的代码。这对于I-cache的使用是有害的。
编译器通常只允许内联到一定级别(3个IIRC步骤?)。想象一下从内联函数中的内联函数中调用内联函数。如果在所有情况下都将inline视为必需的,二进制增长将会爆炸。
有很多编译器会完全忽略inline或在遇到q4312079q时实际上会给您错误。

评论


从性能的角度来看,传递大量参数是好是坏取决于选择。如果一个方法需要十几条信息,而其中一个将用十一个相同的值调用数百次,那么使用该方法采用数组可能要比使用十二个参数更快。另一方面,如果每个调用都需要唯一的一组十二个值,则为每个调用创建和填充数组可能比直接传递值容易慢。

–超级猫
2014年9月2日在21:44

内联不能解决这个问题吗?

– KjMag
17年3月3日在10:51

@KjMag:是的,在一定程度上。但是有很多陷阱取决于编译器。函数通常只会内联到一定级别(如果您调用的内联函数调用了内联函数,内联函数又调用了内联函数...。)如果函数很大并且在很多地方都被调用,则内联到处都会使二进制文件更大,这可能意味着I高速缓存中的丢失次数会更多。因此内联可以提供帮助,但这不是万灵丹。 (更不用说有很多不支持内联的旧嵌入式编译器。)

–狮子座
17年11月4日,0:50

#5 楼

当参数列表增加到五个以上时,请考虑定义“上下文”结构或对象。

这基本上是一个结构,其中包含所有可选参数以及一些合理的默认设置。

在C程序世界中,简单的结构可以做到。在Java,C ++中,一个简单的对象就足够了。不要弄乱getter或setter方法,因为对象的唯一目的是保存“ public”可设置的值。

评论


我同意,当函数参数结构开始变得相当复杂时,上下文对象可以派上用场。我最近在博客中发表了关于使用类似访问者模式的上下文对象的博客

–卢卡斯·埃德(Lukas Eder)
2012年4月19日在11:56

#6 楼

不,没有标准指南

,但是有一些技术可以使带有很多参数的函数更容易使用。参数(args *)或参数字典字典(kwargs **

例如,在python中:

// Example definition
def example_function(normalParam, args*, kwargs**):
  for i in args:
    print 'args' + i + ': ' + args[i] 
  for key in kwargs:
    print 'keyword: %s: %s' % (key, kwargs[key])
  somevar = kwargs.get('somevar','found')
  missingvar = kwargs.get('somevar','missing')
  print somevar
  print missingvar

// Example usage

    example_function('normal parameter', 'args1', args2, 
                      somevar='value', missingvar='novalue')


输出:

args1
args2
somevar:value
someothervar:novalue
value
missing


或者您可以使用对象文字定义语法

例如,这是一个JavaScript jQuery调用,用于启动AJAX GET请求: />
$.ajax({
  type: 'GET',
  url: 'http://someurl.com/feed',
  data: data,
  success: success(),
  error: error(),
  complete: complete(),
  dataType: 'jsonp'
});


如果您看一下jQuery的ajax类,则可以设置很多(大约30个)更多的属性。主要是因为ajax通信非常复杂。幸运的是,对象文字语法使生活变得轻松。


C#intellisense提供了有效的参数文档,因此看到重载方法的非常复杂的排列并不罕见。动态类型的语言(例如python / javascript)没有这种功能,因此看到关键字参数和对象文字定义更为常见。您可以在实例化对象时显式查看正在设置的属性。您将需要做更多的工作来处理默认参数,但是从长远来看,您的代码将更具可读性。使用对象文字定义,您可以摆脱对文档的依赖,从而乍看之下您的代码在做什么。请记住,正确的只读访问控制应该适用于C#中的对象文字构造函数。它们本质上与在构造函数中设置属性的工作原理相同。 ,我强烈建议您尝试一下。这可能是一个启发性的体验。

首先,打破对功能/方法初始化的所有一切方法的参数的依赖可能会很可怕,但是您将学习到用代码做更多的事情而不必增加不必要的复杂性。 br /> Update:

我可能应该提供示例来演示在静态类型语言中的使用,但是我目前不在静态类型上下文中考虑。基本上,我在动态类型的上下文中做了太多工作,以至于突然转回原处。

我所知道的是对象文字定义语法在静态类型的语言中是完全可能的(至少在C#和Java),因为我以前使用过它们。在静态类型的语言中,它们称为“对象初始化器”。以下是一些链接,显示了它们在Java和C#中的用法。

评论


我不确定我喜欢这种方法,主要是因为您失去了各个参数的自我记录价值。对于类似项目的列表,这是很有意义的(例如,采用字符串列表并将它们连接起来的方法),但是对于任意参数集,这比长方法调用差。

– Michael K
2012年4月18日在18:37

@MichaelK再看看对象初始化器。通过它们,您可以显式定义属性,而不是在传统方法/函数参数中隐式定义属性。请阅读msdn.microsoft.com/en-us/library/bb397680.aspx,以了解我的意思。

–伊文·普莱斯
2012年4月18日在18:55

创建一个仅用于处理参数列表的新类型听起来就像不必要的复杂性的定义一样。当然,动态语言可以避免这种情况,但是您会得到一个goo参数。无论如何,这不能回答所提出的问题。

– Telastyn
2012年4月18日19:11

@Telastyn您在说什么?没有创建新类型,您可以直接使用对象文字语法声明属性。这就像定义一个匿名对象,但该方法将其解释为key = value参数分组。您正在查看的是方法实例化(而不是参数封装对象)。如果您的牛肉带有参数包装,请查看其他问题之一中提到的Parameter Object模式,因为这正是它的含义。

–伊文·普莱斯
2012年4月18日19:18在

@EvanPlaice-除了静态编程语言一般需要一种(通常是新的)声明的类型才能允许使用参数对象模式。

– Telastyn
2012年4月18日19:30在

#7 楼

就个人而言,我的代码气味警报触发的地方是2以上。当您将函数视为操作(即从输入到输出的转换)时,在一个操作中使用两个以上的参数并不常见。程序(这是实现目标的一系列步骤)将需要更多的投入,有时是最好的方法,但如今在大多数语言中,这已不再是常态。而不是规则。由于异常情况或易于使用,我经常使用带有两个以上参数的函数。

#8 楼

就像Evan Plaice所说的那样,我非常喜欢在可能的情况下简单地将关联数组(或您语言的类似数据结构)传递给函数。

因此,而不是(例如)这样做:

<?php

createBlogPost('the title', 'the summary', 'the author', 'the date of publication, 'the keywords', 'the category', 'etc');

?>


求助:

<?php

// create a hash of post data
$post_data = array(
  'title'    => 'the title',
  'summary'  => 'the summary',
  'author'   => 'the author',
  'pubdate'  => 'the publication date',
  'keywords' => 'the keywords',
  'category' => 'the category',
  'etc'      => 'etc',
);

// and pass it to the appropriate function
createBlogPost($post_data);

?>


Wordpress通过这种方式做了很多工作,我认为效果很好。 (尽管上面的示例代码是虚构的,而本身并不是Wordpress的示例。)

该技术使您可以轻松地将大量数据传递到函数中,而无需记住代码中的内容。

当您需要重构时,您还将欣赏这种技术-不必潜在地更改函数参数的顺序(例如,当您意识到需要传递另一个参数),则根本不需要更改函数的参数列表。每次调用函数时更改参数的顺序。那是一个巨大的胜利。

评论


看到此发布会突出显示“哈希传递”方法的另一个好处:请注意,我的第一个代码示例很长,它会生成一个滚动条,而第二个代码示例恰好适合页面。在您的代码编辑器中可能也是如此。

–克里斯·艾伦·莱恩(Chris Allen Lane)
2012年4月24日19:28在

#9 楼

先前的答案提到一位可靠的作者,他说您的函数参数越少,您所做的越好。答案并未说明原因,但书中对此进行了说明,这是两个最令人信服的原因,因为您需要采用这种哲学并且我个人也同意:属于与功能不同的抽象级别。这意味着您的代码的读者将不得不考虑函数参数的性质和目的:这种想法比其相应函数的名称和目的“低级”。
第二个对函数使用尽可能少的参数的原因是测试:例如,如果您的函数具有10个参数,请考虑必须为所有测试用例(例如,单元测试)覆盖多少个参数组合。更少的参数=更少的测试。


#10 楼

为了在Robert Martin的“ Clean Code:A Agile Software Craftsmanship手册”中为理想的函数参数个数为零的建议提供更多的上下文,作者说了以下几点: >
论点很难。他们具有很大的概念力。这就是为什么我
从示例中删除了几乎所有它们的原因。考虑
实例中的StringBuffer。我们本可以将其作为参数传递给它,而不是将其作为实例变量,但是
那么我们的读者每次看到它就不得不对其进行解释。
模块讲的故事,includeSetupPage()
includeSetupPageInto(newPageContent)更容易理解。
参数与函数名称
处于不同的抽象级别,并迫使您知道
在那时并不特别重要的细节(换句话说,StringBuffer)。 br />

对于上面的includeSetupPage()示例,这是本章结尾处重构的“干净代码”的一小段内容:
java prettyprint-override“> // *** NOTE: Commments are mine, not the author's *** // // Java example public class SetupTeardownIncluder { private StringBuffer newPageContent; // [...] (skipped over 4 other instance variables and many very small functions) // this is the zero-argument function in the example, // which calls a method that eventually uses the StringBuffer instance variable private void includeSetupPage() throws Exception { include("SetUp", "-setup"); } private void include(String pageName, String arg) throws Exception { WikiPage inheritedPage = findInheritedPage(pageName); if (inheritedPage != null) { String pagePathName = getPathNameForPage(inheritedPage); buildIncludeDirective(pagePathName, arg); } } private void buildIncludeDirective(String pagePathName, String arg) { newPageContent .append("\n!include ") .append(arg) .append(" .") .append(pagePathName) .append("\n"); } }

作者的“学派”主张使用小的类,少量的函数参数(理想情况下为0)和非常小的函数。尽管我也不完全同意他的观点,但我发现它发人深省,并且我认为零函数参数作为理想的想法值得考虑。另外,请注意,即使上面的小代码段也具有非零的参数函数,所以我认为这取决于上下文。从测试的角度来看,这样做会更困难。但是在这里,我主要要强调上面的示例以及他对于零函数参数的基本原理。)

#11 楼

理想情况下为零。一两个就可以,在某些情况下三个也可以。
四个或更多通常是一个坏习惯。测试和调试的观点。

如果只有一个参数,则知道它的值,对其进行测试并找出错误是“相对容易的,因为只有一个因素。随着因素的增加,总的复杂性会迅速增加。举一个抽象的例子:

考虑一个“在这种天气下要穿什么”程序。您可以想象,根据这一因素,穿什么衣服的结果非常简单。现在,考虑如果程序实际通过了温度,湿度,露点,降水等情况,程序可能/将要/应该做些什么。现在想象一下,如果程序给出了“错误”的答案,调试该程序将多么困难。

评论


如果一个函数的参数为​​零,则它要么返回一个常数(在某些情况下有用,但有限制),要么使用某种隐藏状态,最好将其显式显示。 (在OO方法调用中,上下文对象足够明确,不会引起问题。)

–博士生
2012年4月18日在20:55

-1不引用源

–约书亚德雷克
2012年4月19日在13:31

您是否在认真地说,理想情况下所有函数都不需要参数?还是这个夸张?

– GreenAsJade
2014-09-2 11:35



请参阅Bob叔叔的论点,网址为:notifyit.com/articles/article.aspx?p=1375308,请注意,他在底部说:“函数应包含少量论点。最好没有论点,其次是一,二和三超过三个是非常可疑的,应避免产生偏见。”

–迈克尔·杜兰特(Michael Durrant)
2014年9月2日上午11:58

我已经给出了来源。从那时起没有任何评论。我还尝试回答“指南”部分,因为许多人现在将Bob叔叔和Clean Code视为指南。有趣的是,(目前)巨大支持的最高答案表示没有任何指导方针。鲍勃叔叔无意成为权威,但实际上是这样,这个答案至少试图成为对问题细节的回答。

–迈克尔·杜兰特(Michael Durrant)
18-11-29在16:40