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


39

根据使用布尔参数确定行为是否错误?,我知道避免使用布尔参数来确定行为的重要性,例如:

原始版本

public void setState(boolean flag){
    if(flag){
        a();
    }else{
        b();
    }
    c();
}

新版本:

public void setStateTrue(){
    a();
    c();
}

public void setStateFalse(){
    b();
    c();
}

但是,使用boolean参数来确定值而不是行为的情况如何呢?例如:

public void setHint(boolean isHintOn){
    this.layer1.visible=isHintOn;
    this.layer2.visible=!isHintOn;
    this.layer3.visible=isHintOn;
}

我试图消除isHintOn标志并创建2个单独的函数:

public void setHintOn(){
    this.layer1.visible=true;
    this.layer2.visible=false;
    this.layer3.visible=true;
}

public void setHintOff(){
    this.layer1.visible=false;
    this.layer2.visible=true;
    this.layer3.visible=false;
}

但修改后的版本似乎难以维护,原因是:

  1. 它具有比原始版本更多的代码

  2. 它不能清楚地表明layer2的可见性与hint选项相反

  3. 当添加一个新层(例如:layer4)时,我需要添加

    this.layer4.visible=false;
    

    this.layer4.visible=true;  
    

    分别进入setHintOn()和setHintOff()

所以我的问题是,如果布尔参数仅用于确定值,而不用于确定行为(例如:在该参数上没有if-else),是否仍建议消除该布尔参数?


26
如果生成的代码更具可读性和可维护性,那永远不会错;-)我建议使用单个方法而不是两个单独的方法。
helb

32
您提出了一个令人信服的论点,即设置这些布尔值的方法的单个实现将使类的维护和对其实现的理解更加容易。很好; 这些是合理的考虑。但是,该类的公共接口不需要变形即可容纳它们。如果使用单独的方法使公共接口更易于理解和使用,请将您定义setHint(boolean isHintOn)私有方法,并添加分别调用和的public setHintOnsetHintOff方法。setHint(true)setHint(false)
Mark Amery

9
我对那些方法名感到非常不满意:它们实际上并没有提供任何好处setHint(true|false)。马铃薯马铃薯。至少使用类似setHint和的东西unsetHint
康拉德·鲁道夫


4
@kevincline如果条件是一个名称,请is在开头写。isValid等等。为什么要改成两个字呢?此外,情人眼中的“更自然”。如果你想,它的发音是英语句子,那么对我来说会更自然的事“如果提示的是”有“的”已塞入。
利斯特先生

Answers:


95

API设计应从调用方着眼于最适合API客户端的功能。

例如,如果此新API要求调用方定期编写这样的代码

if(flag)
    foo.setStateTrue();
else
    foo.setStateFalse();

那么很明显,避免使用该参数比使用允许调用者编写的API更糟糕

 foo.setState(flag);

前一个版本仅产生一个问题,然后必须在调用方解决(可能不止一次)。这既不会增加可读性,也不会增加可维护性。

执行方面,但是,不应该决定了公共API看起来怎么样。如果像setHint参数这样的函数在实现中需要较少的代码,但是对于客户端而言,用术语setHintOn/ setHintOff看起来更容易使用的API ,则可以通过以下方式实现:

private void setHint(boolean isHintOn){
    this.layer1.visible=isHintOn;
    this.layer2.visible=!isHintOn;
    this.layer3.visible=isHintOn;
}

public void setHintOn(){
   setHint(true);
}

public void setHintOff(){
   setHint(false);
}

因此,尽管公共API没有布尔参数,但是这里没有重复的逻辑,因此,当新要求(例如问题的示例)到达时,只有一个地方可以更改。

反之亦然:如果上述setState方法需要在两个不同的代码段之间进行切换,则可以将该代码段重构为两个不同的私有方法。因此,恕我直言,通过查看内部结构来搜索用于确定“一个参数/一个方法”和“零参数/两个方法”的标准是没有意义的。但是,请看一下您希望以谁的身份来使用API​​的方式。

如有疑问,请尝试使用“测试驱动的开发”(TDD),这将迫使您考虑公共API以及如何使用它。


DocBrown,您是否说合适的分界线是每种状态的设置是否具有复杂且可能不可逆的副作用?例如,如果您只是切换一个标志,该标志可以执行其在锡罐上的指示,而没有底层状态机,则可以对不同的状态进行参数化。举例来说,您不会参数化诸如之类的方法SetLoanFacility(bool enabled),因为提供了贷款,将其再次拿走可能并不容易,并且这两个选项可能涉及完全不同的逻辑-您可能希望将创建分开/删除方法。
史蒂夫

15
@Steve:您仍在尝试根据在实现方面看到的要求来设计API。坦率地说:这完全无关紧要。从调用端使用公共API的任何变体都更容易使用。在内部,您总是可以让两个公共方法通过参数调用一个私有方法。反之亦然,您可以让一个带有参数的方法在两个具有不同逻辑的私有方法之间切换。
布朗

@Steve:看我的编辑。
布朗

我同意您的所有观点-实际上,我是从呼叫者的角度考虑问题的(因此,我引用“锡罐上的字眼”),并试图制定适当的规则,使呼叫者通常希望使用每种方法。在我看来,规则是调用者是否希望重复调用是幂等的,并且状态转换不受限制并且没有复杂的副作用。切换房间照明开关的开启和关闭将被参数化,开启或关闭区域电站的切换将是多方法的。
史蒂夫

1
@Steve因此,如果用户需要确认特定设置,则Toggle()不是提供的正确功能。这就是重点;如果呼叫者只关心“更改”,而不关心“最终结果是什么”,那么Toggle()该选项可以避免进行额外的检查和决策。我不会称其为COMMON案例,也不建议在没有充分理由的情况下使其可用,但是如果用户需要切换,则可以给他们切换。
卡米尔·德拉卡里

40

马丁·福勒(Martin Fowler)引用肯特·贝克(Kent Beck)推荐的单独setOn() setOff()方法,但他又说这不应被视为不可侵犯:

如果你拉从一个布尔源(原文如此)的数据,如UI控件或数据源,我宁愿有setSwitch(aValue)

if (aValue)
  setOn();
else
  setOff();

这是一个示例,应该编写一个API来简化调用者的操作,因此,如果我们知道调用者来自何处,则应牢记该信息来设计API。这也表明,如果我们以两种方式获得呼叫者,有时我们可能会同时提供两种样式。

另一项建议是使用一个枚举值或标志类型给予truefalse更好的,上下文的具体名称。在你的榜样,showHinthideHint可能会更好。


16
以我的经验,由于您的答案中引用了if / else代码,因此setSwitch(value)几乎总比setOn / setOff生成的整体代码少。我倾向于诅咒给我提供setOn / setOff而不是setSwitch(value)的API的开发人员。
18年

1
从相似的角度看待它:如果您需要对值进行硬编码,那么这两种方法都很容易。但是,如果您需要将其设置为例如用户输入,并且您可以直接将值传递进来,则可以节省一步。
Nic Hartley

@ 17of26作为一个具体的反例(表明“取决于”而不是一个相对于另一个),在Apple的AppKit中-[NSView setNeedsDisplay:],您可以通过一种方法YES来重绘视图,NO是否应该重绘。您几乎不需要告诉它不要这样做,因此UIKit只是-[UIView setNeedsDisplay]没有参数。它没有相应的-setDoesNotNeedDisplay方法。
Graham Lee

2
@GrahamLee,我认为Fowler的论点相当微妙,而且确实取决于判断。我不会bool isPremium在他的示例中使用标志,但是BookingType bookingType除非枚举的逻辑完全不同,否则我将使用enum()参数化相同的方法。如果人们希望能够看到两种模式之间的差异,那么福勒所指的“纠结逻辑”通常是可取的。如果它们根本不同,我将在外部公开参数化方法,并在内部实现单独的方法。
史蒂夫

3

我认为您在帖子中混合了两件事,即API和实现。在这两种情况下,我都不认为您可以一直使用严格的规则,但是您应该(尽可能)独立考虑这两件事。

让我们从API入手:

public void setHint(boolean isHintOn)

和:

public void setHintOn()
public void setHintOff()

是有效的替代方法,具体取决于对象应提供的内容以及客户端如何使用API​​。正如Doc所指出的,如果您的用户已经有一个布尔变量(来自UI控件,用户操作,外部,API等),则第一个选项更有意义,否则,您将在客户端代码上强制执行额外的if语句。但是,例如,如果您在开始过程时将提示更改为true,而在结束时更改为false,则第一个选项将为您提供以下信息:

setHint(true)
// Do your process
…
setHint(false)

而第二个选项为您提供:

setHintOn()
// Do your process
…
setHintOff()

哪个IMO更具可读性,因此在这种情况下,我将选择第二个选项。显然,没有什么可以阻止您同时提供这两种选择(或者,例如,可以使用Graham所说的枚举,如果这样做更有意义)。

关键是您应该基于对象应该做什么以及客户端将如何使用它来选择API,而不要基于您将如何实现它。

然后,您必须选择如何实现公共API。假设我们选择了这些方法setHintOnsetHintOff并将它们作为我们的公共API,并且它们与您的示例一样共享此通用逻辑。您可以通过私有方法(从Doc复制的代码)轻松抽象此逻辑:

private void setHint(boolean isHintOn){
    this.layer1.visible=isHintOn;
    this.layer2.visible=!isHintOn;
    this.layer3.visible=isHintOn;
}

public void setHintOn(){
   setHint(true);
}

public void setHintOff(){
   setHint(false);
}

相反,假设我们选择setHint(boolean isHintOn)了一个API,但由于某些原因,将提示On设置为Off完全不同,因此可以反转您的示例。在这种情况下,我们可以按以下方式实现它:

public void setHint(boolean isHintOn){
    if(isHintOn){
        // Set it On
    } else {
        // Set it Off
    }    
}

甚至:

public void setHint(boolean isHintOn){    
    if(isHintOn){
        setHintOn()
    } else {
        setHintOff()
   }    
}

private void setHintOn(){
   // Set it On
}

private void setHintOff(){
   // Set it Off 
}

关键是,在这两种情况下,我们都首先选择了公共API,然后对实现进行了调整以适合所选的API(以及我们所拥有的约束),而不是相反。

顺便说一句,我认为您所链接的关于使用布尔参数确定行为的帖子也是如此,也就是说,您应该根据您的特定用例而不是某些硬性规则来决定(尽管在这种特定情况下通常是正确的做法将其分解为多种功能)。


3
附带说明一下,如果它类似于伪块(开头是一个语句,结尾是一个语句),则可能应该使用beginend或同义词,只是为了明确表明它们是在做什么,并暗示开始必须有结束,反之亦然。
Nic Hartley

我同意Nic的观点,并补充说:如果您要确保起点和终点始终保持联系,则还应为此提供特定于语言的习惯用法:C ++中的RAII /作用域警戒,C#中的usingwith,Python中的语句上下文管理器,传递的主体作为一个lambda或可调用的对象(例如红宝石块语法)等
丹尼尔Pryden

我同意以上两点,我只是想举一个简单的例子来说明另一种情况(尽管你们都指出这是一个不好的例子:))。
jesm00

2

首先,代码不会自动减少可维护性,只是因为代码会更长一些。清晰至关重要。

现在,如果您实际上只是在处理数据,那么您拥有的是布尔属性的二传手。在这种情况下,您可能只想直接存储该值并得出层的可见性,即

bool isBackgroundVisible() {
    return isHintVisible;
}    

bool isContentVisible() {
    return !isHintVisible;
}

(我已经自由地给层赋予了实际的名称-如果您在原始代码中没有此名称,那么我将从此开始)

这仍然使您面临是否有setHintVisibility(bool)方法的问题。就个人而言,我建议将其替换为showHint()hideHint()方法-两者都将非常简单,并且在添加图层时无需更改它们。但是,这不是明确的对/错。

现在,如果调用该函数实际上应该更改这些图层的可见性,那么您实际上就有了行为。在这种情况下,我绝对会建议使用单独的方法。


tl; dr是:您建议拆分为两个函数,因为添加图层时不必更改它们?如果添加了layer4,我们可能也需要说“ this.layer4.visibility = isHintOn”,所以我不确定是否同意。如果有的话,这是不利的,因为现在添加图层时,我们必须编辑两个功能,而不仅仅是一个。
Erdrik Ironrose

不,我(建议)为清晰起见(showHintvs setHintVisibility)推荐它。我提到新层只是因为OP对此有所担心。此外,我们只需要添加一种新方法:isLayer4VisibleshowHint并将属性hideHint设置isHintVisible为true / false,并且不会改变。
doubleYou

1
@doubleYou,您说较长的代码不会自动减少维护性。我要说的是,代码长度是可维护性和清晰度的主要变量之一,仅结构复杂性就超过了它。任何变得更长,结构更复杂的代码都应该得到它,否则,简单的问题将在代码中得到比应有的更为复杂的对待,并且代码库会获得不必要的行丛(“太多级别的间接”问题)。
史蒂夫

@Steve我完全同意您可以过度设计某些东西,即在不使代码变得更清晰的情况下延长代码的长度-otoh,您总是可以以清楚为代价使代码缩短,因此这里没有1:1的关系。
doubleYou

@Steve认为“代码高尔夫” -以这样的代码并且重写其在多行通常说得清楚。“代码打高尔夫球”是一个极端,但是仍然有很多程序员认为将所有内容塞进一个聪明的表达中是“优雅的”,甚至更快,因为编译器的优化不够好。
BlackJack

1

在第二个示例中,布尔参数很好。正如您已经知道的那样,布尔参数本身并不是问题。这是基于标志的切换行为,这是有问题的。

但是第一个示例还是有问题的,因为命名指示了setter方法,但是实现似乎有所不同。因此,您具有行为切换反模式一个误导性的方法。但是,如果该方法实际上是一个常规的setter(不进行行为切换),则不会有问题setState(boolean)。有两种方法,setStateTrue()并且setStateFalse()仅仅是不必要的复杂事情没有好处。


1

解决此问题的另一种方法是引入一个对象来表示每个提示,并让该对象负责确定与该提示关联的布尔值。这样,您可以添加新的排列,而不仅仅是具有两个布尔状态。

例如,在Java中,您可以执行以下操作:

public enum HintState {
    SHOW_HINT(true, false, true),
    HIDE_HINT(false, true, false);

    private HintState(boolean layer1Visible, boolean layer2Visible, boolean layer3Visible) {
         // constructor body and accessors omitted for clarity
    }
}

然后,您的呼叫者代码将如下所示:

setHint(HintState.SHOW_HINT);

您的实现代码如下所示:

public void setHint(HintState hint) {
    this.layer1Visible = hint.isLayer1Visible();
    this.layer2Visible = hint.isLayer2Visible();
    this.layer3Visible = hint.isLayer3Visible();
}

这使实现代码和调用者代码保持简洁,以换取定义新的数据类型,该数据类型将强类型的命名意图清楚地映射到相应的状态集。我认为这更好。


0

所以我的问题是,如果布尔参数仅用于确定值,而不用于确定行为(例如:在该参数上没有if-else),是否仍建议消除该布尔参数?

当我对这种事情有疑问时。我想描绘一下堆栈跟踪的样子。

多年以来,我从事一个PHP项目,该项目使用的功能与settergetter相同。如果传递null,它将返回该值,否则进行设置。和一起工作很恐怖

这是一个堆栈跟踪的示例:

function visible() : line 440
function parent() : line 398
function mode() : line 384
function run() : line 5

您不了解内部状态,这使调试变得更加困难。还有一堆其他的负面影响,但尝试看看有详细功能名称和清晰度当函数执行一个单一的动作。

现在,根据布尔值对具有A或B行为的函数的堆栈跟踪进行操作。

function bar() : line 92
function setVisible() : line 120
function foo() : line 492
function setVisible() : line 120
function run() : line 5

如果你问我,那真令人困惑。同一setVisible行产生两个不同的跟踪路径。

回到您的问题。尝试描绘堆栈跟踪的外观,如何与人交流发生了什么,并问问自己是否在帮助未来的人调试代码。

这里有一些提示:

  • 一个清晰的函数名,它隐含了意图,而无需知道参数值。
  • 一个功能执行一个动作
  • 该名称表示突变或不可变行为
  • 解决与语言和工具的调试能力相关的调试挑战。

有时代码在显微镜下看起来过多,但是当您拉回更大的图片时,一种极简主义的方法会使它消失。如果您需要将其突出以进行维护。添加许多小的功能可能会感到过于冗长,但是当在较大的上下文中广泛使用时,它会提高可维护性。


1
使我感到困惑的setVisible是,调用堆栈setVisible(true)似乎导致对的调用setVisible(false)(或者相反,这取决于您如何使该跟踪发生)。
David K

0

在几乎每种情况下,您都将boolean参数作为标志传递给方法以更改某些对象的行为时,应该考虑一种更显且类型安全的方法。

如果仅使用Enum表示状态的,就可以提高代码的理解力。

本示例使用Node来自的类JavaFX

public enum Visiblity
{
    SHOW, HIDE

    public boolean toggleVisibility(@Nonnull final Node node) {
        node.setVisible(!node.isVisible());
    }
}

总是比在许多JavaFX对象上都好:

public void setVisiblity(final boolean flag);

但我认为.setVisible().setHidden()对于标志为a 的情况,这是最好的解决方案,boolean因为它是最明确且最不冗长的。

对于具有多个选择的对象,以这种方式执行此操作尤为重要。EnumSet仅仅因为这个原因而存在。

埃德·曼(Ed Mann)在这个主题上有一篇非常好的博客文章。我正好解释他的话,所以为了避免重复,我只会在他的博客文章中发布一个链接,作为此答案的附录。


0

在确定用于传递(布尔)参数的某些接口的方法与不使用该参数的重载方法之间进行选择时,请查看使用方客户端。

如果所有用法都将传递恒定值(例如,true,false),则说明过载。

如果所有用法都传递一个变量值,则表明该方法具有参数方法。

如果这两种极端都不适用,则意味着存在多种客户端使用方式,因此您必须选择是支持两种形式,还是使一种类型的客户端适应另一种类型(对他们而言,这是不自然的)。


如果你正在设计一个集成的系统和你最后两个密码生产者和代码的“消费客户”?站在消费客户面前的人应该如何表达他们偏爱一种方法而不是另一种方法?
史蒂夫

@Steve,作为使用方,您知道要传递常量还是变量。如果传递常量,则首选不带参数的重载。
Erik Eidt

但是我有兴趣阐明为什么会这样。为什么不对有限数量的常量使用枚举,因为这在大多数语言中都是专门为这种目的而设计的轻量级语法?
史蒂夫

@Steve,如果我们在设计时/编译时知道客户端在所有情况下都将使用常量值(true / false),则表明确实存在两种不同的特定方法,而不是一种通用方法(带有参数)。我不赞成在不使用参数化方法时引入一般性-它是YAGNI参数。
埃里克·艾德

0

设计有两个注意事项:

  • API:您提供给用户的界面是什么,
  • 实施:清晰度,可维护性等。

它们不应混为一谈。

完全可以:

  • API委托中有多个方法可实现一个实施,
  • API会根据情况将一个方法分配给多个实现。

因此,任何试图通过实现设计的成本/收益平衡API设计的成本/收益的论点都是可疑的,应仔细研究。


在API方面

作为程序员,我通常会偏爱可编程API。当我可以转发一个值时,代码比需要一个if/ switch语句来确定要调用哪个函数的代码要清晰得多。

如果每个函数期望不同的参数,则后者可能是必需的。

因此,就您而言,单一方法setState(type value)似乎更好。

然而,没有什么比无名更糟truefalse2,等......这些神奇的价值观对自己没有任何意义。避免原始痴迷,并接受强类型。

因此,从API POV,我想:setState(State state)


在实施方面

我建议您做任何容易的事情。

如果方法很简单,则最好保持在一起。如果控制流比较复杂,则最好用多种方法将其分开,每种方法都处理一个子案例或管道的一个步骤。


最后,考虑分组

在您的示例中(添加了空格以提高可读性):

this.layer1.visible = isHintOn;
this.layer2.visible = ! isHintOn;
this.layer3.visible = isHintOn;

为什么要layer2逆势而上?它是功能还是错误?

可能有两个列表[layer1, layer3][layer2],并有一个明确的名称指示将它们分组在一起的原因,然后在这些列表上进行迭代。

例如:

for (auto layer : this.mainLayers) { // layer2
    layer.visible = ! isHintOn;
}
for (auto layer : this.hintLayers) { // layer1 and layer3
    layer.visible = isHintOn;
}

该代码不言自明,很明显为什么会有两个小组并且他们的行为不同。


0

setOn() + setOff()与vs set(flag)问题分开,我将仔细考虑布尔类型是否最好。您确定永远不会有第三个选择吗?

可能值得考虑使用枚举而不是布尔值。除了允许扩展性之外,这还使得更难以错误的方式获取布尔值,例如:

setHint(false)

setHint(Visibility::HIDE)

有了枚举,当有人决定需要“如果需要”选项时,扩展起来会容易得多:

enum class Visibility {
  SHOW,
  HIDE,
  IF_NEEDED // New
}

setHint(false)
setHint(true)
setHintAutomaticMode(true) // New

0

根据...,我知道避免使用布尔参数确定行为的重要性

我建议重新评估这些知识。

首先,在链接的SE问题中,我看不到您提出的结论。他们主要讨论的是在方法调用的多个步骤中转发参数,对参数的评估在链的最下方。

在您的示例中,您正在对方法中的参数进行评估。在这方面,它与任何其他种类的参数完全没有区别。

通常,使用布尔参数绝对没有错。显然,任何参数都可以决定行为,或者为什么首先要拥有它?


0

定义问题

您的标题问题是“ [...]错误吗?” -但是“错误”是什么意思?

根据C#或Java编译器,这没错。我确定您知道这一点,但这不是您要的。恐怕除此之外,我们对n程序员的n+1看法也不同。这个答案提出了“ 清洁代码”这本书要说的内容。

回答

通常,“ 干净代码”强烈反对函数参数:

争论很难。他们具有很大的概念力。[...]我们的读者每次看到它时都必须对它进行解释。

这里的“读者”可以是API使用者。也可以是下一个编码器,他们尚不知道此代码的功能-一个月后可能会成为您。他们将分别通过2个功能,或者两次通过1个功能,一次牢记true一次false
简而言之,请使用尽可能少的参数

标志参数的具体情况稍后将直接解决:

标志参数很丑陋。将布尔值传递给函数是一种非常糟糕的做法。它立即使该方法的签名复杂化,大声宣称此功能可以完成多项工作。如果标志为true,则执行一件事,如果标志为false,则另一件事!

为了直接回答您的问题:
根据Clean Code,建议消除该参数。


附加信息:

您的示例非常简单,但是即使在那儿,您也可以看到传播到代码的简单性:无参数函数仅执行简单的赋值,而另一个函数必须执行布尔算术才能达到相同的目标。在此简化示例中,这是微不足道的布尔运算,但在实际情况下可能会非常复杂。


我在这里看到了很多论点,您应该使其依赖于API用户,因为不得不在很多地方这样做很愚蠢:

if (isAfterSunset) light.TurnOn();
else light.TurnOff();

同意一些非最优发生在这里。也许看不出来,但是您的第一句话是提到“避免使用布尔参数确定行为的重要性”,这是整个问题的基础。我看不出有理由使API用户更不容易做到这一点。


我不知道您是否进行测试-在这种情况下,请考虑以下事项:

从测试的角度来看,争论甚至更加困难。想象一下编写所有测试用例以确保所有各种参数组合都能正常工作的困难。如果没有参数,这是微不足道的。


您已经在这里真正埋下了头绪:“ ...您的第一句话提到的是“避免使用布尔参数确定行为的重要性”,这是整个问题的基础。我看不到是使该事情变得不容易使API用户轻松完成的原因。” 这是一个有趣的观点,但是您宁愿破坏上一段中的论点。
通配符

被埋葬了吗?答案的核心是“使用尽可能少的参数”,这在答案的前半部分中得到了解释。此后的所有内容仅是附加信息:驳斥一个自相矛盾的论点(由另一个用户而非OP),并且某些不适用于每个人。
R. Schmitz

最后一段只是试图说明标题问题定义不充分,无法回答。OP询问是否“错误”,但没有根据谁或什么来说明。根据编译器?看起来像有效的代码,所以没有错。根据书的清洁代码?它使用标志参数,因此是“错误”。但是,我改写“ recommended”,因为实用>教条。您认为我需要更清楚一点吗?
R. Schmitz

因此,等等,您对答案的辩解是标题问题不清楚,无法回答?:D好...我实际上以为我引用的观点是一个有趣的新鲜观点。
通配符'18

1
现在清晰可见;做得好!
通配符'18
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.