I3.13.3booleans vs enum state设计

非法状态的出现往往源于用多个独立布尔值而非互斥枚举描述状态

别名: 独立布尔 · 互斥枚举 · flag soup · isLoading

概念解释

isLoadingisErrorisSuccessisEmpty 各是一个可独立翻转的开关。四个开关给出十六种组合,合法的往往只有四五种,剩下的全是非法。互斥枚举把同一事实收成一个值:idle | loading | success | error | empty,组合空间等于节点数。非法状态之所以「往往」出现,不是因为程序员喜欢脏数据,是因为独立布尔是默认的、顺手的建模,空间在声明时就已经超集。

这条点名这种建模习惯。空间一旦超集,非法会出现——那是结构后果,这里要改的是习惯本身。

机制

布尔的诱惑是局部:这一拍要表示「在加载」,就加一个 isLoading。下一拍要表示失败,再加 isError。每个标志在自己的故事里为真,故事与故事之间的互斥没有类型来管。赋值是分别发生的:加载开始时把 loading 置真,成功回调把 success 置真却忘了把 loading 置假,于是 loading ∧ success 诞生。并发下更糟:失败与成功的回调对两个标志各写一次,到达顺序决定哪种非法。

枚举把互斥写进赋值:写成 success 的那一拍,不可能同时还是 loading。这不是风格。它把十六格收成五格,让忘了清标志在类型层无法表示。从布尔迁到枚举时,那些曾经靠「先看哪一个标志」来猜的界面分支会被迫变成穷尽匹配——这正是把隐藏的非法暴露出来的时机。

边界

真正独立的事实应用布尔:通知开关与深色模式互不排斥。把它们收成枚举是另一种错。正交的两台小机器(「同步状态」和「编辑状态」)应是两个枚举,而不是一个十二值的大枚举,也不是六七个布尔。三态(开 / 关 / 不确定)不是两个布尔(checked 与 indeterminate)——那又是经典的非法源,应用一个三值。历史 API 只给布尔时,适配层仍应在内部收成枚举,不要把布尔汤传进界面。有人用位掩码当「省空间的布尔簇」,掩码与独立布尔是同一超集,只是更难读。

怎么落地

  • 凡是描述同一对象「现在处于哪一拍」的标志,收成一个枚举。禁止 isLoadingisError 并列作为真相来源。
  • 界面分支对枚举做穷尽匹配,不写一串 if (isLoading) else if (isError) 的滑梯——滑梯会留下未覆盖的组合。
  • 发现两个布尔「理论上不同时为真」,立刻合并,不要加第三条布尔来「再解释一下」。
  • 验证:在加载成功的回调里故意不把 isLoading 置假(或在枚举版里尝试同时赋两个成员)。布尔版应能渲染出「成功了还在转圈」;枚举版应无法通过编译或在赋值处崩溃。再数产品里所有 isXxx 标志,两两求「可否同时为真」:答「不应,但类型允许」的每一对都是待收的枚举。

延伸

  • 同组I3.13.1 状态机需要为所有可能的转移路径定义结果,不留未定义转移 · I3.13.2 数据结构若允许表达不属于任何合法状态的组合,就会产生非法状态 · I3.13.4 界面呈现的状态应与底层状态机严格对应,不能凭视觉猜测当前状态
  • 相邻I3.01 系统状态可见 · I2.08 加载失败
  • 站内检索boolean flags · discriminated union · isLoading isError

同组卡片

快捷操作

分享

分享当前页面

ios_share

https://hci.top/zh/handbook/I3.13.3