使用布尔参数确定行为是否错误?


194

我不时看到一种“感觉”错误的做法,但是我不能很清楚地指出它的错误之处。也许这只是我的偏见。开始:

开发人员定义一个使用布尔值作为其参数之一的方法,然后该方法调用另一个,依此类推,最终使用该布尔值,仅用于确定是否要执行特定操作。例如,仅当用户具有某些权限时,或者如果我们(或不在)测试模式,批处理模式或实时模式下,或者仅当系统处于运行状态时,才可以使用此权限来执行操作一定的状态。

嗯,总有另一种方法可以做到,无论是通过查询何时该采取行动(而不是传递参数),还是通过该方法的多个版本或该类的多个实现,等等。我的问题是与其说如何改善它,不如说是真的不对(正如我怀疑的那样),如果真的对的话,那是什么问题。


1
这是决策归属的问题。将决策移到中心位置,而不要乱扔它们。这将使复杂度低于每次有if时都具有两倍的代码路径。

28
马丁·福勒(Martin Fowler)对此发表了一篇文章:martinfowler.com/bliki/FlagArgument.html
ChristofferHammarström2012年


@ChristofferHammarström尼斯链接。我将其包含在我的答案中,因为它在很多细节上解释了与我的解释相同的想法。
亚历克斯

1
我并不总是同意尼克所说的,但是在这种情况下,我100%同意:不要使用布尔参数
Marjan Venema

Answers:


107

是的,这可能是代码异味,这将导致难以维护的代码难以理解,并且容易重用的可能性较低。

正如其他发布者所指出的那样,上下文就是一切(如果过时了,或者如果这种做法被公认为是故意招致的技术债务,则不要费力地处理,以后再进行重构),但是从广义上讲,如果有参数通过进入选择要执行的特定行为的功能,然后需要进一步的逐步完善;将此功能分解为较小的功能将产生更高凝聚力的功能。

那么什么是高度内聚的功能?

这个函数只做一件事和一件事。

如您所描述的,传入的参数的问题在于该函数正在做两件事。它可能会也可能不会检查用户的访问权限,具体取决于布尔参数的状态,然后根据该决策树执行一项功能。

最好将访问控制的关注与任务,操作或命令的关注分开。

正如您已经指出的,这些担忧的相互缠绕似乎已经消失了。

因此,“内聚性”概念可以帮助我们确定所讨论的功能不是高度内聚的,并且可以重构代码以生成一组更具内聚力的功能。

因此可以重提这个问题。既然我们都同意最好避免传递行为选择参数,那么我们如何改善问题呢?

我会完全摆脱参数。即使在测试时也具有关闭访问控制的能力,这是潜在的安全风险。出于测试目的,访问检查以测试允许的访问和拒绝访问的方案。

参考:凝聚力(计算机科学)


Rob,您能否解释一下凝聚力是什么以及它如何应用?

Ray,我对此的思考越深,我就越应该反对基于访问控制将布尔值引入应用程序的布尔值的安全漏洞。对代码库的质量改进将是一个很好的副作用;)
Rob 2012年

1
内聚力及其应用的很好的解释。这确实应该获得更多选票。我也确实同意安全性问题……尽管如果它们都是私有方法,那么这是一个较小的潜在漏洞
Ray

1
谢谢雷。在时间允许的情况下,听起来很容易进行重构。可能值得在TODO评论中强调该问题,在技术权威和对我们有时会面临的完成工作压力的敏感性之间取得平衡。
罗布2012年

无法

149

我很久以前就停止使用这种模式,原因很简单。维修费用。几次我发现我有一些函数说过的frobnicate(something, forwards_flag)代码,该函数在我的代码中被多次调用,并且需要找到代码中将值false作为值传递的所有位置forwards_flag。您无法轻松地搜索到这些内容,因此这成为维护工作的麻烦。并且,如果您需要在每个站点上进行错误修复,则可能会遇到一个不幸的问题。

但是,无需从根本上改变方法即可轻松解决此特定问题:

enum FrobnicationDirection {
  FrobnicateForwards,
  FrobnicateBackwards;
};

void frobnicate(Object what, FrobnicationDirection direction);

使用此代码,只需搜索的实例FrobnicateBackwards。尽管有可能有一些代码将其分配给变量,所以您必须遵循一些控制线程,但实际上,我发现这很少见,因此这种替代方法行之有效。

但是,至少在原则上以这种方式传递标志还有另一个问题。这就是说,某些(只有某些)具有这种设计的系统可能会将过多的关于代码的深层嵌套部分(使用标志)的实现细节暴露给外层(它们需要知道要传递哪个值)在此标志)。要使用Larry Constantine的术语,此设计在设置器和布尔标志的用户之间可能具有过强的耦合。弗兰基(Franky),尽管在不进一步了解代码库的情况下很难对这个问题有任何确定性。

为了解决您提供的特定示例,我会在每个示例中都有一定程度的关注,但主要是出于风险/正确性的原因。就是说,如果您的系统需要传递指示系统处于什么状态的标志,则可能会发现您已经获得了应该考虑这一点但不检查参数的代码(因为它没有传递给此功能)。所以您有一个错误,因为有人省略了传递参数。

值得承认的是,几乎所有需要传递给每个函数的系统状态指示器实际上都是全局变量。全局变量的许多缺点都将适用。我认为,在许多情况下,最好将系统状态的知识(或用户的凭据或系统的身份)封装在一个对象中,该对象负责根据该数据正确执行操作。然后,您传递对该对象的引用,而不是原始数据。这里的关键概念是封装


9
真正的好例子,以及对我们正在处理的事物的本质及其对我们的影响的深刻见解。

33
+1。我为此尽可能使用枚举。我见过一些函数,这些函数bool以后会添加额外的参数,并且调用开始看起来像DoSomething( myObject, false, false, true, false )。不可能弄清楚额外的布尔参数的含义,而对于有意义地命名的枚举值,这很容易。
Graeme Perrow'5

17
噢,天哪,终于是如何FrobnicateBackwards的一个很好的例子。一直在寻找这个。
Alex Pritchard

38

这不一定是错误的,但可以表示代码的味道

关于布尔参数应避免的基本情况是:

public void foo(boolean flag) {
    doThis();
    if (flag)
        doThat();
}

然后打电话时,你通常会打电话foo(false),并foo(true)取决于你想要的确切行为。

这确实是一个问题,因为这是内聚力差的情况。您在方法之间创建了不必要的依赖关系。

在这种情况下,您应该做的是离开doThisdoThat作为单独的公共方法进行操作:

doThis();
doThat();

要么

doThis();

这样,您就可以将正确的决定留给调用方(就像您要传递布尔参数一样)而无需创建耦合。

当然,并不是所有的布尔参数都以这种不好的方式使用,但这绝对是一种代码味道,如果您在源代码中看到很多,就可以怀疑了。

这只是根据我编写的示例来解决此问题的一个示例。在其他情况下,有必要采用其他方法。

马丁·福勒(Martin Fowler)的一篇很好的文章进一步解释了相同的想法。

PS:如果方法foo而不是调用两个简单的方法具有更复杂的实现,那么您所要做的就是应用一个小的重构提取方法,因此生成的代码看起来类似于foo我编写的实现。


1
感谢您喊出“代码气味”一词-我知道它闻起来很难闻,但无法完全理解气味是什么。您的示例与我正在查看的内容完全一致。

24
在很多情况下,if (flag) doThat()内部foo()是合法的。doThat()向每个调用方推送有关调用的决定会导致重复,如果您以后发现某些方法,则必须删除该重复,该flag行为也需要调用doTheOther()。我宁愿将依赖关系放在同一个类中,而不是以后必须去搜索所有调用者。
Blrfl 2012年

1
@Blrfl:是的,我认为更直接的重构将是创建a doOnedoBoth方法(分别用于错误和真实的情况),或使用单独的枚举类型,如James Youngman所建议
hugomg 2012年

@missingno:您仍然有将冗余代码推出给调用者以做出doOne()doBoth()决定的相同问题。子例程/函数/方法具有自变量,因此可以改变其行为。如果参数的名称已经说明了它的作用,那么将枚举用于真正的布尔条件听起来很像重复自己。
Blrfl 2012年

3
如果一个接一个地调用两个方法或一个方法被认为是多余的,那么编写try-catch块或一个if否则也是多余的。这是否意味着您将编写一个函数来抽象所有这些函数?没有!请注意,创建一个仅调用另一个方法的方法并不一定表示一个好的抽象。
亚历克斯(Alex)

29

首先,编程不是一门科学,而是一门艺术。因此,很少有“错误”和“正确”的编程方式。大多数编码标准只是一些程序员认为有用的“首选项”。但最终它们相当随意。因此,我永远不会将参数选择本身标记为“错误”,当然也不会像布尔参数那样泛泛而有用。在许多情况下,使用boolean(或int,表示)来封装状态是完全合理的。

总体而言,编码决策应主要基于性能和可维护性。如果性能没有受到威胁(并且我无法想象您的示例中的性能如何),那么您的下一个考虑因素应该是:这对我(或未来的编校)而言将有多容易维护?它直观易懂吗?它是孤立的吗?在这方面,您的链接函数调用示例实际上似乎很脆弱:如果您决定将to更改bIsUpbIsDown,那么代码中还需要更改多少其他位置?另外,您的参数清单是否激增?如果您的函数具有17个参数,则可读性是一个问题,您需要重新考虑是否欣赏了面向对象体系结构的好处。


4
我感谢第一段中的警告。我故意说出“错误”来挑衅,当然也承认我们正在处理“最佳实践”和设计原则领域,而且这类事情通常都是情境性的,必须权衡多个因素
雷·

13
您的回答让我想起了一个引文,我不记得它的出处了:“如果您的函数有17个参数,则可能缺少一个”。
Joris Timmermans,2012年

我非常同意这一观点,并回答以下问题:是的,传递布尔标志通常是一个坏主意,但从来没有像坏/好那么简单……
JohnB 2013年

15

我认为Robert C Martins Clean代码文章指出,应尽可能消除布尔型参数,因为它们表明一种方法可以完成多项工作。一种方法应该做一件事情,而我认为只有一件事情是他的座右铭之一。


@dreza是您指的是Curlys Law
MattDavey

当然,根据经验,您应该知道什么时候应该忽略此类争论。
gnasher729

8

我认为这里最重要的是实用。

当布尔值确定整个行为时,只需再创建一个方法即可。

当布尔值仅确定中间部分的行为时,您可能希望将其保留为一个,以减少代码重复。在可能的情况下,您甚至可以将方法分成三部分:两种用于布尔选项的调用方法,另一种用于完成大部分工作。

例如:

private void FooInternal(bool flag)
{
  //do work
}

public void Foo1()
{
  FooInternal(true);
}

public void Foo2()
{
  FooInternal(false);
}

当然,在实践中,您总是会在这些极端之间找到一点。通常,我只是选择感觉正确的东西,但是我宁愿在减少代码重复的方面犯错。


我仅使用布尔参数来控制私有方法中的行为(如此处所示)。但是问题是:如果某些dufus决定FooInternal在将来增加可见度,那又如何呢?
ADTC 2014年

实际上,我会走另一条路。FooInternal内部的代码应分为4部分:2个用于处理布尔值true / false情况的函数,一个用于发生在之前的工作,另一个用于在发生之后的工作。然后,您Foo1成为:{ doWork(); HandleTrueCase(); doMoreWork() }。理想情况下,doWorkdoMoreWork函数分别分解为(一个或多个)有意义的独立动作块(即,作为单独的函数),而不仅仅是为了分割而划分的两个函数。
jpaugh

7

我喜欢通过返回不可变实例的生成器方法来自定义行为的方法。这是番石榴的Splitter用法:

private static final Splitter MY_SPLITTER = Splitter.on(',')
       .trimResults()
       .omitEmptyStrings();

MY_SPLITTER.split("one,,,,  two,three");

这样做的好处是:

  • 出色的可读性
  • 明确区分配置和操作方法
  • 通过强迫您考虑对象是什么,对象应该做什么和不应该做什么来促进凝聚力。在这种情况下,它是一个Splitter。您永远不会放入someVaguelyStringRelatedOperation(List<Entity> myEntities)名为的类Splitter,但会考虑将其作为静态方法放入StringUtils类中。
  • 实例是预先配置的,因此很容易依赖注入。客户端不必担心是通过传递true还是传递false给方法来获取正确的行为。

1
作为部分番石榴爱好者和传教士,我偏爱您的解决方案...但是我不能给您+1,因为您跳过了我真正想要的部分,这是错误的(或有臭味的)关于另一种方式。我认为这实际上在您的某些解释中是隐含的,因此,如果您可以明确地表明这一点,它将更好地回答这个问题。

我喜欢将配置方法和acton方法分开的想法!
Sher10ck

番石榴图书馆的链接已断开
Josh Noe

4

绝对有代码味。如果它没有违反“ 单一责任原则”,则可能违反了“告诉,不要问”。考虑:

如果事实证明没有违反这两个原则之一,则仍应使用枚举。布尔标志是魔术数字的布尔等效项。foo(false)与...一样有意义bar(42)。枚举对策略模式很有用,并且具有让您添加其他策略的灵活性。(请记住要适当命名它们。)

您的具体例子特别困扰我。为什么此标志通过许多方法传递?听起来您需要将参数拆分为子类


4

TL; DR:请勿使用布尔参数。

参见下文,了解它们的坏处以及如何更换它们(粗体显示)。


布尔参数很难理解,因此很难维护。主要问题是,当您读取命名该参数的方法签名时,目的通常很清楚。但是,大多数语言通常不需要命名参数。因此,您将拥有一些反模式,例如RSACryptoServiceProvider#encrypt(Byte[], Boolean)boolean参数确定函数中将使用哪种加密方式。

因此,您将收到如下电话:

rsaProvider.encrypt(data, true);

读者必须在其中查找方法的签名,才能确定到底是什么true意思。传递整数当然同样糟糕:

rsaProvider.encrypt(data, 1);

会告诉您或多或少:即使您定义了要用于整数的常量,函数的用户也可能会忽略它们并继续使用文字值。

解决此问题的最佳方法是使用枚举。如果您必须传递RSAPadding带有两个值的枚举:OAEP否则PKCS1_V1_5您将立即能够读取代码:

rsaProvider.encrypt(data, RSAPadding.OAEP);

布尔值只能有两个值。这意味着,如果您有第三种选择,则必须重构签名。通常,如果向后兼容是一个问题,则无法轻松执行,因此您必须使用其他公共方法扩展任何公共类。这是Microsoft最终在引入RSACryptoServiceProvider#encrypt(Byte[], RSAEncryptionPadding)使用枚举(或至少是模仿枚举的类)而不是布尔值的地方时所做的事情。

如果需要对参数本身进行参数化,则甚至可以更容易地使用完整的对象或接口作为参数。在上面的示例中,可以使用散列值对OAEP填充自身进行参数化,以供内部使用。请注意,现在有6种SHA-2哈希算法和4种SHA-3哈希算法,因此,如果仅使用单个枚举而不是参数,则枚举值的数量可能会激增(这可能是Microsoft下一步要发现的事情) )。


布尔参数也可能表示该方法或类的设计不正确。与上面的示例一样:.NET以外的任何加密库都根本不在方法签名中使用填充标志。


我喜欢警告几乎所有的软件专家都反对布尔参数。例如,约书亚·布洛赫(Joshua Bloch)在备受赞誉的《有效Java》一书中警告他们。通常,不应使用它们。您可能会争辩说,如果存在一个易于理解的参数,则可以使用它们。但是即使这样:Bit.set(boolean)最好使用以下两种方法来更好地实现:Bit.set()Bit.unset()


如果您不能直接重构代码,则可以定义常量以至少使它们更具可读性:

const boolean ENCRYPT = true;
const boolean DECRYPT = false;

...

cipher.init(key, ENCRYPT);

比以下内容更具可读性:

cipher.init(key, true);

即使您希望:

cipher.initForEncryption(key);
cipher.initForDecryption(key);

代替。


3

我很惊讶没有人提到命名参数

我看到的布尔标志的问题是它们损害了可读性。例如,什么是true

myObject.UpdateTimestamps(true);

做?我不知道。但是关于:

myObject.UpdateTimestamps(saveChanges: true);

现在很清楚,我们要传递的参数是要做什么的:我们要告诉函数保存其更改。在这种情况下,如果该类是非公共类,那么我认为布尔参数很好。


当然,您不能强迫您的班级用户使用命名参数。因此,enum根据您的默认情况的普遍程度,通常最好使用一个或两个单独的方法。.Net正是这样做的:

//Enum used
double a = Math.Round(b, MidpointRounding.AwayFromZero);

//Two separate methods used
IEnumerable<Stuff> ascendingList = c.OrderBy(o => o.Key);
IEnumerable<Stuff> descendingList = c.OrderByDescending(o => o.Key); 

1
问题不是关于什么比行为确定标志更可取,而是这样的标志是否闻到气味,如果是,为什么?
Ray

2
@Ray:我认为这两个问题没有区别。在可以强制使用命名参数或可以确定始终使用命名参数的语言(例如,私有方法)中,可以使用布尔参数。如果该语言(C#)无法强制使用命名参数,并且该类是公共API的一部分,或者该语言不支持命名参数(C ++),则myFunction(true)可能会编写类似这样的代码,则该代码为-闻。
BlueRaja-Danny Pflughoeft 2012年

命名参数方法更加错误。如果没有名称,将被迫阅读API文档。使用名称,您认为不需要:但是参数可能会被误命名。例如,它本可以用于保存(所有)更改,但是后来实现更改了一点,以便仅保存较大的更改(对于big的某些值)。
Ingo 2013年

@Ingo我不同意。那是一个通用的编程问题。您可以轻松定义其他SaveBig名称。任何代码都可以修改,这种修改不是特定于命名参数的。
Maarten Bodewes,

1
@Ingo:如果你被白痴包围,那么你去别的地方找工作。这类事情就是您需要代码审查的地方。
gnasher729

1

我不太清楚这是怎么回事。

如果它看起来像是代码气味,感觉像是代码气味,并且-很好-闻起来像代码气味,则可能是代码气味。

您想要做的是:

1)避免有副作用的方法。

2)手柄必要状态与中央,正式状态机(如)。


1

我同意使用布尔参数不能确定性能的所有问题; 提高了可读性,可靠性,降低了复杂性,降低了封装和内聚力差带来的风险以及具有可维护性的总体拥有成本的降低。

我从70年代中期开始设计硬件,我们现在将其称为SCADA(监控和数据采集),它们是经过微调的硬件,其EPROM中的机器代码运行宏遥控器并收集高速数据。

该逻辑称为MealeyMoore 机器,我们现在称为有限状态机。除非这是一台具有有限执行时间的实时计算机,然后必须使用快捷方式来达到目的,否则还必须按照与上述相同的规则来完成这些操作。

数据是同步的,但命令是异步的,命令逻辑遵循无记忆布尔逻辑,但具有基于先前,当前和期望的下一个状态的存储器的顺序命令。为了使它能够以最高效的机器语言(仅64kB)运行,我们非常谨慎地以启发式IBM HIPO方式定义每个流程。有时这意味着传递布尔变量并执行索引分支。

但是现在有了很多的存储空间和OOK的简便性,封装已成为当今必不可少的组成部分,但是当以字节为单位计算实时代码和SCADA机器代码时,代价是不小的。


0

它不一定是错误的,但是在具体的操作示例中,取决于“用户”的某些属性,我将通过对用户的引用而不是标志。

这可以通过多种方式澄清和帮助。

任何阅读调用语句的人都会意识到结果将根据用户而改变。

在最终调用的函数中,您可以轻松实现更复杂的业务规则,因为您可以访问任何用户属性。

如果“链”中的一个功能/方法根据用户属性执行不同的操作,则很有可能将对用户属性的类似依赖关系引入“链”中的其他一些方法。


0

大多数时候,我会认为这种编码不好。但是,我可以想到两种情况,这可能是一个好习惯。由于已经有很多答案说出它不好的原因,因此我两次给出可能很好的答案:

第一个是序列中的每个调用本身是否有意义。它将使意义,如果调用代码可以从真更改为假或假为真,或者如果调用的方法可能会改为使用布尔参数,而不是直接将它传递出去。连续十次这样的调用的可能性很小,但是有可能发生,并且如果这样做,将是一种很好的编程习惯。

考虑到我们正在处理布尔值,第二种情况有些麻烦。但是,如果程序具有多个线程或事件,则传递参数是跟踪线程/事件特定数据的最简单方法。例如,一个程序可能从两个或多个套接字获取输入。为一个套接字运行的代码可能需要生成警告消息,而为另一个套接字运行的代码可能不需要。然后(某种程度上)有意义的是,布尔值集在很高的级别上可以通过许多方法调用传递到可能生成警告消息的位置。数据无法以任何全局方式保存(非常困难的情况除外),因为多个线程或交错事件都需要各自的值。

可以肯定的是,在后一种情况下,我可能会创建一个仅包含布尔值的类/结构,并将其传递周围。几乎可以肯定,我很快就会需要其他字段,例如将警告消息发送到何处


即使使用您编写的类/结构(即上下文),枚举WARN,SILENT也会更有意义。或者实际上只是在外部配置日志记录-无需传递任何内容。
Maarten Bodewes,

-1

上下文很重要。这种方法在iOS中非常普遍。仅作为一个经常使用的示例,UINavigationController提供了方法-pushViewController:animated:,而animated参数是BOOL。该方法在两种方式上执行的功能基本上相同,但是如果您输入YES,则动画化从一个视图控制器到另一个视图控制器的过渡,如果您传递NO,则它不动画。这似乎是完全合理的。不得不提供两种方法来代替这一方法是很愚蠢的,以便您可以确定是否使用动画。

在Objective-C中证明这种情况可能更容易,因为与使用诸如C和Java这样的语言相比,方法命名语法为每个参数提供更多的上下文。不过,我认为采用单个参数的方法很容易采用布尔值,并且仍然有意义:

file.saveWithEncryption(false);    // is there any doubt about the meaning of 'false' here?

21
实际上,我不知道示例中的false含义file.saveWithEncryption。这是否意味着无需加密即可保存?如果是这样,为什么在世界范围内该方法的名称中会带有“具有加密”的权限?我可以理解有一种类似的方法save(boolean withEncryption),但是当我看到时file.save(false),乍一看该参数表明使用或不使用加密都是完全不明显的。我认为,实际上,这是詹姆斯·扬曼(James Youngman)关于使用枚举的第一点。

6
另一个假设是false,不要覆盖任何同名的现有文件。我知道人为的示例,但是请确保您需要检查该功能的文档(或代码)。
James Youngman

5
saveWithEncryption有时无法与加密一起保存的名为的方法是一个错误。它应该是Java file.encrypt().save()或类似Java new EncryptingOutputStream(new FileOutputStream(...)).write(file)
ChristofferHammarström'5

2
实际上,执行除声明之外的其他功能的代码不起作用,因此是一个错误。它不会产生一个saveWithEncryption(boolean)有时不进行加密而保存的方法,就像它不会产生一个saveWithoutEncryption(boolean)有时会因加密而保存的方法一样。
ChristofferHammarström'5

2
这种方法是不好的,因为显然“这里对'假'的含义存在怀疑”。无论如何,我永远都不会首先编写类似的方法。保存和加密是独立的操作,一种方法应该做一件事并且做得很好。请参阅我之前的评论,以获取有关如何执行此操作的更好示例。
ChristofferHammarström'5
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.