...
Back

把按钮藏起来,不等于把门关上

我们通过移除入口来关掉一个功能。用户还是找到了它 —— 变成了一个点了没反应的按钮。关于「一个功能开关要覆盖什么才算真的关掉」。

把按钮藏起来,不等于把门关上

把按钮藏起来,不等于把门关上 🚪

我们需要在一个 app 里临时关掉一道门槛,等一个决定。改动只有一行:不再渲染那个入口。发版,继续下一件事。

用户拿到的结果比两种状态都糟。这个功能仍然能经由不走那个隐藏入口的路径抵达,而每一条这样的路径都撞上了一个仍然生效的强制检查。可见的结果是:一个点了没反应的按钮。没有报错、没有解释、没有出路。"关闭"变成了"坏了",而"坏了"是两个选项本来都不该产生的那个结果。


一个开关有三个面,而它们通常是不同的代码

呈现 —— 入口是否可见?这是所有人都会改的那个,因为它是你能看见的那个。

强制 —— 那个检查还在跑吗?一道 UI 被藏起来但仍在求值的门槛,产生的恰好就是我们那个死按钮。从其他路线抵达的用户 —— 深链接、保存的状态、从另一个界面过来的路径 —— 会撞上一堵没有解释的墙,因为解释本来在你移除掉的那个 UI 里。

状态 —— 已经在门那一侧的人会怎样?这是产生工单的那个。如果一道门槛被关掉,已经通过它的人还保有权限吗?他们还能退出登录吗?还能注销账号吗?关掉一个入口,绝不能同时关掉一个出口。

一个只覆盖呈现层的开关不是开关,而是一个附带强制逻辑 bug 的外观改动。


本可以救我们的那条规则

关掉一个功能,意味着通往它的每一条路径都产生一个自洽的结果 —— 而不是某一条路径被藏起来了。

自洽有三种可接受的形状,而选哪一种是一个应该被明确做出的产品决策:

  1. 透明 —— 功能能用,只是不宣传。强制关闭,入口隐藏。适合软启动。
  2. 有解释 —— 功能是真的关了,并且说出来。强制开启,配一条真实的信息:不可用、为什么、该怎么办。对面向用户的门槛,这几乎总是正确答案。
  3. 不存在 —— 根本没有路径能抵达。这要求你真的去审计那些路径,而这正是被跳过的那部分工作。

永远不可接受的是我们发出去的第四种形状:入口隐藏、强制生效、没有信息。


开关会堆积,而这才是真正的成本

每一个开关都会把它所守护的代码的状态空间翻倍。两个互相作用的开关是四种配置,其中你测了两种。没测的那些组合正是怪 bug 的住处,而它会由一个账号恰好落在其中的用户报告上来。

更糟的是,开关活得比它的理由长。一个为某次上线加的开关,一年后还在,它的默认值如今是承重的,而没人记得另一个分支还能不能跑。到那时它就不是开关了 —— 它是一段有着看似合理名字的死代码。

对我们管用的做法:

  • 在加开关的同一个提交里写下退出条件。 一行字:商店审核通过后移除供应商配额提上去后移除。一个没有退出条件的开关就是永久的,就该按永久的来评审。
  • 默认值设成你打算保留的那个状态。 开关读起来应该是"当前答案外加一个逃生舱",而不是一枚立在边上的硬币。
  • 两个分支都测,或者删掉一个。 如果半年没人走过关闭那条路,它就是不能用的。你拥有的是一个多绕了几步的常量,外加一种可回滚的错觉。
  • 优先删除,而不是加开关。 开关是用来处理真正悬而未决的决定的正确工具。它不是用来处理"舍不得删的代码"的工具 —— 那是版本控制的活。

可以推广的那部分

那个死按钮不是 UI bug。它是两层之间的不一致,而这两层各自单独看都做了合理的事:视图不再提供这个功能,强制层继续拒绝它。单独看谁都没错。

任何时候一个行为由不止一个地方控制,失败模式都不是"某个地方错了",而是"这些地方不一致" —— 而这种不一致渲染出来的结果是什么都没有。所以该做的检验从来不是"我藏起来了吗",而是:一个从别处抵达的用户,看到的是什么?