读前:这一章要回答什么
写软件真正的瓶颈是什么?一本讲"设计哲学"的书,能用什么方式帮你突破它?
第 1 章很短,没有代码,也没有正式提出任何原则。但它定下了全书的基调:作者把软件设计的全部问题收束到一个词上,复杂性(complexity)。后面每一章,无论讲模块、异常、注释还是命名,最终都要回到同一个问题:这样做让系统更容易理解和修改了吗?
这一章还交代了两件对你读书方式影响很大的事:一是作者认为设计不是项目开头的一个阶段,而是贯穿软件一生的持续活动;二是这本书该怎么用,作者的答案是配合代码评审、学会认红旗。本站的训练场就是照着这个思路搭的。
- 第 1 章开头讲"两条路"的段落:作者给对抗复杂性分了两类办法。读完想一想,你最近一次"把代码改简单了",属于哪一类?
- 第 1 章开头关于瀑布模型和增量开发的段落:注意作者从"软件可以中途大改"推出了一个对个人很具体的要求,读的时候把它找出来(提示:和你的时间分配有关)。
- 1.1 节:作者对"怎么用这本书"给了三条建议:配合代码评审、学认红旗、保持适度。第三条会在后面多章的 "Taking it too far" 小节反复出现。
翻开书之前,你大概先看到了封面:上半部分是一大团蓝色、绿色的线,绕来绕去缠成一堆;中间一条黑色横带写着书名;横带下面,同样的线一根根垂直落下,互不交叉。书里没有解释这幅图。
以下不在书里,来自 Ousterhout 在 X(原 Twitter)上的回帖。2026 年有读者直接问这幅图是什么意思,Ousterhout 回答(2026-01):那些线代表"意大利面代码"。横带上方是一团乱;应用了好的软件设计方法之后,也就是黑色横带下方,它变得更简单、更干净。书名所在的那条横带,就是"设计"起作用的地方。
更早还有一次。2018 年书刚出版时,一位读者晒出新到的书,开玩笑说:看来我们都该努力写出封面这样的代码?Ousterhout 回了一句:不如努力写得像封面的下半部分(2018-05)。
本站补充:留意一个细节,上下看上去是同一批线,只是不再缠在一起。这和第 2 章的说法是一致的:好的设计不一定让系统做的事变少,它消掉的是线与线之间的纠缠,也就是依赖。
核心观点
瓶颈是"理解"
编程几乎不受物理定律约束,真正限制我们的是能否理解自己造出来的系统。复杂性随系统演化不可避免地累积,开发因此变慢、出错。
两条路:消除与封装
消除:让代码更简单、更显然(去掉特殊情况、一致地命名)。封装:模块化,让人一次只面对一部分复杂性。
工具有上限
好工具能帮忙,但只靠工具走不远;要想用更低的成本造更大的系统,得让软件本身更简单。
原则不是菜谱
书里的原则近乎哲学,不会直接告诉你最好的设计是什么,但能帮你比较几种方案、引导你探索设计空间。
适度
每条规则都有例外,任何想法推到极端都会出问题。好的设计是在相互竞争的想法之间取得平衡。
设计永远没有做完
软件可以中途大改,所以增量开发行得通;也正因如此,设计是持续的,每个开发者都该拿出一部分时间改进设计。
红旗 + 代码评审
红旗是"这里可能过于复杂"的迹象。看别人的代码更容易发现问题;看到红旗就停下来找替代设计,多试几种。
The greatest limitation in writing software is our ability to understand the systems we are creating.— 第 1 章开头
心智模型:两条路,和这本书的地图
先把作者给的两类办法画出来。左边是"消除":复杂性直接没了;右边是"封装":复杂性还在,但被关进了模块,大多数人大多数时候看不见它。书的主体(第 4~11 章)大多在讲第二条路怎么走好,但第一条路贯穿全书。下图把各章大致归了类,这是本站的整理,不是原书的划分,不少章节两边都沾。
下面用一个小例子把两条路走一遍:一个网络服务里的收包队列,生产者往里放包,满了就丢弃并计数。
// 网络线程(为简化,假设单线程)
if (count == kCap) {
dropped++;
} else {
ring[tail] = pkt;
tail++;
if (tail == kCap) tail = 0; // 回绕:特殊情况
count++; // count、head、tail 三者要保持一致
}
// 工作线程里还有一段对称的 head 处理,同样有回绕判断
调用方直接摆弄 head、tail、count 三个状态,还要处理回绕。每个碰这段代码的人都得把这些全部弄懂。
// head、tail 只增不减(uint64_t,实际不会溢出),
// 元素个数就是 tail - head,下标取模即可,不再需要 count 和回绕判断。
if (tail - head == kCap) {
dropped++;
} else {
ring[tail++ % kCap] = pkt;
}
回绕这个特殊情况消失了,需要保持一致的状态从三个减到两个。代价:读者得理解"只增不减的下标"这个约定,所以那两行注释不能省。
// 固定容量的包队列。满时 tryPush 返回 false,空时 tryPop 返回 nullopt。
class PacketQueue {
public:
bool tryPush(const Packet& p);
std::optional<Packet> tryPop();
};
if (!queue.tryPush(pkt)) dropped++;
下标、取模、容量判断全藏进了 PacketQueue,调用方只剩一行。复杂性没有消失,但只有维护这个类的人需要面对它。
两条路不是二选一:第三个面板的实现里,用的正是第二个面板的写法。先尽量消除,消除不了的再封装。至于"封装进一个类"什么时候值得、什么时候只是多了一层,第 4 章会给出判断标准。
这本书怎么读,本站怎么用
作者坦白说,书里的原则偏抽象,又很难找到"小到能放进书里、又大到能体现真实问题"的例子,所以光读书可能不够。作者的建议是把书和代码评审结合起来:读别人代码的时候,想想它是否符合书里的概念、和代码的复杂程度有什么关系。
本站按这个思路分成三层,建议每章按顺序走一遍:
| 层 | 做什么 | 对应作者的哪条建议 |
|---|---|---|
| 章节导读(就是这种页面) | 读原书前看"读前"和"读原书时留意";读完回来看核心观点、边界与反方,做自测题。 | 理解概念,并且知道每条原则的边界(适度)。 |
| 训练场 | 读代码、认红旗、在两种设计里选一种、先写再对照参考答案。答题记录进入间隔复习。 | 学会认红旗;像做代码评审一样读别人的代码。 |
| 项目 | 一个本站原创的 C++ 迷你键值存储,故意按"先跑起来再说"的风格写成。从第 4 章起,每隔几章有一个重构任务。 | 看到红旗就找替代设计,多试几种,在真实代码里体会取舍。 |
每一章建议这样走
- 读前(5 分钟):看本站这一章的"读前"和"读原书时留意",记住那三个问题。
- 读原书:带着问题读。本站不翻译、不复述原书,很多关键论证只在原书里。
- 读后(15 分钟):回来看核心观点、原创例子和"边界与反方",做自测题。自测错了,回原书找对应小节。
- 训练:去训练场做本章的题。答错的题 10 分钟后会再出现,答对的按间隔复习逐渐拉长间隔。
- 动手:有项目任务的章节,在 minikv 上做对应的重构;最后在页面底部写下"读后一句话"。
两个常见的误用。一是把红旗当成检查清单逐条打勾:作者说红旗是代码可能过于复杂的迹象,它提示你去找替代设计,而不是直接判死刑。本站补充:找了一圈发现原设计就是最好的,也是正常结果。二是"读完就觉得懂了":这本书的观点很少让人反对,真正难的是在自己的代码里、在赶工期的时候认出问题。所以本站把大部分篇幅给了训练题。
如果你读过 OSTEP,你已经见过"封装复杂性"走到极致的样子:进程抽象把 CPU 调度藏起来(OSTEP 第 4 章),地址空间把分页、TLB、换页藏起来。应用程序员几乎从不需要想这些,而操作系统内部的复杂性丝毫不少。用第 2 章的话说,这些复杂性被关在了几乎没人需要去看的地方。
另外还有速查页(全部红旗和原则,各配一个小例子)和几篇专题,例如APOSD 与 Clean Code 的分歧。
红旗与代码评审
作者认为,提高设计能力最好的办法之一是学会认红旗(red flag):一段代码可能比它需要的更复杂的迹象。书中会在各章陆续指出红旗,书末汇总了最重要的十几个。第 1~3 章还没有正式的红旗,第 4 章的"浅模块"是第一个。
When you see a red flag, stop and look for an alternate design that eliminates the problem.— 第 1.1 节
这句话里有两个容易忽略的要求。第一是停下来:红旗出现时你通常正忙着让功能跑起来,最自然的反应是"先这样吧"。第二是找替代设计,而不是在原设计上打补丁。作者还提醒,刚开始可能要试好几种方案才能消掉一个红旗,不要轻易放弃,试得越多学得越多;久而久之你的代码红旗会越来越少,你也会自己发现新的红旗。
为什么要配合代码评审?作者给的理由很朴素:看别人代码里的设计问题,比看自己的容易;评审还能让你见到新的设计思路和编程技巧。本站补充一个写评审意见的小模板,你可以在训练场的题目里反复练:
位置:文件和行号
现象:看到了什么(哪个红旗 / 哪种复杂性症状)
后果:以后谁、在什么情况下会因此多花力气或出错
替代:至少一种更好的设计(最好两种,并说明你的倾向)
这四行里最重要的是"后果":它把"我觉得不好看"变成"它会让以后的修改变难",这正是第 2 章给复杂性下的定义。
边界与反方
作者在 1.1 节明确说,任何设计想法推到极端都会出问题,并专门在几章里写了 "Taking it too far" 小节(如 5.9、8.3、10.9、17.3 节)。读这些小节和读原则本身一样重要。本站每章的"边界与反方"一节就是为此而设。
作者在前言里把这本书称为一本观点性的书(opinion piece),建议读者对书中的建议保持一点怀疑,并说降低复杂性这个总目标比书里任何一条具体原则都重要:如果某个想法在你那里并没有降低复杂性,就不必坚持用它。本站会在适当的地方标出作者观点与其他流派(例如 Clean Code)的冲突,但不替你下结论。
作者支持增量开发,理由是瀑布模型做不到一开始就想清楚大型系统,问题暴露时又只能打补丁。但增量的推论是设计永远在进行,而不是"边做边看、不必设计"。第 19.2 节会补上另一半:敏捷开发很容易让人只盯着功能,滑向战术式编程(第 3 章的主题)。
作者在开头承认好工具帮了大忙,但认为只靠工具有上限,必须让软件本身更简单。本站补充:写代码越来越快的工具,并不会减少需要被理解的东西;如果用它更快地堆出复杂性,反而会加速第 3 章说的战术式编程。专题AI 时代读这本书会展开讨论。
以下不在书里,来自书中给出的读者讨论组。2025 年有位教师问怎样教这本书的内容。Ousterhout 的回答对自学的人同样适用(2025-03):除非真的在写系统、亲身体会到设计选择的后果,否则很难学会。在 Ousterhout 的课上,学生"开窍"的时刻是交了第一个不算小的项目、拿到代码评审之后:评审指出哪里太复杂,说明这份复杂是违反了哪条原则造成的,再演示照着原则改完之后简单了多少。没有自己写的代码里的那些细节,也没有重写的机会,原则是体会不到的。伪代码不行,因为只有代码真的要编译、要运行时,那些细小的依赖才会冒出来。Ousterhout 说很希望有不必大量写代码就能教设计的办法,但还没找到。
这也是本站把重心放在训练场和重构项目上的原因。训练题只能练"认出来",替代不了"自己写、被人指出、再重写";所以项目里每个任务都请先自己做完,再对照参考解答。
同一个月,有读者把复杂性比作熵。Ousterhout 说自己常讲一句玩笑式的"软件热力学第一定律":软件系统总是朝复杂性增加的方向演化;复杂性一旦进来,就极难再降下去(2025-03,不在书里)。针对"只要随手保持代码整洁,好设计自然会浮现"的说法,Ousterhout 表示难以相信:整洁是局部的,找到合适的分解需要全局地想;有了设计之后,才能在各个部分上局部、独立地干活。这是对上面"增量开发 ≠ 不需要设计"的又一种说法。
练一练
第 1~3 章还没有红旗,题目以判断和设计选择为主。先自己判断,再看解析。
自测题
小结与读后一句话
- 软件的瓶颈是理解:复杂性会随系统演化不可避免地累积,设计的目标是延缓它、控制它。
- 两条路:消除复杂性(更简单、更显然),封装复杂性(模块化)。
- 软件设计贯穿整个生命周期,每个开发者都应该一直想着设计,并拿出一部分时间改进它。
- 原则用来比较方案,不是菜谱;每条都有边界,要适度。
- 学会认红旗,配合代码评审;看到红旗就停下来,多试几种替代设计。
下一章回答本章留下的问题:复杂性到底是什么?作者会给出一个实用的定义、三种症状和两个成因,后面全书都用这套词汇说话。
读后一句话:用你自己的话写下这一章最重要的一个观点,最好结合你的工作。