请不要写令人困惑的条件
你是否曾盯着同一行代码超过几秒钟?你是否经常发现一些难以解析的条件语句?在本文中,我们将探讨代码中令人困惑的条件语句是如何体现的。此外,我们还将探索不同的重构技巧,以提升条件控制流的可读性。
双重否定
当我们否定一个变量的名称本身就带有非肯定语气时,就会出现双重否定。请考虑以下示例。
// Non-affirmative Naming Convention
const isNotReady = doSomething();
const isForbidden = doSomething();
const cannotJoinRoom = doSomething();
const hasNoPermission = doSomething();
// Negating once results in an awkward double negative situation.
console.log(!isNotReady);
console.log(!isForbidden);
console.log(!cannotJoinRoom);
console.log(!hasNoPermission);
这与肯定式风格相反,肯定式风格需要的认知负荷较小,因为解析时无需费力否定变量名称来提取其逻辑的本质。换句话说,肯定式变量背后的意图可以直接解读——无需任何繁琐步骤!请考虑对上例进行以下修改。
// Affirmative Naming Convention
const isReady = doSomething();
const isAllowed = doSomething();
const canJoinRoom = doSomething();
const hasPermission = doSomething();
// Negation actually makes sense here!
console.log(!isReady);
console.log(!isAllowed);
console.log(!canJoinRoom);
console.log(!hasPermission);
不幸的是,由于标准和向后兼容性,有时非肯定性的命名约定是不可避免的。以HTML DOM APIHTMLInputElement#disabled
的属性为例。标签中存在该属性会告诉浏览器(在视觉和字面上)禁用元素的表单控件。否则,该属性的缺失会导致其显示其默认行为,即接受用户输入。这是 HTML 人机工程学的一个不幸的副作用。disabled
<input>
<input>
<!-- Normal Checkbox (Default) -->
<input type="checkbox" />
<!-- Disabled Checkbox -->
<input type="checkbox" disabled />
尽管如此,我们仍然应该尽可能地追求肯定的命名约定。它们更容易阅读和解析,因为不需要总是在心里否定变量名。这条规则适用于变量名和函数名。
非肯定性控制流
双重否定的下一种形式稍微微妙一些。
// Suppose that we _do_ follow affirmative naming conventions.
const isAllowed = checkSomething();
if (!isAllowed) {
// There is something funny about this...
doError();
} else {
// Notice how the `else` block is practically a double negation?
doSuccess();
}
如上所示,非肯定风格也适用于条件控制流。回想一下,else
代码块实际上是对相应条件的否定if
。因此,我们必须在这里扩展肯定风格。解决方法其实相当简单。
// Just invert the logic!
const isAllowed = checkSomething();
if (isAllowed) {
doSuccess();
} else {
doError();
}
同样的规则也适用于平等和不平等的检查。
// ❌ Don't do this!
if (value !== 0) {
doError();
} else {
doSuccess();
}
// ✅ Prefer this instead.
if (value === 0) {
doSuccess();
} else {
doError();
}
有些人甚至会为了用肯定句式否定某个条件而让条件块留空。虽然我并不提倡每个人都这么做,但我明白为什么这样对某些人来说可能更具可读性。instanceof
例如,运算符如果没有括号就很难被否定。
if (obj instanceof Animal) {
// Intentionally left blank.
} else {
// Do actual work here (in the negation).
doSomething();
}
提前退货的例外情况
顺便提一下,对于提前返回的条件控制流,存在一些特殊例外。在这种情况下,可能需要进行否定。
if (!isAllowed) {
// Return early here.
doError();
return;
}
// Otherwise, proceed with the success branch.
doSuccess();
但是,只要有可能,我们仍然应该尝试反转逻辑,如果它可以减少嵌套、减少缩进级别并提高肯定样式的可读性。
// Prefer affirmative early returns.
if (isAllowed) {
doSuccess();
return;
}
// If we did not invert the logic, this would have been
// nested inside the `!isAllowed` conditional block.
if (!hasPermission) {
doPermissionError();
return;
}
// When all else fails, do something else.
doSomethingElse();
return;
以肯定的方式(没有提前返回)表达相同控制流的另一种方法如下。
// Hooray for the affirmative style!
if (isAllowed) {
doSuccess();
} else if (hasPermission) {
doSomethingElse();
} else {
doPermissionError();
}
return;
当然,还有很多其他方法可以交换、反转和重构代码——每种方法的优缺点完全是主观的。因此,保留肯定性惯例就成了某种艺术形式。无论如何,只要我们坚持肯定性风格的一般准则,代码的可读性就会不断提高。
复合条件
AND
有了诸如and 之类的逻辑运算符,情况就变得稍微复杂一些了OR
。例如,我们如何以更肯定的风格重构下面的代码?
// This is fine... but there has to be a better way,
// right? There are just too many negations here!
if (!isUser || !isGuest) {
doSomething();
} else {
doAnotherThing();
}
对于复合条件,我们引入布尔代数中最被低估的定律:德摩根定律!
// Suppose that these are **any** two Boolean variables.
let a: boolean;
let b: boolean;
// The following assertions will **always** hold for any
// possible pairing of values for `a` and `b`.
!(a && b) === !a || !b;
!(a || b) === !a && !b;
感谢德摩根定律,我们现在有办法在条件中“分配”否定,然后“翻转”其运算符(从&&
到||
,反之亦然)。
虽然以下示例仅涉及二元比较(即两个元素),但只要我们遵循运算符优先级,德·摩根定律就适用于任意数量的条件变量。也就是说,
&&
运算符总是先于||
运算符求值。
// By De Morgan's Laws, we can "factor out" the negation as follows.
if (!(isUser && isGuest)) {
doSomething();
} else {
doAnotherThing();
}
// Then we simply invert the logic as we did in the previous section.
if (isUser && isGuest) {
doAnotherThing();
} else {
doSomething();
}
现在,是不是可读性更强了?利用德摩根定律,我们可以清理掉那些“否定太多”的条件句。
结论
至此,整体主题应该已经清晰了。我们应该尽可能避免编写那些迫使读者费力费力(不必要地)的代码。在本文中,我们讨论了以下技巧:
- 鼓励肯定的命名约定。
- 避免使用否定词/前缀,
no
如not
、、、等dis-
。mal-
- 更喜欢积极的等价物。
- 避免使用否定词/前缀,
- 反转条件控制流(如果可能)以适应肯定风格。
- 交换、反转和重构分支时可以随意尝试。
- 提前回报可能需要否定。
- 使用布尔代数的一些技巧来反转条件。
- 德摩根定律对于重构来说是特别强大的工具!
现在就行动起来,用更清洁的条件来祝福世界吧!
文章来源:https://dev.to/somedood/please-dont-write-confusing-conditionals-2n32