如何用布尔清除器拨打电话?布尔陷阱


76

如@ benjamin-gruenbaum在评论中所指出的,这称为布尔陷阱:

说我有一个这样的功能

UpdateRow(var item, bool externalCall);

在我的控制器中,该值externalCall始终为TRUE。调用此函数的最佳方法是什么?我通常写

UpdateRow(item, true);

但是我问自己,我是否应该声明一个布尔值,以表明“真”值代表什么?您可以通过查看函数的声明来知道这一点,但是如果您看到类似以下内容,显然会更快,更清晰

bool externalCall = true;
UpdateRow(item, externalCall);

PD:不确定这个问题是否真的适合这里,如果不合适,我在哪里可以得到更多有关此的信息?

PD2:我没有标记任何语言,因为我认为这是一个非常普遍的问题。无论如何,我使用c#,并且可接受的答案适用于c#


10
这种模式也被称为布尔陷阱
本杰明Gruenbaum

11
如果您使用的语言支持代数数据类型,则我非常推荐使用新的adt而不是布尔值。data CallType = ExternalCall | InternalCall例如在haskell中。
菲利普·哈格隆德

6
我想Enums可以达到完全相同的目的;获取布尔值的名称以及一些类型安全性。
菲利普·哈格隆德

2
我不同意声明一个布尔值表示含义“明显更清晰”。使用第一个选项,很明显,您将始终通过true。对于第二个,您必须检查(变量定义在哪里?其值是否改变?)。当然,如果两行在一起,那不是问题……但是有人可能会决定添加代码的理想位置恰好在两行之间。它发生了!
AJPerez

声明布尔值并不干净,更清楚的是,它只是跳过了一个步骤,即查看相关方法的文档。如果您知道签名,那么添加一个新常量就不太清楚了。
Insidesin

Answers:


154

并非总是有完美的解决方案,但是您有许多选择可供选择:

  • 如果您的语言可用,请使用命名参数。这非常有效,并且没有任何特定的缺点。在某些语言中,任何参数都可以作为命名参数传递,例如updateRow(item, externalCall: true)C#)或update_row(item, external_call=True)(Python)。

    您建议使用单独的变量是模拟命名参数的一种方法,但没有相关的安全性(不能保证您为该参数使用了正确的变量名)。

  • 为您的公共接口使用不同的功能,并使用更好的名称。这是通过将参数值放在名称中来模拟命名参数的另一种方法。

    这是很容易理解的,但是会给编写这些功能的人带来很多样板。当存在多个布尔参数时,它也不能很好地应对组合爆炸。一个重要的缺点是客户端无法动态设置此值,但必须使用if / else来调用正确的函数。

  • 使用一个枚举。布尔值的问题在于它们分别称为“ true”和“ false”。因此,请改用名称更好的类型(例如enum CallType { INTERNAL, EXTERNAL })。另外一个好处是,这增加了程序的类型安全性(如果您的语言将枚举实现为不同的类型)。枚举的缺点是它们会向您的公开可见API添加类型。对于纯内部函数,这无关紧要,枚举没有明显的缺点。

    在没有枚举的语言中,有时会使用短字符串代替。这行得通,甚至可能比原始布尔值更好,但很容易产生错字。然后,该函数应立即断言该参数与一组可能的值匹配。

这些解决方案都不会对性能产生过大的影响。命名参数和枚举可以在编译时完全解析(对于已编译的语言)。使用字符串可能涉及字符串比较,但是对于小的字符串文字和大多数类型的应用程序而言,其成本可以忽略不计。


7
我刚刚了解到存在“命名参数”。我认为这是迄今为止最好的建议。如果您可以就该选项进行详细介绍(这样其他人就不必对此太过google了),我将接受此答案。如果可以的话,也可以在性能上得到一些暗示。谢谢
Mario Garcia '18

6
可以帮助缓解短字符串问题的一件事是对字符串值使用常量。@MarioGarcia没有太多要详细说明的了。除了此处提到的内容外,大多数细节都取决于特定的语言。您是否想在这里提到一些特别的东西?
jpmc26

8
在我们谈论的是一个方法,语言枚举可以作用域I类一般用枚举。它是完全自记录的(在声明enum类型的单行代码中,所以它也是轻量级的),完全可以防止使用裸布尔调用该方法,并且对公共API的影响可以忽略不计(因为标识符限于该类)。
davidbak

4
在此角色中使用字符串的另一个缺点是,与枚举不同,您无法在编译时检查其有效性。
罗斯兰

32
枚举是赢家。仅仅因为参数只能具有两种可能的状态之一,并不意味着它应该自动为布尔值。
皮特

39

正确的解决方案是按照您的建议进行操作,但是将其打包成一个迷你外观:

void updateRowExternally() {
  bool externalCall = true;
  UpdateRow(item, externalCall);
}

可读性胜过微观优化。您可以负担得起额外的函数调用,这肯定比您可以负担得起甚至必须一次查找布尔标志的语义的开发人员的负担更好。


8
当我在控制器中只调用一次函数时,这种封装真的值得吗?
马里奥·加西亚

14
是。是的。就像我在上面写的那样,原因是成本(函数调用)比收益小(您节省了不小的开发人员时间,因为没有人必须去看做什么true)。即使这样做也值得由于CPU时间便宜得多,因此花费的CPU时间与节省开发人员的时间一样多。
凯莉安·佛斯

8
另外,任何称职的优化器都应该能够为您内联该代码,这样您最终就不会丢失任何东西。
firedraco

33
@KilianFoth除非那是一个错误的假设。维护人员确实需要知道第二个参数的作用,以及为什么它是正确的,并且几乎总是与您要执行的操作的细节有关。将功能隐藏在千篇一律的微小功能中会降低可维护性,而不是增加可维护性。我已经看到了这一点我自己,伤心地说。实际上,过度划分功能要比庞大的上帝功能差。
格雷厄姆

6
我认为UpdateRow(item, true /*external call*/);使用允许该注释语法的语言会更干净。在一个简单的情况下,使用额外的功能膨胀代码只是为了避免编写注释似乎并不值得。也许如果此函数有很多其他参数,和/或周围的代码杂乱无章,那将开始吸引更多人。但是我认为,如果您要调试,您将最终检查包装器函数以查看其功能,并且必须记住哪些函数是库API的精简包装器,而哪些实际上具有某些逻辑。
彼得·科德斯

25

说我有一个像UpdateRow(var item,bool externalCall);

为什么会有这样的功能?

在什么情况下,您可以将externalCall参数设置为不同的值来调用它?

如果一个来自外部客户端应用程序,而另一个来自同一程序(即不同的代码模块),那么我认为您应该有两种不同的方法,每种方法一种,甚至可能在不同的情况下定义。接口。

但是,如果您基于从非程序源(例如,读取的配置文件或数据库)获取的某些数据进行调用,则布尔传递方法会更有意义。


6
我同意。只是为了敲击其他读者的观点,可以对所有调用者进行分析,这些调用者将传递true,false或变量。如果没有找到后者,我会在使用布尔值的情况下争辩两种单独的方法(不带布尔值),这是YAGNI参数,布尔值引入了未使用/不必要的复杂性。即使有一些调用方正在传递变量,我仍然可以提供(布尔值)无参数版本,以供传递常量的调用方使用-更简单,这很好。
埃里克·埃德特

18

虽然我同意使用语言功能来增强可读性和值安全性是理想的,但您也可以选择一种实用的方法:呼叫时注释。喜欢:

UpdateRow(item, true /* row is an external call */);

要么:

UpdateRow(item, true); // true means call is external

或(正确地,按照Frax的建议):

UpdateRow(item, /* externalCall */true);

1
没有理由投票。这是一个完全正确的建议。
Suncat2000

感谢<3 @ Suncat2000!
主教

11
一个(可能更可取的)变体只是在其中放置普通的参数名称,例如UpdateRow(item, /* externalCall */ true )。全句注释更难解析,实际上主要是杂音(尤其是第二种变体,它也与参数非常松散地耦合在一起)。
Frax

1
到目前为止,这是最合适的答案。这正是评论的目的。
哨兵

9
不太喜欢这样的评论。如果在重构或进行其他更改时不加修改,评论会变得过时。更不用说一旦您拥有多个布尔参数,就会很快使您感到困惑。
John V.

11
  1. 您可以“命名”您的布尔值。以下是OO语言的示例(可以在提供的类中表达UpdateRow()),但是概念本身可以以任何语言应用:

    class Table
    {
    public:
        static const bool WithExternalCall = true;
        static const bool WithoutExternalCall = false;
    

    并在呼叫站点中:

    UpdateRow(item, Table::WithExternalCall);
    
  2. 我相信项目1更好,但是在使用函数时,它不会强制用户使用新变量。如果类型安全性对您很重要,则可以创建一个enum类型并使其UpdateRow()接受而不是bool

    UpdateRow(var item, ExternalCallAvailability ec);

  3. 您可以通过某种方式修改函数名称,使其bool更好地反映参数的含义。不太确定,但也许:

    UpdateRowWithExternalCall(var item, bool externalCall)


8
如果现在使用#调用函数,现在就不喜欢#3了externalCall=false,它的名称绝对没有意义。剩下的答案是好的。
Lightness Races in Orbit

同意 我不太确定,但这对于其他功能可能是一个可行的选择。我
simurg

@LightnessRacesinOrbit确实。这样做比较安全UpdateRowWithUsuallyExternalButPossiblyInternalCall
埃里克·杜米尼尔

@EricDuminil:Spot上的亮点
轨道

10

我在这里还没有阅读的另一个选项是:使用现代IDE。

例如的IntelliJ IDEA打印的方法,如果你传递一个文字如你调用变量的变量名truenullusername + “@company.com。这是用小字体完成的,因此它不会在屏幕上占用太多空间,并且看起来与实际代码有很大不同。

我仍然不是说在任何地方都放入布尔值是一个好主意。说您读代码的频率比编写代码的频率高的论点通常非常有力,但是在这种情况下,它确实很大程度上取决于您(和您的同事!)用来支持工作的技术。与IDE相比,IDE的问题要少得多。

我的测试设置部分的修改示例: 在此处输入图片说明


5
仅当团队中的每个人都只使用IDE来读取您的代码时,这才是正确的。尽管非IDE编辑器很普遍,但是您也拥有代码检查工具,源代码控制工具,堆栈溢出问题等,即使在那些情况下,保持代码的可读性也至关重要。
丹尼尔·普赖顿

@DanielPryden当然可以,因此是我的“和您的同事”评论。在公司中使用标准IDE是很常见的。至于其他工具,我认为这些工具完全有可能在将来支持或将来支持这些理由,或者这些论点根本不适用(例如两人对编程团队)
Sebastiaan van den Broek

2
@塞巴斯蒂安:我不同意。即使是一个单独的开发人员,我也会定期在Git diff中阅读自己的代码。Git永远也不会做IDE可以做的相同类型的上下文感知可视化。我全力以赴使用一个好的IDE来帮助您编写代码,但是您绝不应该以IDE为借口来编写没有它就无法编写的代码。
丹尼尔·普里登

1
@DanielPryden我不主张编写极其草率的代码。这不是黑白情况。在某些情况下,在过去的特定情况下,您可能会赞成40%的人使用布尔值编写函数,而60%的人则不赞成这样做,而最终您却不这样做。也许有了良好的IDE支持,现在剩下的55%是这样写的,而不是45%。是的,有时您可能仍需要在IDE之外阅读它,因此它并不完美。但这仍然是替代方案的有效折衷方案,例如添加其他方法或枚举以澄清代码。
Sebastiaan van den Broek '18

6

2天,没有人提到多态吗?

target.UpdateRow(item);

当我想用项目更新行时,我不想考虑数据库名称,微服务的URL,用于通信的协议或是否使用外部调用。需要被用来实现它。不要再向我推这些细节了。只需拿走我的物品,就可以在某处更新行了。

这样做,它便成为施工问题的一部分。这可以通过多种方式解决。这是一个:

Target xyzTargetFactory(TargetBuilder targetBuilder) {
    return targetBuilder
        .connectionString("some connection string")
        .table("table_name")
        .external()
        .build()
    ; 
}

如果您正在查看并想到“但是我的语言已经命名了参数,那么我就不需要了”。很好,很好,去使用它们。只是把这些废话放在甚至不知道在说什么的主叫客户端之外。

应该注意的是,这不仅仅是语义问题。这也是一个设计问题。传递布尔值,您将不得不使用分支来处理它。不是很面向对象。有两个或多个布尔值可以传递吗?希望您的语言有多次派遣吗?查看将嵌套的集合可以做什么。正确的数据结构可以使您的生活更加轻松。


2

如果可以修改updateRow功能,也许可以将其重构为两个功能。鉴于该函数带有布尔参数,我怀疑它看起来像这样:

function updateRow(var item, bool externalCall) {
  Database.update(item);

  if (externalCall) {
    Service.call();
  }
}

这可能有点代码味道。根据externalCall变量的设置,函数可能具有截然不同的行为,在这种情况下,它具有两种不同的职责。将其重构为只有一个职责的两个功能可以提高可读性:

function updateRowAndService(var item) {
  updateRow(item);
  Service.call();
}

function updateRow(var item) {
  Database.update(item);
}

现在,在任何调用这些函数的地方,您都可以一目了然地判断是否正在调用外部服务。

当然,情况并非总是如此。这是情境和品味的问题。重构将布尔参数转换为两个函数的函数通常值得考虑,但并非总是最佳选择。


0

如果UpdateRow在受控代码库中,则应考虑策略模式:

public delegate void Request(string query);    
public void UpdateRow(Item item, Request request);

其中,Request代表某种DAO(平凡情况下的回调)。

真实情况:

UpdateRow(item, query =>  queryDatabase(query) ); // perform remote call

错误的情况:

UpdateRow(item, query => readLocalCache(query) ); // skip remote call

通常,端点实现将在更高的抽象级别上进行配置,我将在此处使用它。作为有用的免费副作用,这使我可以选择实现另一种访问远程数据的方式,而无需更改代码:

UpdateRow(item, query => {
  var data = readLocalCache(query);
  if (data == null) {
    data = queryDatabase(query);
  }
  return data;
} );

通常,这种控制反转可确保数据存储和模型之间的耦合程度较低。

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.