构建防御性软件
· Matt Senter

前不久,我的笔记本电脑电量耗尽关机了。插上电源重启之后,Comoji 不工作了。我输入一个表情符号快捷键,什么也没有发生。没有解释,没有任何迹象表明有什么变了。只有一个关机前还好好的、现在显然不再工作的应用。
Comoji 是我做的一个小型 Mac 应用,在你已经在用的文本框里提供表情符号自动补全。它的工作很简单:识别一个快捷键,给出匹配的表情符号,然后别碍事。当这一切停止发生时,这个产品就没剩下多少可以交互的东西了。
只不过 Comoji 其实并没有坏。它是在尊重一条安全边界,而我电脑上的另一个东西卡在了这条边界后面。
为什么做正确的事看起来会像什么都没做?
macOS 有一项叫作 Secure Input(安全输入)的功能,用来防止敏感的键盘输入被其他进程截获。这对 Comoji 这样的应用很重要,因为它需要识别你输入的内容才能提供自动补全。一个表情符号工具没有任何理由在你输入密码时旁观,所以在 Secure Input 处于活动状态时,Comoji 会主动退让。
这一次,Chrome 显示为持有 Secure Input 的进程,尽管我早已不在输入密码了。我强制退出了 Chrome,以为这样就能解决问题。结果 Secure Input 仍然处于活动状态,而且现在关联到了 Apple 的登录进程上。不管重启之后哪里出了岔子,关掉浏览器都不足以解决它。
这是 Apple 自己也记录在案的一类问题。一个应用在不再需要 Secure Input 时仍让它保持开启,可能会干扰其他依赖键盘事件的应用,即使肇事的应用已经退到了后台。安全机制继续尽职尽责,机器上其他地方的软件却要承担后果。
Comoji 当时就处在这样的境地。系统说键盘输入需要保护,它就正确地拒绝工作。但这一切对我来说都是不可见的。这个应用是我写的,可就连我一开始也只看到它不工作了。
用户不应该需要去调查操作系统的状态,才能弄明白为什么自己的表情符号快捷键突然什么都不做了。
为什么「不是我的 bug」不算一个完整的回答?
一旦确定问题源自你的代码之外,就停止追查,这种诱惑可以理解。Chrome 持有 Secure Input。然后 Apple 的登录进程也牵涉其中。Comoji 的行为完全符合设计。结案了,对吧?
并不是。这解释了是谁造成了问题,却帮不到那个正试图使用我的应用的人。他们不会去看进程归属,也不会去判断哪家公司该收到 bug 报告。他们看着 Comoji,纳闷它为什么不工作。
底层的输入问题在 Comoji 之外。而缺少解释这件事,是我可以修的。
防御性软件开发正是在这里超越了检查非法参数和捕获异常。你还得考虑,当周围的环境不再正常运转时,你的应用看起来是什么样子。用户能区分有意的暂停和崩溃吗?他们知道是什么在阻止应用工作吗?你有没有给他们一个有用的下一步去处?
当你的产品看起来坏了的时候,技术上正确并不能带来多少安慰。
我改了什么?用一把小锁取代一个谜团
我给 Comoji 的菜单栏图标加了一个小小的锁形指示,只要 Secure Input 处于活动状态就会显示。现在,自动补全缺席有了一个看得见的原因:Comoji 已暂停,因为键盘输入受到保护。这个应用不再默不作声地把一个不可用的功能摆在那里,好像它本该在工作一样。
我还把托盘菜单链接到了一篇解释什么是 Secure Input 的帮助文章,以及 Comoji 为什么要尊重它。卡住的人可以直接从应用里找到这个解释,而不必猜测正确的搜索词,或者联系我才发现原来系统里有这么一项功能。
重要的是我没有试图解决什么。答案不是为了让表情符号自动补全能用而绕过密码保护。Secure Input 处于活动状态本身并不是错误。它的存在有充分的理由,Apple 也明确警告过,开启安全键盘输入可能会影响其他应用,也就是那些需要这些按键的应用。
所以这把锁传达的是一种状态,而不是一场危机。在正常输入密码期间,它解释了为什么 Comoji 暂时不可用。当这种状态意外地持续下去时,它给了用户一个理解发生了什么的起点。
我没有让 Chrome 或 macOS 变得不会卡住。我只是让它们卡住时,对使用 Comoji 的人来说不那么令人困惑,同时没有削弱 Comoji 本应尊重的那层保护。
为什么大公司依然是外部依赖?
为了 Google 的浏览器和 Apple 的操作系统牵扯出的问题,去加固一个微不足道的小工具,这件事有点让人恼火。它们不是我不假思索拖进项目里的冷门组件。它们是两家巨型科技公司的产品,而它们的行为依然成功地让我的小应用显得有缺陷。
但它们的体量并不改变工程上的决定。我不能在「凡是比我大的公司做出来的东西都永远表现正确」这个假设之上构建 Comoji。我也不能对一个沮丧的用户说,这个问题属于一家足够气派的公司,然后就认为我的工作完成了。
我不知道这种特定的事件序列发生得有多频繁。它也许很少见。但我遇到了,而结果暴露出一个有用的区分:Comoji 已经安全地处理了这个安全条件;它还没有处理好由此产生的困惑。
这并不意味着每一个假想的故障都值得一个新设置、一个警告对话框或一个恢复子系统。在这个案例里,应对措施很小,而且直接针对问题本身。一个锁形图标让这个状态可见。一篇帮助文章让它可以被理解。两者都不需要假装我能从自己的应用内部修好另一个应用。
对我来说,这是构建防御性软件的一个重要部分。你的责任不止于确保周围一切正常时你的代码能正常工作。你还需要决定,当它无法完成自己的工作时会发生什么,包括原因出在别人的错误上的时候。
有时候,正确的行为就是停下来。好的软件应该能告诉你为什么。