在条件语句中使用改变条件顺序的函数,是否是一种不好的编程实践?

12
var a = 1;

function myFunction() {
    ++a;
    return true;
}

// Alert pops up.
if (myFunction() && a === 2) {
    alert("Hello, world!");
}

// Alert does not pop up.
if (a === 3 && myFunction()) {
    alert("Hello, universe!");
}

https://jsfiddle.net/3oda22e4/6/

myFunction会增加一个变量并返回某些东西。如果我在包含它递增的变量的if语句中使用这样的函数,则条件将取决于顺序。

这样做是好还是坏的实践,为什么?


5
为了可读性,我会尽量避免这样的副作用 - 但如果您能提供更详细的用例,可能会有所帮助。 - msrd0
4
这段代码几乎没有实际用途。它在很多方面都非常糟糕,难以描述。就好像你从未听说过(1)抽象化、(2)继承、(3)多态一样。调用一个函数来完成任务,每当需要时都可以调用它,该函数应返回一个外部一致的答案。 - Jon Goodwin
5
我不认为这与继承或多态有任何关系。 - Bergi
1
这不是一个坏习惯。考虑一下函数time()。我还有几个真实世界的例子如下。 - danny117
1
刚想到这个。考虑数据库上的moveNext()函数。它可以移动某些东西。这不是一个坏习惯,因为在读取行的过程中,可能会添加行,使得getRow(n)函数变得过时。 - danny117
6个回答

14

条件的顺序是否有变化,取决于您是否更改了条件中使用的变量。您用作示例的两个if语句是不同的,无论您是否使用myFunction(),它们都将是不同的。它们等价于:

if (myFunction()) {
   if (a === 2) {
     alert("Hello, world!")
   }
}

// Alert does not pop up.
if (a === 3) {
   if (myFunction()) {
     alert("Hello, universe!")
   }
}

我认为你代码中的不良实践不在于你在条件语句中更改条件操作数的值,而在于你的应用程序状态被暴露和操纵在一个函数内部,而该函数甚至不接受此状态变化变量作为参数。我们通常尝试将函数与其范围外的代码隔离,并使用它们的返回值来影响代码的其余部分。全局变量90%的时间都是一个坏主意,随着代码库的逐渐增大,它们往往会产生难以追踪、调试和解决的问题。


2
此外,条件的顺序依赖有时对优化很有用。我们把它称为短路。建议阅读 这个问题 的答案,以快速了解短路求值的概念。 - Brishna Batool

5

这是不好的编程习惯,原因如下:

  • 与良好构建的代码相比,该代码可读性较差。如果第三方稍后检查该代码,则这一点非常重要。

  • 如果稍后更改myfunction,则代码流程将变得完全不可预测,并可能需要进行重大文档更新。

  • 小而简单的更改可能会对代码的执行产生极大的影响。

  • 看起来很业余。


1
这个回答非常模糊。如果您提供具体的改进建议,那么您的答案会更好。 - TinkerTenorSoftwareGuy

5

MyFunction 违反了一项原则,称为“Tell, Don't Ask”(说出来,不要问)。

MyFunction 改变了某些东西的状态,因此它成为了一个命令。如果 MyFunction 成功或无法增加 a,则不应返回 true 或 false。它被赋予了一项任务,必须尝试成功,如果发现该任务目前不可能完成,则应抛出异常。

在 if 语句的谓词中,MyFunction 被用作查询。

一般而言,查询不应呈现副作用(即不更改可以观察到的事物)。好的查询就像计算一样,对于相同的输入,它应该产生相同的输出(有时被描述为“幂等性”)。

还要知道这些都是指导方针,可以帮助您和其他人推理代码。会引起混乱的代码终究会出错。

有一些好的模式,比如 Trier-Doer 模式,可以像您的代码示例那样使用,但每个阅读它的人都必须通过名称和结构理解发生了什么。


1
你将会在使用moveNext()数据库函数时遇到很大的困难。它会操纵内部指针,并且每次调用时返回不同的结果。 - danny117

5
如果你需要问,那这显然不是一个好的实践。是的,正如你所提到的那样,改变逻辑操作数的顺序不应该影响结果,因此应该尽量避免在条件语句中使用副作用。特别是当它们隐藏在函数中时。
无论函数是纯函数(仅读取状态并进行一些逻辑处理)还是它会改变状态,都应该从函数名称中明确。你有几个选项来修复这段代码:
  • put the function call before the if:

    function tryChangeA() {
        a++;
        return true;
    }
    
    var ok = tryChangeA();
    if (ok && a == 2) … // alternatively: if (a == 2 && ok)
    
  • make the mutation explicit inside the if:

    function testSomething(val) {
        return true;
    }
    
    if (testSomething(++a) && a == 2) …
    
  • put the logic inside the called function:

    function changeAndTest() {
        a++;
        return a == 2;
    }
    
    if (changeAndTest()) …
    

1
我认为实际上只有你的第一个建议是好的。(在if之前放置函数调用) 另外两个仍然将条件测试与状态更改混合在一起。 谓词绝不能改变状态。 - Tiago Coelho
2
@TiagoCoelho 我同意第一个是最好的,但我不会绝对地说“永远不要”。当然,changeAndTest不再是一个纯谓词,但有些模式(特别是循环)需要在条件中执行类似于regex.match(string)i--的操作,而我并没有看到使用临时变量的更简洁的变体相比之下有多大的优势。 - Bergi

3
代码实际上存在多个不良实践:
var a = 1;
function myFunction() {
    ++a; // 1
    return true;
}

if (myFunction() && a === 2) { // 2, 3, 4
    alert("Hello, world!")
}

if (a === 3 && myFunction()) { // 2, 3, 4
    alert("Hello, universe!")
}
  1. 在不同的作用域中改变变量的值。这可能会有问题,但通常是有问题的。

  2. if语句条件中调用函数。 本身并不会引起问题,但并不是很清晰。更好的做法是将该函数的结果赋值给一个变量,可能使用一个描述性的名称。这将帮助读代码的人理解您想要在if语句中检查什么。顺便说一下,该函数总是返回true

  3. 使用了一些魔术数字。 想象一下其他人阅读该代码,而它是大型代码库的一部分。这些数字代表什么意思?更好的解决方案是用良好命名的常量替换它们。

  4. 如果您想支持更多消息,则需要添加更多条件。 更好的方法是使其可配置。

我会将代码重写如下:

const ALERT_CONDITIONS = { // 4
  WORLD_MENACE: 2,
  UNIVERSE_MENACE: 3,
};

const alertsList = [
  {
    message: 'Hello world',
    condition: ALERT_CONDITIONS.WORLD_MENACE,
  },
  {
    message: 'Hello universe',
    condition: ALERT_CONDITIONS.UNIVERSE_MENACE,
  },
];


class AlertManager {
  constructor(config, defaultMessage) {
    this.counter = 0; // 1
    this.config = config; // 2
    this.defaultMessage = defaultMessage;
  }

  incrementCounter() {
    this.counter++;
  }

  showAlert() {
    this.incrementCounter();
    let customMessageBroadcasted = false;
    this.config.forEach(entry => { //2
      if (entry.condition === this.counter) {
        console.log(entry.message);
        customMessageBroadcasted = true; // 3
      }
    });

    if (!customMessageBroadcasted) {
      console.log(this.defaultMessage)
    }
  }
}

const alertManager = new AlertManager(alertsList, 'Nothing to alert');
alertManager.showAlert();
alertManager.showAlert();
alertManager.showAlert();
alertManager.showAlert();
  1. 使用具有精确功能的类,该类使用自己的内部状态,而不是一堆依赖于任何可能位于任何位置的变量的函数。是否使用类取决于您的选择,也可以以不同的方式完成。

  2. 使用配置文件。这意味着如果您想要添加更多消息,则无需修改代码。例如,假设配置来自数据库。

  3. 正如您可能注意到的那样,这会改变函数外部作用域中的变量,但在这种情况下不会造成任何问题。

  4. 使用具有清晰名称的常量。(好吧,它可能更好,但请容忍我给出的示例)。


1
我认为,在条件语句中使用函数调用作为谓词没有任何问题,只要该函数确实是纯函数。仅仅引入一个本地符号来存储函数结果似乎更糟糕。 - Pointy
1
在我看来,在条件语句中使用函数调用是令人困惑的。我更喜欢将结果分配给一个具有描述性名称的变量。例如:const hasFuel = driveCar(1000); if (!hasFuel) { refill(); } - Giacomo Cosimato
1
这个问题不需要OOP来解决。在使用OOP之前,应该先看问题的核心。原始问题可以通过函数式编程来解决。添加架构是一个单独的问题。 - TinkerTenorSoftwareGuy
1
使用类还是不使用,这是一个选择的问题。可以用不同的方式来完成。 - Giacomo Cosimato

1

一个改变事物的函数。这个世界要变成什么样子?这个函数必须改变事物并在每次调用时返回不同的值。

考虑一副扑克牌的dealCard函数。它发牌1-52。每次调用它时,它应该返回一个不同的值。

function dealCard() {
    ++a;
    return cards(a);
}

/* 我们假设数组cards已经洗牌 */

/* 为了简洁起见,我们假设这副牌是无限的,并且不会在52张牌后重新开始 */


1
我的意思是我不想因为接了个电话就要重新布置厨房。但是,如果我和清洁服务有安排,并且他们打电话说他们会在15分钟内到达,那就是另一回事了。如果在这种情况下,a“属于”dealCards,并且每个人都同意对该方法的调用更改a,那么就没有问题。Ruby数组shuffle只会给你一个全新的已经被洗牌过的数组,所以甚至不会影响你的东西。 - Greg
1
Greg。更多的函数是有序依赖的。getNationalDebt("USA"); time(); IKEA("Kitchens"); - danny117
1
所有这些都应该是可预测、可靠的程序输入,我可以对其进行推理和可靠地测试。 - Greg
1
是的。对于依赖调用顺序的函数,简单的单元测试是检查在后续调用中函数是否返回不同的结果。更高级的测试方法是针对依赖调用顺序的函数进行测试,以确保结果包含在该函数的已知结果中(测试卡实际上存在于牌组中)。测试usDebtclock生成函数需要两次调用,其中第一次调用的结果小于第二次调用的结果... - danny117

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接