数字与数据
每个数据点都有三种状态:我拿到了,确实什么都没有,以及我不知道。后两种在图表上都会画成零,但它们的含义正好相反。
检测问题 我即将展示的数字,或我即将相信的“一切正常”,会不会来自失败的查询、空表、被拒绝的权限,或者一个我没有读完整的输出(见第 44 条)?
警示信号 “如果读取失败,我就直接设成零,继续往下走。”
示例与应对方法 例子 一条查询因为权限过期而失败,仪表盘却显示该地区“0 笔销售”。没有人质疑,因为零看起来就像数据。
怎么做 如果读取失败,就把错误直接显示在卡片上,而不是显示一个空白或零。还有,一个显示“0 个问题”的计数器,只有在它先找到了你故意埋下的一个问题时,才值得信任。
数字与数据
“活跃客户”、“日期”、“老板”、“阈值”:同一个词常常掩盖着两种定义。当两份报告对不上时,几乎总是因为这个。
检测问题 是否存在一条规则,对两者中的一个成立,对另一个却不成立?如果我在同一个计算里合并两个数字,它们是否来自同一个范围、同一个量纲和同一个时期?
警示信号 “是同一个日期” · “视图已经过滤过了” · “它们都是比索。”
示例与应对方法 例子 “日期”可能是事件日期、加载日期或分区日期。或者“老板”:是这个人现在的老板,还是事件发生时他的老板。
另一个例子 某个活动表里,支出是整个公司的,但营收来自每一百笔购买中抽取的一笔样本。两列都标着“比索”,结果每个活动的回报都是负数。
怎么做 在代码和屏幕上,给每个变体起不同的名字,哪怕名字很丑。在除以或相减两个来自不同来源的数字之前,先写下每个数字的来源:如果结果在所有情况下都一样(全都亏损,全都盈利),先怀疑量纲,而不是业务本身。
数字与数据
一个还没结束的月份,看起来总比已经结束的月份差。这不是趋势,而是日历造成的。
示例与应对方法 例子 销售曲线在最后一个点“下跌”了,那个点代表的是才过了一半的月份。
怎么做 把指标截止到最近一个已结束的期间,或者在屏幕上标明不完整的期间。同时把两种时钟分开:群体层面的数字用已结束的月份,具体某个人则用当天的数据。
数字与数据
许多系统只存储当前的值。如果你在这种字段上构建一个“历史”变量,你衡量的其实是现在,却把它包装成过去。
检测问题 这个字段在变化时会保留之前的值,还是直接替换?
示例与应对方法 例子 系统只存储每个人最新的老板。对已经离职的人来说,那个老板的记录就被冻结了,于是“每位老板团队的离职率”最后衡量的是“这个团队已经不存在了”。
怎么做 在用它回头看历史之前,先问数据源这份数据是快照还是历史。
数字与数据
如果某个变量几乎毫无误差地把离开的人和留下的人区分开来,它很可能把结果本身藏在了里面。
检测问题 我的质量指标上升,会不会是因为模型在猜某件事发生的时间,而不是猜它发生在谁身上?
警示信号 “结果几乎是完美的!” · “那就按年份调整一下,继续往下走吧。”
示例与应对方法 例子 一个模型突然变得好得多。结果发现有一个变量对所有离开的人都是 1,对所有留下的人都是 0,因为数据源对两组人的填写方式不同。另一个变体是:模型学会的是某件事发生在哪一年,而不是它发生在谁身上。
怎么做 向前测试:用过去的数据训练,在之后的时期评估。在根据某个因素做调整之前,先问这个因素是不是恰恰决定了你要预测的结果。
脚本与文件
如果“什么算活跃”这条规则在每条查询里都重复一遍,这些副本迟早会互相偏离。不是会不会的问题,而是什么时候的问题。
检测问题 如果我改了这个定义,要动多少个文件?如果超过一个,说明这条规则没有集中设置。
警示信号 “我也放在这儿吧,就一行而已。” · “这个数字不会变的。”
示例与应对方法 例子 一个校验器把参考条目的数量手动写成了 43。文档增长到 47 条后,它把新增的四条判定为不存在而拒绝。现在它直接数文档里的条目。
怎么做 把定义放在每条查询都经过的唯一位置,让它们都从那里读取。手动输入一个从别处复制来的数字,同样是重复的定义。
不要混淆 在教学材料中为了有意说明而重复一段解释,不算复制定义。不该复制的,是计算这个值的规则。
脚本与文件
一个流程会声明它打算做什么,但发布出来的是它实际做了什么,两者不一定相符。使用输出的人应该读取结果。
检测问题 这个东西是根据流程的配置构建的,还是根据它发布的结果构建的?
示例与应对方法 例子 一个屏幕根据模型“打算”使用的变量列表来生成控件。训练时去掉了其中一个变量,结果好几天都有一个什么也不做的控件。
怎么做 从已发布的结果中派生。无法派生的内容,应该明确地报错,而不是悄无声息地消失。
Git 与流程
当你改动一项数据,而其他人是按它的名字来读取时,谁在读它的清单必须写下来。记忆总会漏掉一个。
检测问题 我这份使用者的清单,是从搜索中得来的,还是凭记忆?
警示信号 “我已经覆盖了它使用的三个地方” · “他们已经看过了” · “我已经全部加上了。”
示例与应对方法 例子 一个模型更新之后,三个屏幕上的标签都改了。第四个屏幕,没有人记得它,仍然提供上一个模型的选项。
另一个例子 写一章的时候,凭记忆断言读者已经了解前一章的三个函数。搜索之后发现,那一章只教了一个,而另一章其实已经解释过被当作新内容来呈现的那个主题。
再一个例子 一次改动在保存时,所写的文件清单是凭记忆列的,漏掉了一个:正是让新功能能够运行的那个文件。事后审查待提交改动的清单时,才发现了它。
怎么做 按改动类型保存一份影响地图。完整地过一遍,每一项都要说明“有改动”或“不适用,因为……”;如果改动产生了新的使用者,就在同一次改动中加上它。在引用一个来源之前,把你所断言的那句话从该来源中复制出来:别人说某人做了什么、看到了什么,也需要核对。
Git 与流程
关闭一个待办事项通常涉及三个地方:待办列表、监视它的测试,以及描述它的文档。如果你只动了其中一个,其他两个就会与它相矛盾。
检测问题 还有哪些地方声明了这种状态?在保存之前把它们都列出来。
示例与应对方法 例子 一个待办在列表中被标记为已关闭,但监视它的测试仍标着“预期失败”,而文档仍把它描述为未解决。三个地方,三个版本。
怎么做 工作的单位是完整的一组,并且在同一次改动中完成。
界面与设计
如果你的产品对两类用户、两个地区或两款产品,各有一个版本的同一屏幕,那么对其中一个的改进,就意味着另一个也有一项待办的改进。
检测问题 我正在改动的东西,是否有一个用户视为“同一模块”的孪生版本?
示例与应对方法 例子 桌面版增加了日期筛选器。而移动版,在用户眼里是“同一个屏幕”,却一个月都没有这个功能。
怎么做 在检查过孪生版本并写下“相同”或“不适用,因为……”之前,不要结束这项改动。
写作与文档
如果你因为数据不可靠而删掉某样东西,那么同样的结论很可能还留在其他文本里,只是换了说法。
检测问题 还有哪些其他文本说的是和我刚删掉的内容相同的东西?
示例与应对方法 例子 一张不可靠的图表被撤下了,但一张“主要发现”卡片和一份行动计划仍然在推荐图表所说的内容。
另一个例子 一份章节计划从第 7 章重新编号为第 15 章。另一份文档里的两个列表仍然指向旧计划中的“7.4”和“7.7”,而它们现在已经是不同的主题。替换计划时,旧的编号也是一种结论。
怎么做 按主题搜索,而不是按标识符搜索。这类重复的说法不会沿用同一个标识符。
数字与数据
“没有报错”不等于“结果正确”。要拿它和独立于你自己计算的东西去对照。
检测问题 除了我自己的代码,我会用什么外部东西来核对这个结果?对于文本声称系统会做的任何事情,无论是出现在示例、规则还是选项说明里,我都在真实引擎上运行过了吗?
警示信号 “在我的机器上是能跑的” · “它没报错” · “就是这么运作的,我只是解释一下。”
示例与应对方法 例子 一道选择题声称,把一个数字和一段引号括起来的文本比较会报错。在某些其他引擎里确实如此;但在人们真正在用的那个引擎里,查询跑通了,还返回了行。
另一个例子 某篇文章正确地解释了一个命名的代码块只在它自己的查询中存在,但它自己的示例却假设三个独立的查询共享它。实际运行时,第二个查询报错了:表不存在。
再一个例子 一页上的所有示例都运行过了,但仍有三句话是错的:文末写的某个选项的说明,描述了一个没有人运行过的行为;一条关于如何读懂错误的规则(“如果它显示这个,那就是缺少那个”)被同一页上的另一步骤推翻了;而引言说某个函数“返回一个结果”,实际上它并不返回。
怎么做 与已知数字、源系统或上一期数据对账。在真实环境中测试:使用它的权限、它的库,以及真正运行的那份代码。每一条关于系统行为的说法,在写下之前都要至少运行一次,包括那些顺带出现在示例里、或最后才写下的说法。如果说法是一条通用规则(“只有”“总是”“从不”),就找出能推翻它的情况并一并运行:“把值转换为整数只会读取整数”这一说法,是用一个带小数点的文本字符串检验的,结果对带小数的数字来说并不成立。当你写出一个分数(“六分之五”)时,要说明它是哪个整体的分数;假设的场景也要检验:规模、差值和 p 值必须彼此一致。
测试与检查
一个永远通过的测试可能是坏的。要知道它是否有效,唯一的办法是让它对着真实的错误失败一次。
检测问题 我有没有用错误在真实会出现的各种形式去测试我的检查?有没有用正确的情况去测试?
示例与应对方法 例子 一个用来禁止某个词的测试只查找了小写形式;只要是大写或带重音的形式,它就会通过。
另一个例子 为一个练习植入了 37 条重复记录,结果计数返回了 92:数据中本来就有天然的匹配。一个在保存前把计数与植入数量作比较的检查,抓住了这个问题。
再一个例子 一条用来识别缩进代码的规则,把一行以文本开头的内容也标记了出来:本应通过的情况,形式并不是这样的。还有一个测试,本来是要验证文本比较功能是否正常,结果却通过了,尽管比较本身是坏的,因为另一个审查先拒绝了这个错误。
怎么做 用两个情况来校准:一个必须能找到问题,一个必须找不到问题,并在它诞生的当天就真正运行。好情况和坏情况同样重要:一个拒绝正确内容的检查器也是坏的,而且更难发现,因为它看起来很严格。好情况要包括正确内容的真实形式,包括你自己没写过的形式:另一种顺序、另一个名字。每个检查都要为自己校准:如果另一个检查先抓住了错误,那说明你的检查从未被测试过。而且,一个只是重复它已经读过的内容的审查是不可能失败的(比如一次“第二遍运行”,只是重新读取同一个文件):要把它换成一个真正会在出错时失败的对账。
测试与检查
如果你的检查经过的是可能造成损害的同一条流水线,那么损害也会影响检查,无论一切正确还是错误,它给出的答案都一样。
检测问题 我正在调查的那种转换,也会转换我的测试吗?
示例与应对方法 例子 他们想找一个重音问题,于是通过同一条正在造成破坏的路径发送带重音的查询。
怎么做 把测试做成损害触碰不到的形式。
测试与检查
“如果找不到数据,就跳过测试”这类分支,恰恰会在世界发生变化时启动,而它们在没有人注意的情况下停止了监视。
检测问题 我的测试是否有一个会在合法变更时激活的静默退出?
示例与应对方法 例子 一个测试通过标题来定位一张表,找不到时就跳过。标题变了,这个测试连续几周都通过,却什么也没检查。
变体 一个检查在遇到第一个损坏的文件时就停止,于是之后的每个文件都没有被监视。一个验证器如果直接崩溃,而不是报告“这个页面无法读取,第 12 行”,就等于关闭了整个审查。
怎么做 如果缺少的是工具,跳过它是诚实的。如果缺少的是被监视的数据,测试就必须失败。而一个无法读取的文件,应该作为一项带有行号的发现呈现,而不是让整个审查就此结束。
测试与检查
每个自动审查都会过滤出“相关”的内容,而这个过滤器可能让它恰好在问题所在的地方看不见。
检测问题 我的过滤器排除了什么?在相信结果之前,先把它写下来。
警示信号 “它找到了 48 处差异,我已经有清单了。” · “测试会遍历所有内容。”
示例与应对方法 例子 一个设计审计器只审查没有子元素的元素。需要审查的标签里有一个帮助图标,所以它从来没有看到这些标签。
另一个例子 一个测试会遍历每一页,总是选中第一个选项,并逐个运行每条查询。它每次都通过,却从未检查正确答案是否被接受。
再一个例子 为了判断某人是否修改了一段文字,比较时忽略了空白。结果恰好漏掉了一种情况:一页专门讲如何用空格和换行排版这段文字。规范化本身也是一种过滤。
怎么做 写下过滤器排除了什么,并亲手检查一部分被排除的项目。当你发现某个过滤器让你看不见问题时,去找找你之前写过的其他类似的过滤器:有一个审查去掉了代码中的所有空格,把两个单词连成了一个,从而拒绝了正确的代码;而这发生在人们刚从另一种规范化中吸取了同样的教训之后不久。
测试与检查
语法检查器会通过那些运行时会崩溃的代码。一个所有检查都是“绿色”的空白屏幕,是最糟糕的失败。
示例与应对方法 例子 一个页面通过了语法检查和类型检查。打开时却是空白的:它用了一个只在服务器端存在的变量。
怎么做 至少有一个测试要运行完整的页面或程序。
测试与检查
如果你检查的是 X,而用户拿到的是从 X 派生出来的 Y,那么两者之间就有一段没人在看的代码。
检测问题 有没有人检查过最终到达屏幕上的内容,还是只检查了源头输出的内容?
警示信号 “它来自已经验证过的数据” · “文件没问题。”
示例与应对方法 例子 他们验证了每个发布的结果都是正确的,但从这些结果构建出的版本在传到屏幕的过程中,用了另一套处理缺失数据的规则。
另一个例子 审查者读了每一页的源文件并批准了。一张在手机上堆叠成卡片、因而无法再对比的表格,只有在真正看页面时才暴露出来。
怎么做 在内容到达人的那一刻进行检查:看到的页面、下载的文件、收到的消息(另见第 41 条)。
界面与设计
“它们已经一样了”,需要你比较了所有你能看到的属性,而不只是其中一个。
检测问题 我比较的是一个属性,还是用户能看到的所有属性?
示例与应对方法 例子 两个屏幕的设计为了保持一致,经历了四轮修改:先是尺寸,然后是字体排版,然后是颜色,最后是标签颜色。每次交付都说“完成了”。
另一个例子 一份资料里的插图只审查了内容和尺寸,没有审查对比度:结果有四张图里的两种文字颜色低于最低可读水平。
怎么做 在修复第一个差异之前,先列出所有差异。在你的记录中,动词会暴露你:“修好了”和“会变成”是意图;“它显示”和“它呈现”才是测量。
见于模型 ~opus 2026-08 claude-sonnet-5-5 2026-10 由 AI 翻译,等待母语者审阅。 在 GitHub 上查看
脚本与文件
“如果不存在就插入”能防止重复,但它会把内容冻结住:已经存在的东西再也不会被重新写入。
检测问题 如果我改变了这段代码注入的内容,下一次运行会更新它吗?
示例与应对方法 例子 一个用于分级的图例,其范围只插入了一次。范围后来变了,图例却一直显示旧的范围。
怎么做 替换它:删除旧的,重新注入,并检查只剩下一份副本。
脚本与文件
如果你通过搜索用户会读到的文字(比如标题或标签)来定位自己的改动,那么另一个改进会贴在它上面,你的改动就找不到它了。
检测问题 我用作锚点的文字,会不会因为别的原因而存在,或者会不会在它里面长出别的东西?
示例与应对方法 例子 在几个标题旁边加了一个帮助图标,而那些通过标题定位自己的更新,都不再起作用了。
另一个例子 一个审查通过搜索文本 "int(" 来检查某人是否转换了数字,结果它匹配到了 "print(" 里面:任何打印了内容的行,看起来都通过了。你搜索的内容必须是一个完整的词,而不是另一个词的一部分。
再一个例子 一个审查把代码打印的最后一行当作它的答案。对于一个在内部打印数字却不返回它的函数,那一行的存在另有原因:答案显示在了屏幕上,而代码其实从未交付它。
怎么做 使用你自己的标记,比如 ID 或分节注释。当一次改动涉及多个代码块时,要检查所有的块,而不只是第一个。
脚本与文件
一个重复执行却没有出错的改动,必须运行两次来检查,而不是一次。
检测问题 我是否在一个已经应用了这项改动的东西上运行过它?在我之后,还有哪些其他过程会修改同一段文本?
示例与应对方法 例子 为了检查一个脚本在第二次运行时是否没有改动任何东西,他们比较了同一次运行的两个校验和,然后又比较了两份不同文件列表得出的校验和。两种比较都会说“相同”或“不同”,却都没有证明任何东西。正确的比较方式是:在同一条命令中,对相同的文件先取校验和,运行脚本,再取一次校验和。
怎么做 从同一个起点运行两次,并比较文件:第二次运行不能再改动任何东西。
脚本与文件
如果你在“第 9 个单元格”前面插入了一个内容,那么现在的第 9 个单元格已经是另一个单元格了。
检测问题 这个工具是否通过位置来定位东西?而我是否刚刚在它前面插入或删除了什么?
示例与应对方法 例子 一个脚本在笔记本的第 9 个单元格前插入了一个标题单元格,然后去编辑“第 9 个单元格”:结果编辑的是它刚刚插入的那个单元格。
怎么做 每次插入之后都重新定位,并通过检查完整的结构来核对结果。
脚本与文件
打开一个文件准备写入,会立刻把它清空。如果写入到一半出错,你得到的就是一个空文件。
警示信号 “这只是一个简单的替换,我直接写入就行。”
示例与应对方法 例子 一个脚本打开配置文件准备重写,在计算其中一个值时失败了,结果把文件留空。服务器再也没能启动。
怎么做 先在内存中准备好所有内容,写入一个临时文件,最后用一步操作替换原文件。
脚本与文件
“计划任务已启用”并不代表“它这个月运行过”。而且,等待某个告警在失败时提醒你,就意味着相信这个告警永远不会失效。
示例与应对方法 例子 一个计划任务显示为“活跃”,但因为凭据过期,已经两个月没有运行了。仪表盘却把最后一个数据点显示得仿佛是今天的。
怎么做 在屏幕上显示数据最后一次更新的时间。检查哪些时期的数据缺失。并让流程可以安全地重复运行,而不会重复添加行。
测试与检查
质量过滤通常只审查新内容。已经存在的内容被放在一边,它的假设随着时间老化,却没有人再去问。
检测问题 这个已经运行了几个月的东西,发布以后有没有人重新验证过它的假设?
示例与应对方法 例子 一份已发布的报告一直按某一列进行过滤,而数据源已经停止填写这一列。它显示为零,但因为它“已经验证过”,没有人再去问。
怎么做 在每次审查中,让正在使用的内容接受与新候选内容相同的测试。
终端与工具
如果一个数字没有如预期地变化,或者恰好在你盯着看的时候变了,请按以下顺序怀疑:缓存、另一个进程写入同一张表、你用来查看的工具,最后才是系统本身。
检测问题 我看到的是系统本身,还是一个缓存、一份副本,或是一个保留了旧快照、或在途中改动过它的工具?
警示信号 “我已经修过了,可还是一样” · “这次我真的打对了。”
示例与应对方法 例子 一张表“少了一半的列”。几分钟后它又完整了:你看它的时候,另一个进程正在重建它。
另一个例子 页面上的一张新图片“加载不出来”。文件是好的:测试服务器看不到它启动之后才创建的文件夹。
再一个例子 一个脚本“把文本模式算错了”。脚本没有问题:代码所经过的终端在脚本读取之前,把每个 \n 都变成了真正的换行。而在一个多个单元格共享内存的笔记本里,本该出现的错误没有出现:变量还保存着之前单元格的值。
怎么做 在宣布出了大问题之前,过几分钟再拍一次快照。如果某个仪器已经用同样的方式让你失望过,就替换它,而不是继续盯着它:重复出现的失误不是运气不好,而是工具用错了。在途中会转换 \n 或 \\ 的终端,不适合用来传递代码:代码应该写进一个文件,用一个不会改动它的工具写入,然后从文件运行。凡是机械性的事情(一条已经失败了三次的命令),都应该用守卫拦住,而不是靠记忆。
见于模型 ~opus 2026-08 claude-opus-5-5 2026-10 由 AI 翻译,等待母语者审阅。 在 GitHub 上查看
测试与检查
如果同一个人既运行又确认,那么执行中的错误就会自己确认自己。
检测问题 除了做这件事的人,还有谁审查过用户实际收到的结果?
示例与应对方法 例子 谁写了每一页,谁就拍摄审查用的截图,而且只拍自己认为有风险的步骤。审查者读的是源文件。没有截图的步骤,没有人看到过。
怎么做 一个人运行,另一个人通过阅读结果来检查。“我已经做过了”是可以开始验证的信号,而不是验证本身。验证的人也不能通过与执行的人相同的通道去看(见第 14 条):如果两人都只读源文件,就没有人看到屏幕。
终端与工具
替别人运行命令的人,是蒙着眼做的。每一条出错的命令,都会消耗他们对下一条命令的信任。
检测问题 对于这条命令里的每个名称,我是今天从系统中读取的,还是凭记忆输入的?
示例与应对方法 例子 一条部署命令里的分支名是凭记忆打出来的。分支的名字其实不同,执行的人花了半个小时,才去问为什么。
怎么做 在交给别人之前自己先运行一遍,并且从系统中复制每个名称(脚本、路径、分支),而不是靠记忆。
见于模型 ~opus 2026-08 claude-sonnet-5-5 2026-10 由 AI 翻译,等待母语者审阅。 在 GitHub 上查看
Git 与流程
朝着错误的方向快速前进,并不是进步。
检测问题 这个改动块是否很大、很难撤销,或者它会改变另一个人已经决定的东西?如果是,就解释并等待。如果不是,就说明你要做什么然后继续:每一步都请求许可,也是有代价的。
示例与应对方法 例子 一个整个项目的文件夹结构在没有通知的情况下被重新组织了。这次改动本身是正确的,但它被完全撤销了,因为另一个人已经基于代码之外的理由决定了那个结构。
怎么做 在每个大块工作之前,用通俗的语言说明你要做什么、为什么做,然后等待确认。
界面与设计
业务屏幕上的每个元素都应该有助于做出某个决定。方便审计的内容放在代码里;方便决策的内容放在屏幕上,并使用业务语言。
检测问题 这会改变使用这个屏幕的人的任何决定吗?如果不会,它为什么在这里?
示例与应对方法 例子 一个销售屏幕在第一行显示了每个订单的内部 ID,以及上次计算的时间。没有人靠它们做任何决定,它们却占据了真正重要的数据的位置。
怎么做 在往屏幕上添加任何东西之前,先写下它会改变什么决定。如果它什么都不改变,就放到技术日志或代码里,而不是屏幕上。
界面与设计 web apps
如果一个按钮改变的东西在用户的视野之外,那么对用户来说,有效和无效看起来是一样的。
检测问题 使用完这个控件后,不挪动位置,用户能看到发生了什么变化吗?
示例与应对方法 例子 一个筛选器确实筛选了表格,但表格在两屏之外的地方。他们却报告说它“没有任何作用”。
变体 隐藏一个容器,会把里面的所有东西都隐藏掉,即使有些东西和它毫无关系。例如,一个有用的链接恰好位于一个已废弃的控件里面。
怎么做 每个控件都要说明它改变了什么,并把用户带到那个地方。在隐藏任何东西之前,先检查里面还有哪些其他元素。
界面与设计
在小字号下,“÷”和“+”、“l”和“1”、“O”和“0”很容易被混淆。如果两个人都把同一段文字读错了,那不是他们分心,而是字号的问题。
检测问题 在它将被阅读的字号和字体下,这个符号会不会和另一个符号混淆?或者它看起来像一个无法键入的东西?
示例与应对方法 例子 两个不同的人在一张小字号的卡片上,都把“98 ÷ 441”读成了“98 + 441”,并且都报告说这个计算是错的。
另一个例子 一份资料的代码字体把 >= 连写成了一个符号 ⩾。学习者照着所见抄写,而这个符号既不在键盘上,也不在语言中存在。
怎么做 当一个符号容易混淆时,用文字替换它:用“98 除以 441”代替“98 ÷ 441”。
测试与检查
一份质量报告通常对现象判断正确,但它提出的原因往往只是一个假设。
检测问题 我能用自己的测量重现这个原因吗,还是因为它听起来对就接受了?
示例与应对方法 例子 报告要求修复一张加总出错的图表,但那张图表在屏幕上根本不存在:数据传过去了,却没有任何东西把它画出来。
怎么做 在修复前后都进行测量。只部分改善了现象的修复,还不算结束。
Git 与流程
你在做别的事情时记下的“以后再说”,可能已经不存在了,或者它原本就是一个有意的决定。
检测问题 我今天检查过这一项吗,还是只因为它被写下来了,我就去做它?
示例与应对方法 例子 一条笔记写着“去掉日期筛选器”。等到真正去做时,这个筛选器已经是团队书面确定的决定了,去掉它会弄坏一份报告。
怎么做 在处理一个待办之前,先在今天检查它,并查找是否有书面决定说明它是被有意保留的。
数字与数据
文档中的每个数字都要来自带有日期和来源的测量,而不是来自记忆,或者文档的早期版本。
检测问题 这个数字,或这个比较(“超过一千”“过半”“几乎全部”),来自哪些数据、哪个日期?
警示信号 “这是我们一直用的那个数字” · “大概就是这个数” · “你看,他们都通过了。”
示例与应对方法 例子 一段说明写道,某个错误让人丢失了“将近五分之一”的工单。测量之后发现,实际比五分之一还多:这句话来自心算,而不是数据。
另一个例子 一篇文章说某组队列“超过 50%”;但十二个队列中有五个低于这个数字。另一篇的插图展示了一个手算的结果,但百分比乘以总数并不等于图中画出的数字。
怎么做 写下一个数字之前,重新测量一次,并在旁边注明它来自哪些数据、哪个日期。数量词(“几乎”“超过”“大多数”)其实是伪装成文字的数字:要用同样的方法去测量它们。如果配图里有计算,就让绘制图形的同一个脚本来完成计算。
写作与文档
文档审查是改进文档,而不是重新设计系统。如果系统有某种限制,文档就要写明它,不能隐藏它,也不能承诺以后会修。
检测问题 我即将写下的内容,描述的是它现在如何运作,还是它应该如何运作?
示例与应对方法 例子 一份手册写着数据会自动同步。实际上系统只在应用打开时才同步。审查时把手册“修正”成承诺很快会自动同步,而不是如实写出限制。
怎么做 把关于文档的发现(“写得很差”)和关于系统的发现(“运作方式与理想不同”)分开。第二类要作为限制声明出来。
写作与文档
正式文件对所有内容都以同样的权威口吻陈述。一个编造出来的细节,比如负责人、时间表或响应时间,读起来和真的一样。
检测问题 这个细节我是真的知道,还是只因为听起来合理而假定它?
示例与应对方法 例子 一份流程写了一个 24 小时的响应时间,但没有人同意过。它是因为听起来合理而写进去的,一旦发布,就变成了承诺。
怎么做 用醒目的方式标出缺失的内容,比如用红色写上“待定”,并去问真正知道的人。中性的措辞比错误的措辞好。
不要混淆 这个标记是给做决定的人看的,不是给最终读者看的。已发布文本中的“待确认”也是一种不该存在的数据。如果文档在确认前就要发出,就用不声称缺失内容的中性措辞,并把待定事项保留在报告或项目记录里。
写作与文档
每次修改都要在封面上更新版本号,并在修订记录中添加一行,写明日期、改了什么以及由谁修改。当两个副本同时流传时,版本号是唯一能说明哪一份才是正确版本的依据。
检测问题 如果有人手里是上周的副本,他怎么知道它已经不是正确版本了?
示例与应对方法 例子 一份手册的两个副本在邮件里流传。旧版写着工具“双击即可打开”,而这已经不对了,却没有人能分辨两者中哪一份才是正确的。
怎么做 在修改文档的同一次变更中,提升版本号并写下修订记录。
40 日志记录的是留下了什么、为什么留下,而不是失误的编年史
Git 与流程
变更历史是为了让人理解产品,而不是为了讲述做这次改动的人已经解决的失误。
检测问题 这一行是帮助人理解产品、避免重蹈覆辙,还是只是记录了发生在我身上的事情?
示例与应对方法 例子 一份历史写着:“试过 A,失败;试过 B,失败;保留了 C。”几个月后有人读它,他只需要 C 和它的理由,却不得不读完三段才找到它。
怎么做 写下最终状态及其理由。试过并失败的内容,只有在它能教会别人某件需要避免重复的事情时,才写进去。
界面与设计 web
在文本中找到某个词,并不能证明它会显示出来。文本可能在文档的其他地方,而应该负责显示它的格式却输出为空。
检测问题 我是否以读者看到的方式看到了那个确切的元素,还是只是在源文件中找到了那个词(或那段代码)?
警示信号 “这个词在文件里” · “这只是一行的修复。”
示例与应对方法 例子 三个编号步骤显示成了空的项目符号。自动检查通过了,因为这些词在文档的别处出现过。
另一个例子 添加了一条“右侧还有更多列”的提示,因为代码已经写好,就被当作完成了。它却从未出现过:一条通用的表格样式移除了触发它的条件。后来是别人在截图中看到的。
再一个例子 一个答案选项在文件中写着“总计:{2 * 78}”。页面绘制时,系统计算了花括号里的内容,显示为“总计:156”:于是出现了两个完全相同的选项。只有看屏幕的人才发现了这一点。
变体 审查者判断的截图,可能展示的是没有人会到达的屏幕。某门课程的截图工具,为了能继续下一步,会用一行只为凑数而填入的内容回答每道代码练习,于是“之后”的截图显示的是这一行,以及“不是预期结果”的提示:审查者从未看到学习者答对之后所看到的内容。在桌面端,截图还会把页眉和侧边菜单也拍进去,结果这一课变成了一条看不清的长条。用于审查的截图,要按照成功用户的方式回答每一步,并且只截取被审查的那一部分。
怎么做 检查确切的元素,比如“这三个步骤,以项目符号显示”,并且至少有一次以读者将看到的方式查看页面。即使是小修复也要检查:因为那里最可能被另一条规则覆盖。第一次在页面上使用新资源(代码块、固定页眉)时,也要在手机上看一眼。
见于模型 ~sonnet 2026-09 claude-sonnet-5-5 2026-10 claude-opus-5-5 2026-10 由 AI 翻译,等待母语者审阅。 在 GitHub 上查看
写作与文档
在旧副本上做的审查,会把已经修好的错误重新带回来。
检测问题 我正在编辑的版本,是最新的那个,还是我之前打开的那个?
示例与应对方法 例子 对一份手册的重新组织是在几周前的版本上做的。它又一次写着工具“双击即可打开”,而这已经不对了。
怎么做 取审查提出的结构,把最新的内容填进去,而不是反过来。
写作与文档
经过几轮零散的修补,文档可能会自相矛盾。每一处修改在它所在的章节里都是对的,但没有人重读其他部分。
检测问题 自从有人完整读过这份文档以来,它又经过了多少次修改?
示例与应对方法 例子 一节写道没有连接就什么都保存不了,而常见问题解答里仍然写着,采集到的内容会保存在设备上。
怎么做 每完成一轮修改,就按主题搜索文档中所有关于你改过内容的说法。
终端与工具
只看结果的最后几行,或者把它通过另一个命令管道处理,可能会丢掉恰恰写着“失败”的那一部分。一个失败测试的截断输出,看起来和一个通过的测试的输出一模一样。
检测问题 我是在读结论(通过了多少、失败了多少、以什么方式结束),还是只读了输出的一部分?
警示信号 “屏幕上没有出现错误” · “最后几行就够了。”
示例与应对方法 例子 一个页面被标记为已审查,因为它的测试“执行完毕,没有报错”。结果经过了一个只显示末尾的命令,于是测试失败的提示在途中丢失了。
另一个例子 一个测试套件会自动重试每一个失败的测试。在第二次尝试中通过的测试不计为失败,只被算作“不稳定”,于是原因被隐藏了:每次切换步骤都要向服务器重新请求页面。没有重试时,失败会显现出来,也可以被测量。
怎么做 在改变状态之前,先找到结论那一行。如果你串联了多条命令,要确保第一条命令的失败能传到最后。如果输出里没有结论,那就表示“我不知道”,而不是“通过了”(见第 1 条)。并且把完整的输出保存到文件里,在文件中查找结论:这样你就不必事先决定要看多少内容。
界面与设计 web apps
如果你只测试一切顺利时会发生什么,那么犯错的人就会找到这个漏洞。而在学习时,犯错是最常见的事情。
检测问题 如果用户做了最可能的错误操作(拼写错误、空答案、连点两次),他们会看到什么?他们还能继续吗?
示例与应对方法 例子 一个查询单元格只有在查询运行时才计为一次尝试。输入有拼写错误时,前进按钮永远不会激活,使用者就卡住了。测试都通过了,因为它们总是运行那条没有错误的起始查询。
怎么做 对每个控件,测试三条路径:正确的路径、最常见的错误和空的情况。三者都要说明发生了什么,并让用户能继续下去。
见于模型 ~sonnet 2026-09 claude-sonnet-5-5 2026-10 由 AI 翻译,等待母语者审阅。 在 GitHub 上查看
界面与设计 web
在每个页面上都要做的事情,比如读取全部内容或重新计算索引,在十个页面时成本很低,在一百个页面时就很高。它是逐渐增长的,没有人会注意到,直到某些东西开始“时不时”出错。
检测问题 这个每次请求都运行的东西,是否要遍历全部内容?它今天需要多长时间,内容翻倍后需要多长,以及很多请求同时到达时需要多长?
警示信号 “在本地加载得很快” · “这是一个间歇性的失败。”
示例与应对方法 例子 网站的每个页面都会读取并解析全部内容,以生成目录。单独一个页面只需 0.4 秒。但侧边栏一次请求大约 40 个页面,加在一起就要 16 秒。几个月后,测试开始在切换页面时失败,而失败的样子看起来像是测试本身的问题。
怎么做 在编译后的服务器上,并以人们实际使用的方式,把所有请求放在一起测量,而不是只测一个页面。缓存服务器运行期间不会改变的内容;并留下一个测试,用真正导致变慢的那种情况去测量它。
见于模型 ~sonnet 2026-09 claude-sonnet-5-5 2026-10 由 AI 翻译,等待母语者审阅。 在 GitHub 上查看
Git 与流程
多人同时工作时,每个人都能清楚地看到自己负责的那部分。但他们共享的东西(测试、数据、整个项目的文档)会把各自的改动合并在一起:名字相互冲突,副本落后于原件,测试读取了别人尚未完成的工作。集成本身就是工作,不是粘贴。
检测问题 哪两个部分同时在改同一个文件?谁来集成它?我的测试是否在读取别人尚未完成的工作?
警示信号 “我们每个人都测试了自己的部分” · “问题出在另一个部分。”
示例与应对方法 例子 四个人同时各自写一个部分,但把共享部分当作交付物。集成时,两个交付物使用了共享测试中已经存在的同一个名字,于是那个测试停止运行。另一个测试保留了它所检查内容的一份副本;部分改变了,副本却没有。一次测试运行因为另一个人仍在绘制的图片而失败。最后,每个部分都无法单独保存:共享文件里已经混入了所有人的改动。
怎么做 由一个人负责集成共享内容,并设置一个步骤,在名字冲突时停止。让测试读取原件,而不是副本。等所有人都完成后运行完整的测试套件,或者在排除仍在变动的部分后运行,并说明被排除的是哪些部分。把共享同一文件的部分一起保存,使每个保存的版本都能完整工作。
脚本与文件
当一个文件是由脚本或由其他部分拼装而成时,这个文件只是输出,不是原始版本。直接在它上面做的修复,会在下次生成时无声无息地丢失。
检测问题 这个文件是由别的东西写出的吗(脚本、模板、部件拼装)?如果我重新生成它,我的改动还在吗?
警示信号 “我直接在文件里改了” · “我刚重新生成过。”
示例与应对方法 例子 一份说明是由每个人提交的段落拼装而成的。三行直接在说明里修正了,但当它从提交的内容重建时,那三行旧内容又回来了。另一个例子中,审查者手动修正了一个来自模板的页面的替代文本;它的作者重新生成了页面,修正就消失了。
怎么做 修复之前,先找出它来自哪里。修复源头(模板、脚本、提交内容)然后重新生成;如果你不得不修复输出,那么在同一步中也要修复源头。谁审查了生成的输出,就要说明改了什么,以便把改动移到源头里。在从源头重建文件之前,先将它与即将生成的结果进行比较:任何不同之处,都是会丢失的修复。
界面与设计 web
一条适用于所有东西的规则(全局样式、默认值),可以在没有任何警告的情况下,覆盖每个部件自己做出的决定。
检测问题 这条通用规则是否触及了每个部件自己已经决定的东西?如果它们相互冲突,哪一个会获胜?
警示信号 “我把它做成全局的,这样就不用重复写了。”
示例与应对方法 例子 为整个网站编写的键盘焦点样式,自带了一个圆角。由于它的优先级高于每个组件自己的样式,一个药丸形状的按钮在用键盘获得焦点时,形状就变了。没有人注意到,因为用鼠标时焦点根本不会出现。
怎么做 让通用规则只触及它自己负责的东西(焦点轮廓,而不是形状)。如果它应该有意获胜,就在它旁边写下来。并且要在最不同的那个部件上测试,而不只是最常见的那个。
见于模型 ~sonnet 2026-09 claude-sonnet-5-5 2026-10 由 AI 翻译,等待母语者审阅。 在 GitHub 上查看
50 如果两种解释预测的结果相同,你的例子就无法在两者之间做出选择
数字与数据
只有当竞争的解释会得出不同的结果时,例子才能证实你的解释。如果两者的预测相同,例子就只会让你相信最先想到的那一个。
检测问题 还有什么其他解释会得出同样的结果?我的例子排除了它吗?
示例与应对方法 例子 一节课比较了把漏斗的第一步提高五个百分点和提高最后一步的效果。第一步胜出,课文写道:“它们在访问量更多的地方价值更高。”但第一步也正是转化率最低的那一步,而这才是真正的原因;在第一步转化更好的漏斗里,最后一步反而胜出,尽管它的访问量更少。在另一个项目里,一个页面被归咎于加载耗时 2.7 秒;一个逐页请求的测试,即使没有修复也能通过,因为真正的原因是同时发出四十个请求。
怎么做 在教授或报告某件事为什么发生之前,先写下竞争的解释,并构造一个让两者预测不同的案例。比较大小相同的部分,换一种例子规模再试一次,并在写下一般性结论之前,先在模拟数据上测量它。如果无法把两者区分开,就说明这个例子与两种解释都相容。
写作与文档
已经懂的人看不到自己不假思索就会做的那些步骤。于是他们的第一个例子一次就带来好几样新东西,初学者在第一行就迷失了。
检测问题 在我的第一个例子里,对一个毫无基础的人来说有多少样东西是新的?每一个符号、每一个词、每一个按钮名称、每一个我要求的操作,之前都教过了吗?
警示信号 “谁都懂” · “就一行而已” · “这是常用词。”
示例与应对方法 例子 一门面向初学者的编程课程,开头的一个单元格里一次就有六样新东西,而整个计划从未教过什么是数据类型,也没有教过 = 是从右往左存储并读取的。课程负责人用一句话点明了问题:“你对学习者的假设太多了。”后来,有三节课写着“像拖动公式一样”;但从来没有哪节课教过拖动,只教过“把公式向下复制”。
怎么做 写之前,列出你的例子默认学习者已经知道的东西(符号、词语、点击、学习者本应已经做过的操作),并先教这些:每次只引入一个新概念,每个符号第一次出现时都要解释。对于与按钮名称相同的每个词,以及你要求的每个操作,都要像查找符号那样去核对它是否已经教过。如果可能,让自动检查比较每个例子用到的东西和之前已经教过的内容。
52 关于快速变化的工具,要根据今天的实践来写,而不是凭记忆
写作与文档
一节课或一份指南如果讲的是一个每隔几个月就变化的工具,并且内容来自作者的记忆,那么在写作期间它就会过时。它的前提在第一个读者看到之前,就可能已经不成立了。
检测问题 这段文字的哪些前提依赖于该工具今天能做什么?我上一次核对它们是什么时候,依据的是什么来源?
警示信号 “它就是这样运作的” · “大家都是这么做的。”
示例与应对方法 例子 一份“用 AI 做分析”模块的计划,开头写道:“AI 看不到你的数据,所以要描述你的表格,而不是把它粘贴进去。”课程负责人指出,如今的智能体已经可以连接数据库、自己运行查询并直接给出数字。有出处和日期的调研也证实了这一点:两年前的做法还是从聊天窗口里复制代码。于是,几节课被改写成围绕那些依然成立的东西:定义问题、监督智能体、核对数字。
怎么做 在写关于快速变化的工具之前,先查阅当前的实践,并附上来源和日期:该工具现在能做什么,什么是免费的,人们实际在用什么。文本应围绕能经受住下一个版本的技能来组织(界定问题、监督、验证),而把与具体工具相关的步骤集中放在一个容易更新的地方。标明你无法核实的内容,并在发布前亲自试一遍。
53 代码应经编辑器修改,而非经 shell 的文本处理
终端与工具
用 shell 自带的字符串工具改写源文件(读取、替换、再写回),等于让代码经过为其他用途设计的规则:默认编码、运算符优先级、引号与转义。命令看上去完全正确,文件出来的结果却与命令所表达的不同。
检测问题 这次代码修改是经过编辑器,还是经过 shell 的读取、替换和写回,从而受其编码、优先级和引号规则的支配?
警示信号 “这只是个小替换,一条命令就能搞定” · “几处一起替换更快。”
示例与应对方法 例子 同一次会话中有两次编辑,都发生在 Windows PowerShell 5.1 里。对一个脚本做读取、替换、写回(Get-Content、-replace、Set-Content -Encoding utf8):它以系统的 ANSI 代码页读入文件,却以 UTF-8 写回,于是其中每个 ✓ 和 · 都变成了 ✓ 和 ·,而它的自检也打印出了被弄坏的符号。另一次,用 `@('old', 'new' + "n" + 'more')` 这样的写法列出一组替换项,结果丢掉了几行:在 PowerShell 里逗号的优先级高于 +`,于是每一对变成了四个元素,而新文本只写入了第一段。
怎么做 用编辑器自带的编辑和写入工具修改代码。如果改动必须用脚本完成,就把脚本写进一个文件,并用能正确读写 UTF-8、字符串字面量朴素直接的解释器运行它(node fix.mjs、python -I fix.py)。然后检查差异:如果改动的行数超出了你的预期,或者改变了你没有碰过的字符,那就是 shell 的问题。
EDU-1 如果不具备这项技能也能得出同样的答案,这道练习就测不出它
测试与检查 education
只有当跳过某项技能的人得到的结果不同,练习才能检验这项技能。如果起始状态、某个示例或某个捷径本身就能得出正确答案,这道练习就会让所有人都通过。这就是第 13 条对练习的应用:检查只有在你亲眼见过它拒绝错误之后,才有价值。
检测问题 跳过我所教内容、或原封不动提交练习的人会得到什么结果?它与正确结果不同吗?我是否挑了一个恰好两种计算碰巧相同的案例(第一组、第一步、总和)?
示例与应对方法 例子 一道练习要求用日期筛选找出夏天的最大一笔购买。但整张表中最大的一笔购买恰好也在夏天:不加筛选,结果一样,而练习照样通过了。另一道练习要求代码中包含 =;把结果手动写成 total = 1945 也能通过。这个检查看的是字母,而不是技能。
怎么做 每道练习都要解三遍:原样提交、带上最可能的错误、以及正确地做。只有第三种可以通过。然后对每道练习运行同一份捷径清单:手动输入的数字、在末尾加上答案的起始代码、手工修好症状而不是原因、出现了但内容为空的必需词,以及带着上一次尝试遗留内存的运行。任何容差都要小于正确答案与最可能错误之间的距离,并写下学习者可能使用的三种正确形式,以免检查拒绝了正确的答案。
写作与文档 education
一旦答案已经出现过,问题就不再测量任何东西:它可能出现在图片里、核心要点里、另一道练习的解释里、步骤标题里,或者前面的课里。答对的学习者,可能刚刚才读过它。
检测问题 学习者在作答之前,无论是在这里还是在前面的课中,是否看到过这个数字、图片或这句话?不仅是精确的数字:还包括近似值(“大约 25”)、写在图注里的结论、标题、“你刚才看到的是……”,以及前一道练习中最可能的错误答案在屏幕上显示的内容。
警示信号 “这只是背景” · “这只是标题” · “这只是图注。”
示例与应对方法 例子 一张标出正确数字的插图,放在要求作答的练习上方。一节课问:在变量里存入一个新值会发生什么,而这一步的标题却是“再次存储就会替换它”。放在最后小测验下方的总结,在作答之前就被读到了。第一次尝试之后出现的反馈框,即使是失败的尝试,也已经给出了答案的方向。
怎么做 检查每个答案是否出现在其问题之前展示的任何内容中,包括第一次失败尝试后出现的内容、起始代码运行时打印的内容、可下载的文件,以及绘图中可以数出来的东西。最后再写步骤标题和学习目标,并把它们与每次预测的结果放在一起阅读。自动检查能找出精确的数字;近似值和换了说法的答案,只有带着答案去读的人才能找到。
EDU-3 有两个都说得通的答案的问题,什么也测不出
写作与文档 education
如果根据题目所说的内容,另一个选项也站得住脚,那么因为有好理由而选了“错误”选项的人会被判错,而答对的人则可能只是运气好。这道题测量的是缺失的那句话,而不是概念。
检测问题 根据题目所说的内容和课程所教的内容,我能为另一个选项辩护吗?要排除它,需要什么事实?这个事实写下来了吗?
示例与应对方法 例子 一道题问:“当这笔销售没有折扣时”,空的折扣应该怎么处理。课程教过:代表事实的空白应保持空白,所以“保持空白”是说得通的;而预期的答案——填上零——依赖于题目从未说明的一件事:目标是所有销售的平均值。另一道题写的是“主要指标是第二个月付费的人的比例”,却没有说明“谁的”比例,两种读法下,两个版本的结果要么并列,要么不同。
怎么做 对每个错误选项,只根据题目和课程内容,写一行说明它为什么错。如果排除它需要一个没有写出来的事实,就把它加进题目里:计算的目的、系统的规则、比例的分母、关于数据的已知信息。在排序练习中,要把每个条目与每个类别都进行检验,而不只是与它自己的类别。
EDU-4 如果能靠选项的位置或长度猜出正确答案,这个测验测的就是猜测
测试与检查 education
每道题单独看可能都没有问题,但整组题目仍可能泄露答案:正确选项总是第二个,总是最长,或者总是大小居中的那一个。学习者发现规律的速度,比作者快得多。
检测问题 如果我只看选项的位置和长度、而不看文字,我猜中正确答案的几率会超过三分之一吗?
示例与应对方法 例子 在某个模块的结业测验中,七道题里有六道的正确答案是第二个选项。在整门课程中,55% 的题目里正确选项是最长的,而三个选项时随机猜中的概率是三分之一。手动平衡长度之后,七道题中有六道的正确选项变成了最短的,而连续三节课中每一道题的正确选项都成了中间那个。
怎么做 显示选项时打乱顺序,并且顺序不能取决于正确答案当初写在哪里。按文件统计最长、最短和居中的选项,并把小文件按模块合并,使少数几道题不会掩盖规律。把统计数字与你的要求对照着看:一次平衡调整可能只是把泄露转移到别处,而不是把它去掉。
写作与文档 education
提示会在错误尝试之后出现。如果它给出了答案、解题的那一行,或者指出了正确解法与最可能错误之间的区别,那么第二次尝试测量的就只是抄写。
检测问题 读了这条提示的人,没有这项技能是否也能通过下一次尝试?它是否点明了答案、解题的那一行、练习所测量的步骤,或者它应该与之相符的先前数字?
警示信号 “这只是轻轻提醒一下” · “他们已经试过一次了。”
示例与应对方法 例子 对一门课程 277 道已检查练习的审查,在 49 个文件中修改了 82 条提示。有些提示直接给出了解题的那一行(“第二个是……”)。有些则规定了把正确解法与常见错误区分开的步骤顺序。在“与先前结果核对”的练习中,提示写着“它应该与你在第 2.2 课中得到的结果一致”:记得先前数字的人,无需做这一步就能直接输入。
怎么做 围绕最可能错误的原因来写提示(“位置从 0 开始,而不是从 1 开始”),并且只引用已经教过的内容。“它应该与先前的数字相符”这类话,留给回答之后显示的解释,在那里说明:两种方法得出相同的数字,本身就是一种验证。提示要对照练习最可能的错误来审查,而不是单独审查。
脚本与文件 web
不论网站怎样保存记录,一次损坏的写入都可能把它后面的内容也拖垮。如果记录是一行一行追加的,被截断的一行会把下一条记录粘在它后面;而会跳过“损坏的行”的读取器,会悄无声息地把两条都丢掉。如果所有记录都放在一个文件里,读取、修改、再整体写回,那么一旦这个文件读不了,之后每条新记录都会失败,只有没人看的控制台会提示。这是第 24 条的另一面:那里文件被清空;这里,之后的内容都丢失了。
检测问题 如果明天存储损坏了,下一位访客的记录会怎样?谁会发现?
警示信号 “损坏的行会被跳过。” · “它只是追加到末尾罢了。” · “写入是原子的,所以文件不可能损坏。”
示例与应对方法 例子 在第一次 Web 模拟的两次运行中,表单都把每条消息保存为一行 JSON,而管理面板会跳过它无法读取的行。文件末尾的一行被截断后,下一条消息收到的回复是“谢谢!”,它被保存了,却从未出现在管理面板里。两个版本中,有一个的注释写着“损坏的行:已跳过”。
另一个例子 在模拟重跑的两次运行中,每条消息都存放在同一个 JSON 文件里,先写入临时文件再重命名,所以中途崩溃不再造成损害。但如果文件以其他方式损坏(手动编辑、磁盘写满、错误的恢复),每位新访客都会收到“内部错误”,管理页面什么也不显示,唯一的信号是服务器控制台里的一段堆栈跟踪。
怎么做 让新记录不依赖旧记录:追加到一个以换行符结尾的文件里(如果没有,就让这条记录以换行符开头),或者把每条记录保存到它自己的文件中,而不是重写一个装着所有记录的文件。读取时,统计无法读取的行数,并把这个数字显示在所有者会看到的地方。测试方法:手动损坏存储(把最后一行截断,或截断整个文件),然后提交一条记录。
界面与设计 web
一个对互联网开放的表单,不会只被真人填写。如果没有任何东西阻止自动提交,而且阅读消息的人也无法清理这些垃圾信息,那么第一波垃圾就会淹没真正的客户。
检测问题 如果明天到达一千条假消息,主人怎么找到那一条真实的消息,又如何删除其余的?
警示信号 “这是个小站点,不会有人攻击它的” · “以后再加限制吧。”
示例与应对方法 例子 在 Web 模拟的两次运行中,联系表单都在几秒钟之内接收了脚本发来的 650 条消息,全都写着“谢谢”。管理员没有分页,也没有删除按钮:管理页面增长到一百万到六百万像素那么高,而在手机上,消息的文字会超出屏幕。两次都被构建者记为待办。
怎么做 从一开始就设置一个低成本的制动装置:一个人不会去填的隐藏字段,以及每个地址每分钟的提交次数限制。还要有一个管理后台,支持分页、最新的排在最前,并能删除或标记为垃圾。测试方法是:用脚本发送数百条消息,然后在手机上打开管理后台。
界面与设计 web
带有 method 和 action 的表单承诺一种备用方案,即使 JavaScript 失效也能工作。但如果服务器只响应 JavaScript 期待的请求,这个承诺就是假的,而用户最后面对的只是一堆代码。
检测问题 如果这个 JavaScript 文件没有加载,用户提交时会看到什么?
警示信号 “反正大家都有 JavaScript” · “我把 action 留着,以防万一。”
示例与应对方法 例子 在 Web 模拟的两次运行中,表单都通过 JavaScript 提交,并声明了 method="post"。在没有 JavaScript 的情况下,一个版本的服务器返回一行 JSON 拒绝了提交,消息就丢失了;另一个版本保存了消息,却只在一个空白页面上显示 {"ok":true}。第二个版本的总结说,它不会在没有 JavaScript 的情况下提交。
怎么做 在两者中选定一个并测试:要么服务器接受普通提交,并返回一个页面(或重定向到感谢页面),要么表单不声明它并不具备的备用方案。关闭 JavaScript 后提交一次。
界面与设计 web
在提交时禁用按钮可以防止重复提交,但按钮会失去焦点,浏览器会把焦点移到文档上。使用键盘的人在看到提示后,只能从头开始。
检测问题 提交之后,无论成功还是失败,焦点落在哪里?下一次按 Tab 会去哪里?
示例与应对方法 例子 在 Web 模拟的两次运行中,提交时都禁用了按钮。服务器拒绝了一个邮箱后,提示出现在按钮下方,焦点落在了文档上,于是下一次 Tab 把用户带到了页面上的第一个链接,远离了需要修改的那个字段。
怎么做 提交完成时,把焦点放在用户需要继续的地方:出错的字段(用 aria-invalid 标记,并在旁边显示其提示信息),或者成功提示上。或者,使用 aria-disabled 和一个标志位来防止重复提交,而不是禁用按钮。只用键盘走一遍错误流程。
WEB-5 没有尝试次数限制的密码登录,安全性只取决于最弱的那个密码
界面与设计 web
对每个错误密码都以全速作答的登录,任何人每分钟都能试上千次。强大的默认密码会掩盖这个问题,直到所有者设置了自己的密码;而没有任何机制阻止这个密码过短。
检测问题 在一分钟内,有人能对这个登录尝试多少次错误密码?第五次之后会发生什么?
警示信号 “默认密码很强。” · “这只是管理页面而已。”
示例与应对方法 例子 在 Web 模拟重跑的两次运行中,管理页面要求输入密码,却从不限制失败的尝试次数:连续 200 次错误密码,得到了 200 次拒绝,用时不到一秒,没有任何一次被拦截。默认密码又长又随机,但构建者自己的总结告诉所有者如何设置新密码,却没有要求它必须够长。
怎么做 对重复的失败进行减速或封锁,按来源地址和按账户分别计算,并让等待时间逐次增长。所有者设置密码时,要求最小长度。测试方法:连续发送二十个错误密码,检查最后几次是否被拦截。