不爱布尔参数
单一责任原则
它们令人困惑
不可能的状态
抽象概念错误
啊,布尔值。0 或 1,真或假。永远是其中之一,绝无中间值。如此简单,如此可预测。归根结底,我们写的所有代码最终都会变成大量的 0 和 1。
布尔值本身并没有错。我们每天都会用它们来设置条件:
// ✅ boolean condition
if (user.age() < legalAge) {
return 'Sorry, you are too young to use this service'
}
但是,出于各种原因,将它们用作函数的参数可能表明设计不佳:
单一责任原则
一个函数应该只做一件事,而且只能做一件事。给函数传递一个“标志”通常意味着该函数同时执行两件事,从而违反了这一原则。例如:
// 🚨 booleans as a function parameter
function createReport(user: User, sendEmail: boolean) {
// create the report here
const reportData = ...
if (sendEmail) {
sendReport(user.email, reportData)
}
return reportData
}
有些情况下,我们希望在创建报告后立即通过电子邮件发送,而有些情况下则不需要。但为什么要把这个功能集成到createReport函数中呢?该函数应该只负责创建报告,其他什么都不用做。调用者可以自行决定如何处理这份报告。
它们令人困惑
标志位可能会令人困惑,尤其是在没有命名参数的语言中。例如,Kotlin 标准库中equals 方法的签名如下:
fun String?.equals(other: String?, ignoreCase: Boolean): Boolean
// Returns true if this string is equal to other,
// optionally ignoring character case.
与第一个例子不同,这个函数不是同时执行两件事,而是以两种不同的方式执行同一件事——这是一个重要的区别。当你需要阅读类似这样的调用端代码时,这可能会非常令人困惑:
"foo".equals("bar", true)
"foo".equals("bar", false)
我们如何才能知道true在这种情况下它的含义?更糟糕的是,它到底是什么false意思?它是否会否定 equals 比较的结果?Scala用两种方法解决了这个问题:equals和equalsIgnoreCase。每种方法都只做一件事——无需猜测。
更多猜测
在你在这里查找答案之前——你认为 GroovyList.sort方法中的这个布尔标志是什么意思?
["hello","hi","hey"].sort(false) { it.length() }
如果这一点对每个人来说还不明显的话:
mutate- false 将始终创建一个新列表,true 将直接修改现有列表。
API 逻辑清晰,非常直观,一点也不难理解🤷♂️
不可能的状态
布尔值很容易导致出现不可能的状态。假设你有一个指标,想要对其进行格式化。它可能是一个“普通”数字,但也可能是百分比值。于是你决定这样建模格式化函数:
function formatMetric(value: number, isPercent: boolean): string {
if (isPercent) {
return `${value * 100}%`
}
return String(metric)
}
这是一个相当基础的数字格式化函数,但除此之外,它看起来还不错。坦白说,你添加到函数中的第一个“标志”通常看起来都很普通。
第二面旗帜
需求会随着时间推移而变化(这是常态),现在我们还需要支持某些指标的货币单位。基于上述格式化函数,我们倾向于添加另一个标志,isCurrency
function formatMetric(value: number, isPercent: boolean, isCurrency: boolean): string {
if (isPercent) {
return `${value * 100}%`
}
if (isCurrency) {
return // imagine some currency formatting is returned here
}
return String(metric)
}
我们的代码运行正常,我们编写了测试,如果有货币指标,我们会添加货币标志,一切都很顺利。
但事实并非如此。
添加一个布尔值并不会增加一个状态——状态的数量呈指数级增长。两个布尔值意味着四个状态,三个布尔值意味着八个可能的状态,依此类推。如果我们调用上面的函数,会发生什么?
formatMetric(100, true, true)
答案是:你无法知道。先检查哪个标志是实现细节。而且,这本身就是一种不可能的状态:一个指标不可能同时是百分比和货币单位。这种不可能的状态经常出现在布尔参数中。我最近遇到一个函数,它有 8 个布尔值作为输入——结果发现,它只有 3 个实际状态,其余的都是它们的变体。
克制住这种冲动
为了避免出现不可能的状态,请克制住添加第一个布尔参数的冲动。对人类来说,扩展现有模式远比识别反模式并重构它们容易得多。如果存在一个布尔值,那么必然会出现第二个。如果我们从可能的状态枚举开始,那么很可能最终会被扩展:
function formatMetric(value: number, variant?: 'percent'): string {
if (variant === 'percent') {
return `${value * 100}%`
}
return String(metric)
}
现在我们可以扩展这个变体'percent' | 'currency',并且只需要处理三种状态而不是四种。当然,你也可以显式地包含默认(标准)变体,而不是使用undefined。
更多优势
单一变体属性的其他优势包括:
-
更好的类型安全性:
我们已经讨论过可读性,但标志位也很容易混淆,而且由于它们的类型相同(布尔值),编译器不会发出警告。你可以使用单个选项对象来解决这个问题,这在 JavaScript 中非常流行。 -
我之前写过关于TypeScript 中穷举匹配的文章,它在这个例子中也非常有用。编译器会在我们添加新变体时告诉我们代码需要修改的地方。CDD,编译器驱动开发:
type MetricVariant = 'standard' | 'percent' | 'currency'
function formatMetric(value: number, variant: MetricVariant = 'standard'): string {
switch (variant) {
case 'percent':
return `${value * 100}%`
case 'currency':
return // imagine some currency formatting is returned here
case 'standard':
return String(metric)
}
}
我们在创建 React 组件时也会这样做,你见过同时具有isPrimary和isSecondary标志的按钮吗?当然没有——因为它们怎么可能同时是两者呢?
// 🚨 Don't do this
<Button isPrimary isSecondary />
// ✅ Do this
<Button variant="primary" />
抽象概念错误
很多时候,添加标志是因为我们发现与现有代码相似,我们不想重复自己,保持代码的DRY 原则。
- 这里有一个函数看起来几乎是我想要的,我只需要添加这个标志,因为它略有不同。
- 这个组件看起来也适用于我的情况,我只需要添加一个withPadding属性让它适配即可。
关于这个主题有很多优秀的文献资料,阐述了为什么我们不应该这样做,以及我们可以采取哪些替代做法:
我可以推荐所有这些方法,首先,请克制住向代码库中添加下一个布尔参数的冲动。
无论你是否喜欢布尔值,或者两者都喜欢,都请在下方留言⬇️
文章来源:https://dev.to/tkdodo/no-love-for-boolean-parameters-4il0