为什么在函数名称中对参数名称进行编码不是更常见?[关闭]


47

Clean Code中,作者举了一个例子

assertExpectedEqualsActual(expected, actual)

assertEquals(expected, actual)

前者声称更为清晰,因为它消除了记住论据去向以及由此引起的潜在滥用的必要性。但是,我从未在任何代码中看到前者命名方案的示例,并且一直都在看到后者。正如作者所断言的,为什么编码人员不采用前者,如果后者比后者更清晰?


9
我认为这是一个很好的讨论问题。但是,不能用客观答案来回答。因此,这个问题可能会基于意见而关闭。
欣快的

54
许多人会反对第一个命名方案,因为它过于冗长,远远超出了有助于清晰的范围。特别是对于assertEquals(),该方法在代码库中使用了数百次,因此可以期望读者一次熟悉一下该约定。不同的框架具有不同的约定(例如(actual, expected) or an agnostic (左,右)),但是以我的经验,这最多只是一个很小的混乱源。
阿蒙(Amon)

5
与收益相比,收益是如此之小,以至于任何理智的人都可能走开。如果您想使用一种更流畅的方法,则应该尝试assert(a).toEqual(b)(即使IMO仍然是不必要的冗长),在这里您可以链接一些相关的断言。
Adriano Repetti

18
我们怎么知道实际和期望值?当然应该assertExpectedValueEqualsActualValue吗?但是,等等,我们如何记住它是使用==or .equals还是or Object.equals?应该是assertExpectedValueEqualsMethodReturnsTrueWithActualValueParameter吗?
user253751 '18

6
鉴于此,对于此特定方法,两个参数的顺序无关紧要,选择支持这种命名方案的好处似乎是一个糟糕的例子。
史蒂文·兰德斯'18

Answers:


66

因为它的打字更多,阅读更多

最简单的原因是人们喜欢打字少,而编码信息意味着打字更多。阅读时,即使我熟悉参数的顺序,每次都必须阅读整个内容。即使不熟悉参数的顺序...

许多开发人员使用IDE

IDE通常提供一种机制,可以通过悬停或通过键盘快捷键查看给定方法的文档。因此,参数名称始终在手边。

编码参数会引入重复和耦合

参数名称应该已经记录了它们的含义。通过在方法名称中写出名称,我们也在方法签名中复制了该信息。我们还在方法名称和参数之间创建耦合。说expectedactual并让我们的用户感到困惑。从assertEquals(expected, actual)转到assertEquals(planned, real)不需要使用函数更改客户端代码。从assertExpectedEqualsActual(expected, actual)assertPlannedEqualsReal(planned, real)意味着对API的重大更改。或者我们不更改方法名称,这很快就会造成混乱。

使用类型而不是模棱两可的参数

真正的问题是我们有模棱两可的参数,因为它们是同一类型,所以很容易切换。我们可以改用类型系统和编译器来强制执行正确的顺序:

class Expected<T> {
    private T value;
    Expected(T value) { this.value = value; }
    static Expected<T> is(T value) { return new Expected<T>(value); }
}

class Actual<T> {
    private T value;
    Actual(T value) { this.value = value; }
    static Actual<T> is(T value) { return new Actual<T>(value); }
}

static assertEquals(Expected<T> expected, Actual<T> actual) { /* ... */ }

// How it is used
assertEquals(Expected.is(10), Actual.is(x));

然后可以在编译器级别强制执行此操作,并确保您无法将它们向后退。从不同的角度出发,这基本上就是Hamcrest库用于测试的功能。


5
好吧,如果您使用的是IDE,则会在气球帮助中找到参数名称;如果您不使用它,记住函数名就等于记住参数,因此无论哪种方式都不会获得任何收益。
彼得-恢复莫妮卡

29
如果您反对assertExpectedEqualsActual“因为打字更多,阅读更多”,那么您如何倡导assertEquals(Expected.is(10), Actual.is(x))呢?
ruakh '18

9
@ruakh不可比。assertExpectedEqualsActual仍然需要程序员注意以正确的顺序指定参数。的assertEquals(Expected<T> expected, Actual<T> actual)签名使用编译器执行的正确用法,这是一个完全不同的方法。您可以为简洁起见优化此方法,例如expect(10).equalsActual(x),但这不是问题……
Holger

6
同样,在这种特定情况下(==),参数的顺序实际上与最终值无关。该顺序仅对副作用有影响(报告故障)。订购时,可能(稍微)更有意义。例如strcpy(dest,src)。
克里斯蒂安H

1
不能完全同意,特别是关于复制和耦合的部分。如果每次函数参数更改其名称,函数名称也将不得不更改,那么您就必须跟踪该函数的所有用法,并且还要更改它们...这对于我,我的团队以及所有其他使用我们的代码作为依赖项的其他人来说都是
一大堆

20

您询问有关编程的长期争论。多少详细程度是好?作为一个普遍的答案,开发人员发现命名参数的额外冗长是不值得的。

详尽并不总是意味着更加清晰。考虑

copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)

copy(output, source)

两者都包含相同的错误,但是实际上我们是否更容易找到该错误?一般而言,最简单的调试方法是,当所有事物都非常简洁时,除了少数具有bug的事物之外,这些事物的详细程度足以告诉您出了什么问题。

添加冗长的词已有很长的历史。例如,有一种普遍不受欢迎的“ 匈牙利表示法 ”,为我们提供了像这样的奇妙名称lpszName。在一般的程序员人群中,这通常被抛在一边。但是,在成员变量名称(如mNamem_Namename_)中添加字符在某些圈子中仍然很受欢迎。其他人则完全放弃了。我碰巧在一个物理模拟代码库上工作,该代码库的编码样式文档要求任何返回向量的函数都必须在函数调用(getPositionECEF)中指定向量的框架。

您可能对Apple流行的某些语言感兴趣。Objective-C包含参数名称作为函数签名的一部分(函数[atm withdrawFundsFrom: account usingPin: userProvidedPin]在文档中以表示withdrawFundsFrom:usingPin:。即函数的名称)。Swift做出了一系列类似的决定,要求您将参数名称放入函数调用(greet(person: "Bob", day: "Tuesday"))中。


13
除了所有其他要点,如果写的话 ,那将更容易阅读看看那有多容易?这是因为很容易错过humungousunbrokenwordsalad中途的小改动,而且花更长的时间才能弄清单词边界在哪里。粉碎混乱。copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)copy_from_source_stream_to_destination_stream_without_blocking(file_stream_from_choose_preferred_output_dialog, heuristically_decided_source_file_handle)
tchrist

1
obj-C语法withdrawFundsFrom: account usingPin: userProvidedPin实际上是从SmallTalk借用的。
joH1 '18年

14
@tchrist一定要确保自己在涉及圣战的话题上是正确的。另一面并非总是错误的。
Cort Ammon '18

3
@tchrist Addingunderscoresnakesthingseasiertoreadnotharderasyousee正在操纵该论点。答案是使用大写,而您忽略了大写。 AddingCapitalizationMakesThingsEasyEnoughToReadAsYouCanSeeHere。其次,十分之九,一个名称永远都不能超过[verb][adjective][noun](每个块都是可选的),这种格式使用大写字母就易于阅读:ReadSimpleName
较平缓

5
@tchrist-您的研究科学(免费全文链接)仅表明,受过训练使用下划线样式的程序员在阅读下划线样式方面比骆驼式案例更快。数据还显示,对于经验丰富的科目,差异较小(大多数科目都是学生,即使是那些并非特别有经验的科目也是如此)。这并不意味着那些花费更多时间使用驼峰式案例的程序员也将获得相同的结果。
Jules

8

“清洁代码”的作者指出了一个合理的问题,但是他建议的解决方案相当微不足道。通常,有更好的方法来改进不清楚的方法名称。

他是正确的assertEquals(从xUnit样式的单元测试库中得出)并不清楚哪个参数是期望的,哪个参数是实际的。这也咬了我!许多单元测试库已经注意到了这个问题,并引入了其他语法,例如:

actual.Should().Be(expected);

或类似。当然比清楚得多,assertEquals但也比更好assertExpectedEqualsActual。而且它也更具可组合性。


1
我是肛门的,并且我确实遵循建议的顺序,但是在我看来,如果我期望的结果fun(x)是5,那么颠倒顺序可能会出错assert(fun(x), 5)吗?它是怎么咬你的?
emory

3
@emory我知道jUnit(至少)会根据expected和的值生成一个错误消息 actual,因此反转它们可能会导致消息不准确。但我同意,虽然听起来更自然:)
joH1

@ joH1对我来说似乎很虚弱。失败的代码将失败,并且无论您执行assert(expected, observed)还是,传递代码都将通过assert(observed, expected)。更好的例子是locateLatitudeLongitude-如果您反转坐标,它将严重混乱。
emory

1
@emory人们不在乎单元测试中的明智错误消息,这就是为什么我必须在某些旧代码库中处理“ Assert.IsTrue失败”的原因。调试非常有趣。但是是的,在这种情况下,问题可能不是那么重要(除非在参数顺序通常很重要的情况下进行模糊比较)。流利的断言确实是避免此问题的一种好方法,也可以使代码更富有表现力(并提供更好的启动错误消息)。
Voo

@emory:反转参数将使错误消息产生误导,并在调试时将您发送到错误的路径。
JacquesB

5

您正在尝试使Scylla和Charybdis之间的道路更加清晰,避免不必要的冗长(也称为无目的漫游)以及过分简短(也称为神秘简洁)。

因此,我们必须查看要评估的接口,这是一种使两个对象相等的调试断言的方法。

  1. 它可能还考虑其他功能和名称吗?
    不,所以名称本身很清楚。
  2. 类型有意义吗?
    不,所以让我们忽略它们。你已经做到了吗?好。
  3. 它的论证对称吗?
    几乎在出错时,该消息会将每个参数表示形式放到自己的位置。

因此,让我们看看这种微小的差异是否有意义,而现有的强规范并没有涵盖这些差异。

如果无意间交换了论点,目标听众是否会感到不便?
不,开发人员还会获得堆栈跟踪,并且无论如何都必须仔细检查源代码才能修复该错误。
即使没有完整的堆栈跟踪,断言位置也可以解决该问题。而且即使丢失了该消息,并且从哪条消息中看不出来,它最多也会使可能性加倍。

参数顺序是否遵循约定?
似乎是这样。尽管这似乎充其量是一个薄弱的约定。

因此,这种差异看起来微不足道,并且参数顺序已被足够强的约定所覆盖,即将其编码为函数名的任何努力都具有负效用。


好的顺序可能与jUnit有关,后者会根据expectedand 的值actual(至少使用字符串)来构建特定的错误消息
joH1

我想我涵盖了那部分...
Deduplicator

你提到它,但考虑:assertEquals("foo", "doo")提供错误消息是ComparisonFailure: expected:<[f]oo> but was:<[d]oo>...交换的值将反转消息,听起来更加的意义对称给我。无论如何,就像您说的那样,开发人员还有其他指示器可以解决该错误,但是这可能会误导恕我直言,并且会花费更多的调试时间。
joH1

考虑到两个阵营(dest,src与src,dest)都一直在争论这一问题,至少在AT&T与Intel语法存在的时间里就存在了,因此对于参数顺序存在“约定”的想法很有趣。单元测试中无益的错误消息是一个灾难,应根除而不执行。这几乎和“ Assert.IsTrue失败”一样糟糕(“嘿,您必须执行单元测试以对其进行调试,因此只需再次运行它并在其中放置一个断点”,“嘿,您必须以任何方式查看代码,因此只需检查订单是否正确“)。
Voo

@Voo:关键是要弄错它的“损害”是很小的(逻辑不依赖于此,并且消息实用程序没有受到很大程度的损害),并且在编写IDE时会向您显示参数名称并继续输入。
Deduplicator

3

通常,它不会增加任何逻辑上的清晰度。

比较“添加”到“ AddFirstArgumentToSecondArgument”。

例如,如果需要重载,则添加三个值。什么更有意义?

另一个带有三个参数的“添加”?

要么

“ AddFirstAndSecondAndThirdArgument”?

方法的名称应传达其逻辑含义。它应该告诉它做什么。从微观层面上讲,它采取了什么步骤并没有使读者更容易。如果需要,参数名称将提供其他详细信息。如果您仍然需要更多详细信息,那么代码将在那里。


4
Add建议换向运算。OP关注的是订单重要的情况。
Rosie F

在Swift中,例如,如果定义了add()函数,则可以调用add(5,to:x)或add(5,plus:7,to:x)或add(5,plus:7,给出:x)相应地。
gnasher729

第三次重载应命名为“ Sum”
StingyJack

@StringyJack嗯。。Sum不是指令,它是一个名词,使它不太适合方法名称。但是,如果您有这种感觉,并且想成为一名纯粹主义者,则两个参数的版本也应命名为Sum。如果要使用Add方法,则它应该具有一个添加到对象实例本身的参数(必须是数字或矢量类型)。2个或更多参数变体(无论您如何命名)都是静态的。那么3个或更多的参数版本将是多余的,我们将实现加号运算符:-|
马丁·马特

1
@Martin等待什么?sum是一个非常完整的动词。在“总结”一词中特别常见。
Voo

2

我想添加其他答案提示的其他内容,但我认为并未明确提及:

@puck说:“仍然不能保证函数名称中提到的第一个参数确实是第一个参数。”

@cbojar说“使用类型而不是模棱两可的参数”

问题在于编程语言无法理解名称:它们只是被视为不透明的原子符号。因此,就像代码注释一样,函数的名称与其实际操作方式之间不一定存在任何关联。

比较assertExpectedEqualsActual(foo, bar)一些替代品(从本页面和其他地方),如:

# Putting the arguments in a labelled structure
assertEquals({expected: foo, actual: bar})

# Using a keyword arguments language feature
assertEquals(expected=foo, actual=bar)

# Giving the arguments different types, forcing us to wrap them
assertEquals(Expected(foo), Actual(bar))

# Breaking the symmetry and attaching the code to one of the arguments
bar.Should().Be(foo)

这些都比冗长的名称具有更多的结构,这使该语言看起来不透明。函数的定义和用法也取决于此结构,因此它不会与实现的工作不同步(例如名称或注释可以)。

当我遇到或预见到这样的问题时,在沮丧地大喊大叫计算机之前,我先花一点时间问一下,责怪这台机器是否“公平”。换句话说,机器是否获得了足够的信息以区分我想要的东西和我想要的东西?

像这样的呼叫与assertEqual(expected, actual)一样有意义assertEqual(actual, expected),因此我们很容易将它们混在一起,并且机器可以向前犁并做错事。如果我们assertExpectedEqualsActual改用它,则可能使我们减少犯错的可能性,但不会为机器提供更多信息(它无法理解英语,并且名称的选择不应影响语义)。

使“结构化”方法更可取的原因,例如关键字参数,带标签的字段,不同的类型等,是额外的信息也是机器可读的,因此我们可以让机器发现不正确的用法并帮助我们正确地做事。的assertEqual情况下,是不是太糟糕,因为唯一的问题是不准确的消息。一个更险恶的例子可能是String replace(String old, String new, String content),它很容易混淆,String replace(String content, String old, String new)其含义却大不相同。一个简单的补救方法是采用一对[old, new],这会使错误立即触发错误(即使没有类型)。

请注意,即使使用类型,我们也可能发现自己没有“告诉机器我们想要的东西”。例如,称为“字符串型编程”的反模式将所有数据视为字符串,这使得很容易混淆参数(像这种情况),忘记执行某些步骤(例如转义),意外破坏不变式(例如制作无法解析的JSON),等等。

这也与“布尔盲”有关,在布尔盲中,我们在代码的一部分中计算一堆布尔(或数字等),但是当尝试在另一部分中使用它们时,尚不清楚它们实际代表什么。我们将它们混合在一起,以此类推。将其与例如具有描述性名称(例如LOGGING_DISABLED而不是false)的不同枚举进行比较,如果将它们混合在一起会导致错误消息。


1

因为它无需记住参数的去向

真的吗 仍然不能保证函数名称中第一个提到的参数确实是第一个参数。因此最好查找它(或让您的IDE这样做)并使用合理的名称,而不是盲目地依赖一个相当愚蠢的名称。

如果您阅读了代码,则应该很容易地看到按原样命名参数时会发生什么。copy(source, destination)比诸如此类的东西更容易理解copyFromTheFirstLocationToTheSecondLocation(placeA, placeB)

正如作者所断言的,为什么编码人员不采用前者,如果后者比后者更清晰?

因为对不同的样式有不同的观点,所以您可以找到x篇相反的其他文章的作者。您会发疯,试图跟随某人在某处写的所有内容;-)


0

我同意将参数名称编码为函数名称可以使函数的编写和使用更加直观。

copyFromSourceToDestination( // "...ahh yes, the source directory goes first"

忘记函数和Shell命令中参数的顺序很容易,因此许多程序员都依赖IDE功能或函数引用。名称中描述的参数将是这种依赖的有效解决方案。

但是,一旦写入,对参数的描述将对下一个必须读取该语句的程序员来说是多余的,因为在大多数情况下,将使用命名变量。

copy(sourceDir, destinationDir); // "...makes sense"

这种简洁的风格将赢得大多数程序员的青睐,我个人认为它更易于阅读。

编辑:正如@Blrfl所指出的,编码参数毕竟不是那么“直观”,因为您首先需要记住函数的名称。这需要查找函数引用或从IDE获得帮助,而它们可能仍会提供参数排序信息。


9
因此,如果我可以扮演魔鬼的拥护者一分钟:仅当您知道函数的全名时,这才是直观的。如果您知道有复制功能,并且不记得它是copyFromSourceToDestination还是copyToDestinationFromSource,您的选择是通过反复试验找到它,或者阅读参考资料。可以完成部分名称的IDE只是后者的自动版本。
Blrfl

@Blrfl调用它的要点copyFromSourceToDestination是,如果您认为是copyToDestinationFromSource,则编译器会找到您的错误,但如果调用了copy它,则不会。以错误的方式获取复制例程的参数很容易,因为strcpy,strcat等树立了先例。简洁的文字更容易阅读吗?mergeLists(listA,listB,listC)是从listB和listC创建listA,还是读取listA和listB并写入listC?
Rosie F

4
@RosieF如果我不确定参数的含义,我会在编写代码之前先阅读文档。此外,即使使用更详细的函数名,仍然有解释顺序的空间。那些对代码不屑一顾的人将无法直觉您已经建立了约定,即函数名称中的内容反映了参数的顺序。他们仍然必须提前知道它或阅读文档。
Blrfl '18

OTOH,destinationDir.copy(sourceDir); //“ ...更有意义”
Kristian H

1
@KristianH朝哪个方向dir1.copy(dir2)工作?不知道。那dir1.copyTo(dir2)
maaartinus
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.