读前:这一章要回答什么
"先让它跑起来"错在哪里?每天写代码时,值得为设计多花多少时间?
第 2 章的结论是:复杂性由成百上千个小依赖、小模糊累积而成,单看每一个都无伤大雅。这一章顺着这个结论追问:既然没有哪一个小妥协是致命的,那么决定系统走向的,就是开发者面对每个任务时的心态。
作者把两种心态叫作战术式编程(tactical programming)和战略式编程(strategic programming),并给出了一个具体到百分比的建议。这一章没有技术细节,但它是全书的"动机篇":后面每一章都在教你怎么投资,而这一章说明为什么要投资、投多少。
- 3.1~3.2 节:作者描述战术式编程时,用的是"每一步都合理"的口吻。读的时候留意这个滑坡是怎么发生的;再把 3.2 节列举的几种投资分成"主动"和"被动"两类。
- 3.3 节:作者给了好几个数字(投资比例、回本时间、慢了多少)。逐个留意作者对每个数字的可信度是怎么说的,哪些是建议,哪些明说是个人观点。
- 3.4 节讲 Facebook 的那几段:注意作者在批评之后紧跟着替它说了一句公道话,并承认两种路线的公司都能成功。作者最后落在了什么理由上?
核心观点
战术式编程
首要目标是尽快让功能或修复跑起来。为了快一点,接受一点复杂性、一两个小补丁。每一步都显得合理,系统就是这样变复杂的。
战术龙卷风
产出极快、完全战术式的开发者。管理层有时把这种人当英雄;收拾残局的同事才是真英雄,却因此显得更慢。
能跑的代码还不够
首要目标是做出好的设计,而它恰好也能跑。系统里的大部分代码都是在已有代码上扩展出来的,所以你最重要的工作是让将来的扩展更容易。
投资心态
短期慢一点,长期快很多。投资有主动的(试几种设计、设想未来变化、写文档),也有被动的(发现设计问题就修,不绕过)。
10%~20%,小额持续
作者建议拿出约 10%~20% 的开发时间投资,以大量小额投资的方式持续进行。前期一次性大设计(瀑布)行不通。
技术债很少被还清
战术式编程是向未来借时间,还的比借的多;而且和金融债务不同,大多数技术债永远不会被完全还清。
投资是今天的事
"忙完这阵再清理"是一个滑坡:忙完这阵还有下一阵。拖得越久,问题越大、越吓人,越容易继续拖。
Your primary goal must be to produce a great design, which also happens to work.— 第 3.2 节
心智模型:两个方向相反的循环
两种心态的差别不在某一次决定,而在它们各自形成的循环。战术式的循环越转越紧,战略式的循环越转越松:
战术式循环最可怕的地方,是它很难跳出来:乱到一定程度时,清理要花好几个月,进度不允许;只修一两处又看不出效果(第 2 章说过,复杂性没有单个罪魁祸首)。于是最理性的选择似乎还是继续打补丁。作者说,一旦走上战术式的路,就很难再改。
投资多少、多久回本:一个玩具模型
作者在 3.3 节建议把大约 10%~20% 的总开发时间用于投资:一开始项目会慢 10%~20%,几个月内开始受益,不久之后就会比纯战术式至少快 10%~20%,此后投资就变成"免费"的了。作者估计回本期大约 6~18 个月,同时明确说:这只是个人观点,没有数据。
下面这个模型可以帮你体会这个论证的结构,但请记住它只是本站编的玩具,参数是随手定的,不是书里的数据,也不是任何测量结果。
战术 vs 战略:累计进度玩具模型
模型是怎么算的
每周:交付的功能 = (1 − 投资比例) ÷ (1 + 复杂度)。复杂度从 0 开始,每周增加"交付量 × 0.015 × (1 − 设计质量)",再减去投资带来的清理"0.15 × 投资比例 × 复杂度 ÷ (1 + 复杂度 / 0.6)"。设计质量 = 投资比例 ÷ (投资比例 + 0.04)。最后那个分母让清理在复杂度很高时变得吃力,用来模拟"乱到一定程度就很难清理"。所有常数都是为了画出合理的形状而随手选的。
玩一玩之后,你应该能看到作者论证的几个要点:战略队一开始一定落后;交叉点出现在"复杂度开始明显拖慢战术队"之后;推迟投资不只是晚几周受益,而是要先还清已经积累的复杂性。也请注意模型证明不了什么:交叉点在哪里完全取决于那几个随手定的常数。现实中它取决于代码会被改多少次、团队换人多快、开发者多久会忘掉代码细节。作者给的理由正是最后这一条:代码过几个月,写的人就忘了当时的想法,复杂的代码会让后续开发明显变慢。
一个原创例子:上线前一天的评审
明天上线,需求是"连接时开启 TCP keepalive"。connect() 已经有 6 个参数,项目里有 23 个调用点。两种改法:
Conn connect(const std::string& host, int port, int timeoutMs,
bool useTls, bool verifyCert, int retries,
bool keepAlive = false); // 新增,默认值保证 23 个调用点不用改
// 需要的地方
auto c = connect(host, 443, 3000, true, true, 2, true);
五分钟搞定,零风险,评审一眼就能过。但调用处那串 true, true, 2, true 谁也读不懂;下一个选项只能做第 8 个参数,而且要放在 keepAlive 后面,于是想设置它的人必须把前面所有默认值都抄一遍。
struct ConnectOptions {
std::chrono::milliseconds timeout{3000};
bool useTls = false;
bool verifyCert = true;
int retries = 0;
bool keepAlive = false; // 新增
};
Conn connect(const std::string& host, int port, const ConnectOptions& opts = {});
// 需要的地方(C++20 指定初始化)
auto c = connect(host, 443, {.useTls = true, .retries = 2, .keepAlive = true});
调用处自己说明了意思,以后加选项只加一个字段,没有调用点需要改。代价:要把 23 个调用点迁移过来,大约一两个小时,主要是机械替换,编译器会指出每一个漏改的地方。
这就是作者说的"小额投资":一两个小时,不会耽误上线,却让这一类改动从此变便宜。反过来,如果每次都选左边,这个函数的参数表会一年比一年长,直到没人敢动它。训练场里还有几道上线压力下的决策题。
创业公司、Facebook 与 Google
3.4 节讨论了战略式编程最常见的反对理由:我们现在等不起。作者的回应可以整理成下表(右列是本站的评注):
| 作者的论点 | 本站评注 |
|---|---|
| 代码库一旦变成意大利面,几乎无法修好,你会在产品的整个生命周期里付出高昂的开发成本。 | "成功后招人重构"的设想忽略了一点:新来的人面对的正是第 2 章说的未知的未知。 |
| 设计好坏的回报来得很快,战术式未必能让第一个版本更早发布。 | 这和上面的模型一致:如果交叉点比首发日期还早,战术式连眼前的好处都拿不到。 |
| 最好的工程师很在乎设计;代码库烂的名声会传出去,让招聘更难,进而让结构更差。 | 这是一个很少被提到的成本:代码质量影响的不只是开发速度,还有团队构成。 |
作者用 Facebook 作为鼓励战术式编程的例子(早年的口号 "Move fast and break things",新人入职第一周就往生产环境提交代码),说它作为公司极为成功,但代码库因此吃了苦头,后来口号也改成了强调稳固基础设施的版本。作者随即补充:Facebook 的代码大概并不比一般创业公司差,只是特别显眼。与之对照的是同时期成长起来、更重视代码质量和设计的 Google 和 VMware,它们的技术文化在招聘上几乎无人能敌。
作者的结论很克制:两种路线的公司都能成功;只不过在一个重视设计、代码干净的公司里工作,要有意思得多。
本站的项目 minikv 是一个故意按"先跑起来再说"的风格写成的 C++ 迷你键值存储。从第 4 章开始,你会在它身上一次次做被动投资:发现红旗、想几种替代设计、修在根上。做的时候可以留意一下:每个捷径在当时看起来有多合理。
边界与反方
10%~20% 是作者的建议,6~18 个月的回本期作者明说是个人观点、没有数据支撑;"代码质量差会让开发至少慢 20%",作者给的依据是"去问问在烂代码库里工作过的人"。图 3.1 也被标注为定性示意。这些数字可以作为讨论的起点,但不要拿它们当研究结论去说服别人。
作者反对前期一次性的巨大投资(试图把整个系统设计好),因为理想的设计是在积累经验的过程中一点点浮现的。本站补充:另一种常见的过度投资是为想象中的未来需求提前建好一切。作者说的"设想几种未来可能的改动",是为了检查设计能否轻松应对,而不是现在就把它们实现出来。第 6 章"有点通用"的说法会进一步界定这个尺度。
作者的论证建立在一个前提上:系统里的大部分代码是在已有代码上扩展出来的,所以最重要的是让将来的扩展更容易。本站补充:真正一次性的代码(只跑一次的数据修复脚本、确定会扔掉的原型)不满足这个前提,投资的回报也会小得多。但要小心,"确定会扔掉"的原型最终上了生产,是很常见的事。
这一章挑战的是"快速交付"文化,以及以产出量衡量人的考核方式:在这种考核下,战术龙卷风永远是最佳员工。作者在第 19.2 节还会指出,敏捷开发让人专注于功能,容易滑向战术式编程。本站补充:AI 编码工具让每个人都能以龙卷风的速度产出代码,投资心态因此变得更重要,而不是更不重要,见专题AI 时代读这本书。
以下不在书里,来自书中给出的读者讨论组。2023 年有读者主张"懒重构":容忍一定的技术债,新需求先用局部的权宜办法做掉;反正同一个模块多半还会来新需求,现在重构,过几天又被推翻,不如等代码实在不行了再整体重写。
Ousterhout 的回答是有条件地承认、但不推荐(2023-04):如果你真有纪律把这些尾巴都记着,并且回头收拾,这样做行得通;可是自认为有这种纪律的人里,只有一小部分真的有。而且这是一个滑坡:按这个逻辑,收拾的最佳时机永远在将来,因为将来总会知道得更多,也总会有更多补丁可以合并处理。这是对本章"以后再重构"那个循环的又一种说法。
另外,"如果以后和代码长期相处的是 AI,战略式编程还值得吗"这个问题,Ousterhout 2026 年在讨论组里回答过,见专题:在 AI 写代码的时代读这本书。
以下不在书里,来自 Ousterhout 在 X(原 Twitter)上的回帖。2019 年有人看完 Ousterhout 的一场演讲后指出:战术式与战略式的对比,和 Martin Fowler 的"设计耐力假说"(Design Stamina Hypothesis,2007 年 6 月)太像了。Ousterhout 回答说此前没见过这篇文章,但对方说得对:两者基本是同一个意思,而且 Fowler 早了十年;并说会在书的下一版里加一条说明(2019-03)。本站在 2021 年的第 2 版全文里没有检索到 Fowler 的名字。
Fowler 那篇短文值得对照着读:不做设计的项目起步更快,过了一条"设计回报线"之后被做设计的项目超过,和本章那张图是同一个形状。更有意思的是两个人对证据的态度也一样:Fowler 特意把它叫作"假说",说明这是推测,没有客观数据,而且很难检验;本章 3.3 节里 Ousterhout 也说那些数字只是个人看法。两位互不知情的作者得到同一个结论,算不上证明,但比一个人的经验多了一份旁证。
练一练
本章的题目大多是"上线压力下怎么选"。没有标准答案的地方,解析会说明另一种选择在什么条件下合理。
再加一道,取材于读者讨论组(对应上面"边界与反方"里的"讨论组"一栏):
本章共 12 道题,全部在训练场。
自测题
小结与读后一句话
- 战术式编程追求尽快能跑,每次引入一点复杂性;战术龙卷风是它的极端形态。
- 能跑的代码还不够:首要目标是好的设计,它恰好也能跑。
- 投资分主动(试几种设计、设想变化、写文档)和被动(发现问题就修在根上)。
- 作者建议拿出约 10%~20% 的时间,以小额、持续的方式投资;回本期 6~18 个月是作者的个人估计。
- 技术债大多永远还不清;"忙完再清理"是滑坡。两种公司都能成功,但战略式更省钱,也更有意思。
知道了要投资,下一个问题是投在哪里。第 4 章给出全书最核心的答案:把模块做深。
读后一句话:用你自己的话写下这一章最重要的一个观点,最好结合你最近的一次取舍。