全文共2222字,预计学习时长9分钟
在代码库中使用布尔标志值来管理状态机似乎听起来是个不错的办法,但事实并非如此。布尔值恐怕是很多程序员接触到的第一种数据类型,它非常简单,只有两种状态: true 和false.
随着代码的发展,它很容易产生代码复杂性、可读性和可伸缩性等方面的问题。
通常,标记参数会划分函数的逻辑,迫使该函数根据值执行多项操作,这可能会导致业务逻辑中的混乱运行,代码库很容易会以下列树状结构结束运行:
背景故事
下面这个故事把布尔参数在状态机和函数参数中的弱点体现得淋漓尽致。
一群软件开发人员曾经建立了一个管理用户状态的模块,其中一名开发者坚持使用布尔值,因为该模块要求只有两个状态:ONLINE 和 OFFLINE,看起来又快又简单,直截了当。尽管大多数人并不完全同意该建议,但还是继续做了下去。
最终,类似于下面这样的函数开始占据代码库:
func setUserState(isUserOnline :Bool)
不久后,团队里来了一位新成员,他想知道下面语句的真正含义:
setUserState(true) //The new guyjust kept staring at this.
虽然有人提出了一个看起来更好的函数名(setUserOnline),但当一旦出现新的业务需求(包括另一个用户状态:BLOCKED),事情就变成了一场噩梦。来看看这些开发人员都采取了哪些办法来解决这个问题。
三态布尔问题
布尔值通常表示两种状态,但在某些语言中(如Java使用的是Boolean对象),可以用null来分配第三种状态。在上下文中,BLOCKED 会被设置为null。
虽然这似乎不需要额外布尔值就可以适应新的用户状态,但会很容易得到NullPointerExceptions结果。
另外,在不同情况下,要想区分false和null会比较棘手。比如,布尔属性game.isPlaying为true时则清楚地表明游戏处于播放模式。但为false或null时,false表示游戏暂停或停止。
如你所见,false没有提供足够信息来轻松识别和回忆其绑定状态,三态布尔值只会使逻辑复杂化。
此外,当系统要求包含另一个称为EXPIRED的状态,会出现什么情况?由于现在有四个状态,这种方法无法解决了。接着来看看大多数开发人员采用的另一种方法。
多个布尔值带来隐藏的依赖项
开发人员最终扩展了前一个函数的签名,为新状态添加了两个布尔参数:
func setUserState(
isUserOnline : Bool,
isUserBlocked : Bool,
isUserExpired : Bool)
看起来像是满足业务需求的简单扩展,却被迫在代码库中引入了隐藏依赖项和大量新组合。
创建的两个隐藏依赖项是isUserOnline — isUserExpired和 isUserOnline — isUserBlocked。现在需要明确管理额外状态,以避免冲突状态。例如,被拦截/过期的用户不能在线。以下是要处理的两种冲突状态的示例:
#Condition 1: isUserOnline:false and isUserExpired: true
#Condition 2: isUserOnline: false and isUserBlocked: true
添加状态越多,函数很容易就能变成一长串参数。事情变得不可持续,因为最终会遇到很多&&,||,和其他复杂的分支逻辑来处理互斥和相关的布尔值。
布尔值具有类型安全性和可读性问题
使用多个布尔值,很有可能将它们混淆,最终还可能会传递错误值(可能来自其他对象),且编译器甚至不会作出反应。在重构和执行代码审查时,这可能是一场噩梦,需要编写大量的单元测试来解决此类问题。
此外,很容易忘记布尔变量的false或true值的真正含义,理解充满布尔值的函数调用(如下所示)只会变得非常困难:
setUserState(true, false, false)
有人可能会说,现在很多编程语言都支持可以提高函数可读性的命名参数。但话又说回来,可能会意外传递一个反向或不正确的布尔值,但函数签名仍然匹配。
如果用枚举而不是布尔值,这个故事中的软件开发人员就能避免这些麻烦。
选用枚举,避免用布尔值
枚举数是一种数据类型,由一组能够以类型安全方式使用的命名值组成。虽然它看起来可能不像布尔值那么简单,但用枚举或其他用户定义类型有助于避免设置具有多个分支的复杂if语句。
enum UserStates{case active
case inactive
case blocked
case expired}
1. 枚举清晰明了
枚举强制命名所有状态,这让人很容易理解它们的含义——从而创建自文档化代码。同样,枚举清楚地表明这些值互相排斥,从而消除冲突状态的疑问。
将枚举作为函数中的参数传递更加清晰,有助于避免神秘的布尔值。只需比较下面两行:
setUserState(true,false, false)//The version below is more concise andclearer.setUserState(UserStates.active)
2. 枚举具有类型安全性
对于枚举,不能给它分配除指定值以外的任何值,因为其具有类型安全性,这样就不可能出现意外交换值或传递无效状态,因为能够被编译器发现。
并非所有语言都支持本机枚举,在此种情况下,可以创建自定义类型。例如,在JavaScript中,可以通过“冻结”对象中的常量来解决此问题:
constUserState= {ACTIVE: 1,INACTIVE: 2,BLOCKED: 3,EXPIRED: 4};Object.freeze(UserState);
3.枚举使扩展和重构更加容易
在枚举数中扩展值的集合更容易,因为与布尔值不同,可能状态组合的数量不会在每种新情况下加倍。
而且,许多编译器很智能,足以指示需要进行哪些更改才能适应新枚举。比如,Swift会引发错误,同时,使用其他语言,可以轻松查明枚举中出现的所有案例。
用额外的新案例扩展已经存在的枚举不费吹灰之力,因为数据类型保持不变,这使得重构整体变得更加容易。
当然,布尔值也不是一无是处。如果确定状态是二进制,且互斥,或方法名称已经描述了状态(例如setEnabled(true)),那么就放心大胆地用布尔值吧。
但通常情况下,需求会发生变化,也需要添加新状态。因此包含两个元素的枚举值得一试,它比布尔标志更安全。枚举有助于将来验证代码,且无需跟踪布尔字段。
别贪图省事儿滥用布尔值,枚举是一个不错的选择。
留言点赞关注
我们一起分享AI学习与发展的干货
编译组:齐鑫濛、刘露敏
相关链接:https://medium.com/better-programming/dont-use-boolean-arguments-use-enums-c7cd7ab1876a
如转载,请私信小芯,遵守转载规范