目录
先放一段几乎人人都经历过的对话:
客户:这个页面挺好的,就改一个小地方——用户列表加一列,显示最后登录时间。 我:行,大概半天。 三天后:还在改。
这段对话好笑,是因为听的人都知道结局,而说「行」的那个人第一次也真的相信了半天。
「小地方」这个词有一种魔法:它描述的是需求描述的字数,不是改动的工作量。在软件里,这两件事的相关性低到可以忽略。
一、需求变更笑话博物馆
「就改一个小地方」
所有需求笑话的母题,其他段子都是它的变体。
它的杀伤力来自一个不对称:提需求的人只能看到界面,看不到界面后面。他看到的是一张表格多了一列;他没看到的是这张表被几个接口读、被几个任务 join、有没有缓存、有没有导出、有没有人在做报表。
所以「小地方」这个判断,在他掌握的信息范围内是完全正确的。错的不是判断力,是可见性。
「你照着淘宝做一个就行」
这句话的完整版通常是:「功能不用那么多,照着淘宝做一个就行,先做个简单的。」
它的问题不在于「照着淘宝做」工作量有多大——而在于「不用那么多」是一句无法验收的话。验收标准在需求方脑子里,只有在成品面前才会显形。于是项目会精确地走到那个经典时刻:东西做完了,需求方说「我要的不是这个」。
把这句话翻译成工程语言,它其实是:「我没有需求文档,但我知道你做错了。」
「你先做,做完我就知道要什么了」
这句话听起来像耍赖,但它描述了需求的真实形态。
需求不是一份躺在某处等着被收集的清单,它更像一个只能通过观察来测量的物理量——用户看到东西之后,才知道自己不要什么。原型法的整个理论基础就这一句。
真正的问题不是这句话,而是说完之后没有下一步:没有走查、没有反馈节点、没有「这一版不作为最终验收」的书面约定。那就变成了一次没有护栏的赌博。
「五彩斑斓的黑」
设计圈的名场面,通常还会配上「logo 放大的同时缩小一点」「这个字要显得高端一点」。
它跟程序员的关系是:这就是另一种形态的「加个字段」。需求方描述的是一种感受,而实现方需要的是一组参数。感受无法直接编译,中间的翻译工作没人认领,就变成了来回拉锯。
「这个很简单吧,不就加个按钮」
加按钮本身确实简单。不简单的是这个按钮按下去之后要发生的一切——尤其是当它按下去之后什么都不该发生的时候(权限不足、条件不满足),这部分通常没写进需求,但必须有人写出来。
「需求没变,只是补充说明了一下」
这句是所有段子里最危险的一句。
前五句只是让工期变长,这一句让工期无法承诺。因为「补充说明」意味着需求的边界是开放的:任何一次口头补充都可以事后追认为「本来就是这个意思」。
它还有一种更隐蔽的形态:变更没有消失,只是从会上转移到了私聊里。三个月后回头看 git log,没有任何一条记录说这个功能为什么要长成现在这样。
二、把「小地方」拆开看
回到开头那个「加一列最后登录时间」。这一列到底要动多少东西:
- 数据从哪来——现在没有这个字段,
users表要加列。 - 谁写它——登录成功时更新。看着无害,但登录是高频写路径。
- 写在哪——每次登录都
UPDATE users,主库上就多了一份和登录 QPS 等量的写流量。想避免,就得改成异步写或者另开一张表。 - 空值语义——历史导入的用户从没登录过。显示「从未登录」还是空白?这两种是不同的业务含义,不能用一个
NULL糊过去。 - 时区——库里存 UTC,界面上显示本地时间。那么「最近 7 天登录」的边界按哪个时区切?用户在日本、服务器在美东、数据库在 UTC,三个答案都对,只能选一个。
- 排序——「顺便把最近登录的排前面」是这句话的必然续集。于是要索引。
- 权限——登录行为是敏感数据。运营能看,用户能看到同事的吗?对外的 API 要不要返回这个字段?
- 缓存——用户列表接口大概率有缓存。缓存 key 的维度够不够?这份数据允许多旧?
- 导出——后台那个 CSV 导出要不要带上?带了,是不是要一起改那个跑在凌晨的导出任务?
- 下游——BI 团队正在同步
users表。加列会不会把他们的宽表打歪,会不会让某个SELECT *多出意外的字段? - 回归面——
users是全库被 join 最多的表。
写代码大概占这里面的 10%。剩下 90% 是确认前面十条。
所以变更的真实成本,从来不等于代码的行数,而约等于它的影响半径(blast radius)。同一个「加一列」:
| 场景 | 影响半径 | 真实成本 |
|---|---|---|
| 一张只被一个接口读的从表 | 1 个调用点 | 半小时 |
| 核心业务表,有缓存、有导出、有下游宽表 | 十几个消费方 | 数天 |
| 已经对外发布的 API 契约 | 未知数量的外部调用方 | 需要版本化 + 迁移期,以季度计 |
这也解释了那个反复出现的困惑:为什么同一个需求,在这个项目里要一天,在上个项目里要两周。不是有人偷懒,是两张表的影响半径差了一个数量级。
三、变更成本曲线,和它的反例
教科书上那张著名的图叫「变更成本曲线」:越晚发现要改,代价越高。
Barry Boehm 在《Software Engineering Economics》里给出的经验数字,经常被复述为「需求阶段 1、设计阶段 3~6、编码阶段 10、上线后 100」这样的量级。具体倍数在不同研究里出入很大,但方向是一致的:修正成本随阶段呈量级增长,原因也很朴素——越往后,越多东西建立在那个错误的假设之上。
不过这个故事有个重要的反例,值得一起放进来。
极限编程(XP)的实践者提出过一个相反的假设:变更成本曲线可以被压平。他们的论据不是「变更不重要」,而是如果测试、重构、持续集成做得好,代码就不会随着时间变成一团不敢碰的泥。泥是曲线陡峭的原因,不是时间本身。
两边其实不矛盾,合起来的结论更有用:
变更成本 = 修改本身的成本 × 影响半径。 好的工程实践压的是后半部分,压不动前半部分。
一个字段改了要重新过一遍十几个消费方,这件事和你代码写得多漂亮无关。所以「让变更变便宜」和「让变更变少」是两件必须同时做的事。
四、需求为什么会变
先说一句被引用了无数次的福特名言:「如果我问人们想要什么,他们会说想要更快的马。」
这句话在沟通学教材里出场率极高,但没有任何证据表明福特真的说过它——福特的著作和访谈里都找不到出处,它更像一句事后附会的鸡汤。有意思的是,它恰好和本文主题呼应:一个关于「需求表达不可靠」的段子,本身也是一次不可靠的转述。
把这句话剥掉修辞,剩下的是一个真问题:用户描述的是方案,不是问题。 他要的是更快的马,真实需求是更快的位移。照做,你就去优化马蹄铁;追问,你才有可能去造车。
所以需求变更不是意外,它是常态。真正要分清的只有两类:
- 有价值的变更——它来自对真实问题的更深理解,越早发生越省钱;
- 无记录的变更——它来自口头、来自私聊、来自「我以为你懂」,它唯一的产物是三个月后没人记得为什么。
后者才是所有笑话的主角。
五、把段子变成流程
段子不会消失,但可以让它不致命。下面这几条都是能落地的。
1. 影响面三问
不要直接回答「要多久」。先问三个问题,这时候的「不知道」是专业的:
- 改哪一个数据? 找到那张表、那个字段,然后看它被谁读。
- 谁的行为会变? 只有内部后台,还是外部用户能看到?
- 怎么验证它生效了? 没有验收方式的变更,等于没有终点。
顺手做个习惯:每次口头变更之后,回到工单里补一行。一句话就够,比如「2026-09-26 追加:列表需按最后登录时间倒序」。这不是流程仪式,这是三个月后唯一的考古证据。
2. 让变更变便宜:expand-contract
「重命名一个字段」在传统做法里是道送命题,因为它必须一次性完成——改代码、改数据库、同时上线。而 expand-contract(也叫 parallel change)把它拆成三步,每一步都可以单独上线、单独回滚:
-- 第 1 步 expand:只加,不改,不删。旧代码依然能跑ALTER TABLE users ADD COLUMN full_name VARCHAR(100); -- MySQL 8.0.12+ 可为 INSTANT
-- 应用层进入双写期:写 name 的同时也写 full_name,读仍然读 name-- 第 2 步 migrate:分批回填历史数据-- 关键是 LIMIT + 循环,别让一条 UPDATE 锁住整张表UPDATE users SET full_name = nameWHERE full_name IS NULLORDER BY idLIMIT 1000;-- 第 3 步 contract:读切到 full_name,观察一到两个发布周期,确认没人在写旧列-- 只有到这时才删ALTER TABLE users DROP COLUMN name;三步的价值不在于技术含量,而在于它把一次「大爆炸」变成三次可回滚的小操作。任何一步出事,退回去就好,不需要「全组通宵 + 祈祷」。
同样的思路适用于 API:先加新字段(expand),让所有调用方迁移(migrate),确认旧字段无人使用后再移除(contract)。这也是为什么「加字段容易、删字段难」——加字段在 expand 阶段,删字段在 contract 阶段,中间隔着一整个迁移期。
3. 用开关解耦「上线」和「发布」
一个功能写到一半,需求方说「先上一半看看」。用分支硬扛,最后会得到一个活了三个月的 feature/new-list-v2 分支,和一个每次合并都像拆弹的下午。
用开关就简单得多:
if (flags.enabled('show-last-login', { userId })) { renderLastLogin(user);}代码可以先合进主干(部署了但不生效),再按白名单放量(灰度),最后全量——或者随时关掉。「上线」和「发布」变成两个独立动作,这是让变更便宜的最廉价手段之一。
代价是开关本身会积累。所以每条开关都该有一个删除日期,否则三年后你会拥有一套没人敢动的、由两百个 if 组成的考古现场。
4. 冻结窗口,和「大改小」
发布前一天不接受新需求,这不是官僚主义,是风险管理——大改动的代价不是平均值,是尾部。一个改动如果非要赶在发布前上,那就把它拆成几次小改动,分开发布。
反过来说也成立:如果一个变更没法拆分,那它多半不是一个「小地方」。 这句话本身就是一句很好的需求评审用语。
5. 用能跑的东西代替文档
需求歧义的解法不是写更长的文档(没人读),而是尽早给一个能点的东西。一个跑起来的丑原型,能在十分钟里暴露文档里三个月的歧义。
这其实也是在压那条变更成本曲线:把「发现要改」的时刻,从编码阶段提前到原型阶段。
写在最后
需求变更的笑话之所以流传得这么广,是因为它们全都指向同一件事:
软件的成本从来不在「写出来」,而在「改对」。
所以下次再听到「就改一个小地方」,别急着报价。先问一句:「这个字段,还有谁在读它?」
对方大概率会愣一下——然后你们就开始聊正事了。那一愣,就是这篇笑话集锦唯一真正的用处。
参考来源
- Software Engineering Economics — Barry Boehm
- Extreme Programming:关于变更成本曲线的争论
- ParallelChange(expand-contract 模式)— Martin Fowler
- Feature Toggles (aka Feature Flags) — Pete Hodgson
- Online DDL Operations — MySQL 8.0 Reference Manual
- Scope creep — Wikipedia
延伸阅读:本博客里也有两篇讲「说不清楚的话如何变成技术问题」的文章——《技术黑话词典》和《程序员估算时间的经典笑话》。