专业书籍精读 · A Philosophy of Software Design · 第 1 章
A Philosophy of Software Design · Ch 1 · Introduction (It's All About Complexity) · Ousterhout · 2nd ed 2021
你手机上的 App 是不是越更新越卡、修好一个毛病又冒出两个新的?背后往往是同一件事:写它的人越来越看不懂自己的程序了。这本书的作者是斯坦福的老教授,写过好几个被广泛使用的软件。他在第一章只说一句话:做软件,说到底就是跟「复杂」打仗。
盖房子受钢筋、预算、物理定律限制;写程序几乎不受这些限制,想造多大就造多大。可程序员照样会被卡住——卡住他们的不是机器,而是脑子装不下。程序越长越大,各部分互相牵扯,改一处就得想清楚十处,最后谁都不敢动。
把「这会不会让程序更难懂、更难改」当成每次动手前的第一问,团队就能把系统越做越大还不失控。一句诚实的代价:这些原则靠判断,不是照着做就灵的配方,推到极端同样会把事情搞砸。
程序员最大的敌人不是机器,是自己脑子装不下的复杂;能扔的扔掉,扔不掉的关进盒子,并且天天收拾。
想看复杂度为何是根本约束、消除与封装两条路、瀑布与增量开发、红旗与代码评审,以及 Dijkstra、Brooks、Google SRE 的同调? → 切到精读版
你以为软件设计是在选框架、套模式、抠性能,Ousterhout 却把它收成一件事:编程几乎不受物理定律约束,真正的天花板是我们理解自己所造系统的能力;而这份理解力被复杂度一点点吃掉。对付它只有两条路——消除复杂度(让代码更简单、更显然),或封装复杂度(模块化,让人一次只面对一小块)。又因为软件可塑,设计不是开工前的一个阶段,而是贯穿整个生命周期的日常功课。
全书第 1 章,总纲。上承前言里的命题——计算机科学最根本的问题是问题分解(problem decomposition),即把大问题拆成能独立解决的小块;下启第 2 章「复杂度的本质」与第 3 章「战术 vs 战略编程」。作者是斯坦福教授,Tcl 语言作者、Raft 共识算法共同作者,书由他开设的斯坦福软件设计课 CS 190 提炼而来。现实里对应任何活得够久、越改越慢的代码库。
作者开篇说:编程是最纯粹的创造活动之一,没有材料、重量、物理定律的约束,只要想得出就造得出。那限制在哪?在我们理解自己作品的能力。程序随功能叠加而演化,组件之间长出微妙的依赖,复杂度一点点累积;终有一天,改一处要先读懂一大片,改动变慢,bug 变多,因为没人能把整个系统装进脑子。
举个典型场景(示意):一个两年的业务系统,加一个订单字段要改接口层、校验、存储、缓存、导出等多处,还得记住某个旧模块对这个字段有特殊处理——漏一处就是线上事故。这不是哪个人写得差,而是复杂度在无声复利。更好的工具(类型检查、IDE、测试)能挡一部分,但作者的判断是:工具帮得有限,真正的杠杆是更简单的设计——设计越简单,系统能长到越大才被复杂度压垮。
本章最重要的一步是换掉衡量设计好坏的标尺。性能、功能、开发速度都重要,但它们都受制于一件事:人能不能理解这个系统。所以作者直说:降低复杂度是软件设计里最重要的那件事。这把尺子贯穿全书,后面每一章的判据(模块要深、信息要藏、把复杂度往下沉、注释写什么)都是在回答同一个问题:这样做让系统更好懂、更好改了吗?
注意「复杂度」指的是读者体验到的难度,不是代码量。一个大系统如果结构清晰,可以并不复杂;一个三百行的模块,如果处处是隐含约定,可以复杂得要命。
面对复杂度,作者给出全书的两大对策:
两条路的优先级是有讲究的:能消除就先消除,消除不了才封装。封装本身也有成本——每多一层模块就多一份接口要学;如果封装得浅,等于把复杂度挪了个地方还加了一层包装。
作者拿土木工程对比:盖一座桥,几乎不可能造到一半推倒重来,所以先做完整设计再施工,即瀑布模型。很多软件项目照搬这套,但它很少在软件上奏效:大型软件系统复杂到无法在动工前完整想象,初始设计必然有很多问题,而这些问题要到实现进行到相当程度才显露出来。那时总体设计已定,开发者只能在原框架上打补丁,复杂度随之爆炸。
软件和桥的根本区别是可塑性(malleability):写到一半也可以大改设计。于是今天主流是增量开发:每次只设计一小块功能、实现、评估,在下一轮修正暴露出来的设计问题。这带来一个容易被忽略的推论:软件设计永远没有做完的那一天,开发者应当时刻把设计放在心上,而不是只在立项那几周。
作者说明了全书的两个目的:一是讲清复杂度的本质——它是什么、为什么重要、怎么识别不必要的复杂度;二是给出降低复杂度的技巧。但他坦白没有简单配方能保证好设计,所以给的是一组高层概念(如「类要深」「把复杂度往下沉」)和一套红旗(red flags):看到某种代码征兆,就该停下来想想有没有更好的设计。书末把这些原则和红旗汇成清单。
怎么用这本书?作者建议结合代码评审:读别人代码时对照这些概念,判断它是否符合、对复杂度有什么影响。因为在别人的代码里看出设计问题,比在自己的代码里容易得多——自己写的东西,脑子会自动补全缺失的上下文。最后一条提醒是分寸:每条规则都有例外,每个原则都有极限,任何设计理念推到极端,多半会走到坏地方。
表 1 · 面对一处复杂度:消除、封装,还是先放着
| 做法 | 做什么 | 得到 | 代价 / 风险 |
|---|---|---|---|
| 消除 | 去掉特殊情况、统一命名、合并重复概念 | 复杂度直接归零,所有人受益 | 常要重新思考问题本身,前期投入大;不是每种复杂度都能消除 |
| 封装 | 关进模块,只露小接口 | 多数人不用再理解它 | 封装得浅就只是挪位置,还多一层接口要学 |
| 先放着 | 能跑就行,以后再说 | 当下最快 | 复杂度复利累积,往后每次修改都在付利息 |
表 2 · 设计放在什么时候做
| 方式 | 适合 | 软件上的问题 |
|---|---|---|
| 瀑布:开工前一次设计完 | 改动代价极高、需求稳定的物理工程(桥梁、建筑) | 无法预见全部问题;问题暴露时只能打补丁,复杂度爆炸 |
| 增量:每轮设计一小块 | 绝大多数软件 | 若每轮只管「能跑」、不回头改设计,增量会变成持续堆补丁 |
判据很朴素:动手前先问「这会让系统更难懂、更难改吗」;能消除就别封装,必须封装就封装得深;把设计当作每个迭代都要做的事,而不是某个阶段的交付物。
面试被问「你怎么判断一个设计好不好」,本章给的是最短的回答:看它让系统更容易还是更难理解与修改,而不是看用了几个设计模式。架构评审里同理:一个新服务、一层新抽象、一个配置开关,都该先算它给全队增加的认知成本。Ousterhout 本人的经历也是注脚:他参与的系统横跨语言(Tcl)、操作系统(Sprite)、存储(日志结构文件系统、RAMCloud)与共识(Raft),其中 Raft 论文更把可理解性直接立为首要设计目标——这正是本章标尺在算法设计上的落地。
业界同调 · 公开出处
① 编程几乎不受物理约束,真正的天花板是人理解系统的能力。
② 复杂度随功能演化而无声累积:依赖变多、改动变慢、bug 变多。
③ 降低复杂度是软件设计中最重要的事,它是评判一切设计决定的标尺。
④ 两条路:消除(去特殊情况、命名一致)优先,封装(模块化、小接口)兜底。
⑤ 浅封装只是挪位置,封装要深,这是第 4 章「深模块」的伏笔。
⑥ 软件可塑,瀑布在软件上很少奏效;增量开发让设计成为每一轮的功课,永不完结。
⑦ 本书不给配方,给概念与红旗;最好的用法是配合代码评审,别人的代码更容易看出问题。
⑧ 任何原则推到极端都会变坏,讲分寸;本书是判断框架,不是实证定律。