夜雨聆风学习资料网

ARTICLE · 1089112

影响 Matt Pocock 的一本书:AI 时代重读《软件设计的哲学》

影响 Matt Pocock 的一本书:AI 时代重读《软件设计的哲学》

影响 Matt Pocock 的一本书:AI 时代重读《软件设计的哲学》

进入 AI 生成代码的时代,写出一段可运行的代码,成本在持续下降。与此相对,判断代码该长什么样、半年之后谁还敢改,分量反而在上升。

斯坦福教授 John Ousterhout 把这个难题称为软件设计。他在 2018 年写成《软件设计的哲学》(A Philosophy of Software Design),当时还没有今天这样的 AI 编程工具。TypeScript 开发者 Matt Pocock 后来把这本书拆成一套 AI 编程规则。

本文根据 Uber 前工程经理 Gergely Orosz 对 John Ousterhout 的一次访谈编写。这本书的 GitHub 民间中文版放在文末,需要可以自取。

一、一个每年还写几千行代码的教授,怎么看 AI 编程

AI 会把软件工程带到哪里,这是 Gergely Orosz 抛给 John Ousterhout 的第一个问题。

Ousterhout 的回答很克制,先划出一条分界线。有些事看起来比较清楚,有些事没那么清楚。

比较清楚的一头是低层代码,也就是一个个具体函数、一行行具体语句怎么写的那个层面。AI 工具会让这部分生产变得更容易,也就是人们熟悉的自动补全。这件事会越做越好,而且未必产出草率的代码,质量说不定还相当高。

不清楚的那一头是高层设计,也就是系统该拆成几块、模块与模块之间怎么连接这一类判断。AI 究竟能在多大程度上接手设计工作,Ousterhout 没有答案。至少到目前为止,他在现有工具里看不到任何迹象,能让他相信模型做得到这一步。但他也提醒,不要低估这件事发生变化的速度。

Gergely 给这个判断加了一层画面。AI 工具非常擅长快速生成代码,能写短的,能写长的,能做补全,能生成各种骨架,从不说不,也从不喊累。假设每个工程师手边都有一台这样的生成器,软件工程会变成什么样,这是整场对话反复回到的问题。

其中的推论更值得琢磨。随着 AI 接下越来越多的低层编程,设计者在工作里做的事会越来越偏向设计本身。软件设计在开发者时间里的占比会上升。

这本来是好消息,紧接着却跟了一个坏消息。大学里几乎完全不教软件设计。学生在课堂上学得最熟的那些技能,恰好是最可能被 AI 工具接手的那一批。

这场对话的落点也不只在 AI。下面这些题目占了大半篇幅:

  1. 深模块与浅模块
    ,也就是接口简单、内部复杂的模块,和接口很宽、内部很浅的模块
  2. 错误处理
  3. 注释
  4. Ousterhout 为什么建议避开测试驱动开发
    ,也就是先写测试、再写实现的那种做法

它们最后都指回同一个问题,也就是复杂度,指的是那些让人读不懂、改不动、牵一发动全身的部分。

Ousterhout 敢下这个判断,是有来历的。他创造了 Tcl 脚本语言,发明了 Raft 共识算法,后者被 MongoDB、CockroachDB、Kafka 这类数据库和系统广泛使用。

Ousterhout 在 Sun Microsystems 工作过,创办过两家科技公司,现在是斯坦福大学计算机科学教授,《A Philosophy of Software Design》的作者。一年当中,他仍然尽量写上 5000 到 10000 行代码。

伯克利十四年,硅谷十四年,斯坦福又十四年

Ousterhout 年轻时就想同时做学术和业界。拿到博士学位时,他认真权衡过,因为两件事他都喜欢,学术界的创作自由、教书,以及做出真实可用的软件。写代码是他生活里的热情所在。

Gergely 自己某个阶段也想过这件事。他的父亲是大学老师,学术界里也有两边都做过的人。他关心的是,被称为科技世界的那一边,和学术界最大的差别究竟在哪里。

当时 Ousterhout 得到的建议是先做学术。理由很实际,从学术界转向业界,比反过来容易。他去了伯克利,在那里度过愉快的十四年。

时间久了,想做商业软件的那股劲越积越强。他最终放弃伯克利的职位,搬到硅谷,在 Sun 工作几年,又创办了两家只算小有成绩的公司。

再往后,回学术界的念头重新积累起来。他慢慢意识到,自己虽然喜欢做公司,但做教授才是本来的热爱。做了十四年创业公司之后,他拿到斯坦福的职位,又在斯坦福待了十四五年。

学术界和业界的差别,在他看来没有旁人想的那么大。两边都是在一支不大的团队里,做一件相对新的事情,都想做出真能跑的软件。哪怕在学术界,他也要求东西能用,而不是做完就丢的研究原型。

差别出现在人际上。业界要面对的人群广得多,销售、市场、融资的人,以及风险投资人。能接触这么多类型的人,是这份工作有意思的地方。

让 Ousterhout 不太舒服的是创业公司的压力,尤其是把自己说得比实际更大更好的压力。公司要活下去、要拿到融资,就会有很强的动力去包装、夸大、推销。人人都或多或少这么做,有人做得太极端,触了法律,惹上麻烦。

Ousterhout 自己开公司时,对外当然要推销,内部则尽量保持诚实。他不愿在内部听到谎话和包装,因为那会把公司置于风险之中。

他喜欢学术界的地方在另一面。做一个项目,有些成,有些不成。项目失败时,只需要说一句,这个想法不行,原因是什么,随后接着做下一件。

在公司里,项目不行的时候,没法说一句「对不住各位,明天关门」,只能想办法绕过去。这是他最不喜欢业界的一点。

两边的日子都过过之后,Ousterhout 给了这样一句:

两边都做过,反而让我在两边都做得更好。


二、战术龙卷风与技术债:产出最快的人,留下一地鸡毛

书里最著名的一个说法叫「战术龙卷风」,出现在全书最靠前的章节。Ousterhout 在书里是这样描述的:

战术龙卷风是一位高产的程序员,出代码的速度比同事都快,但方式是纯粹战术性的。要快速实现一个功能,没人比他更快。在有些组织里,管理层把战术龙卷风当成英雄。然而战术龙卷风身后留下一路废墟,收拾残局的往往是别的工程师。

Ousterhout 没法点名具体的人,就算能,也不愿意点。但职业生涯里他确实遇到过这类人,也相信任何有相当软件开发经验的人都碰到过、观察过,并且为此沮丧过。

在他看来,这是一种特定的人格类型。有人非常关注细节,属于收尾型,要求每件事都做到位,做一件就做完一件。也有人热衷于启动项目,把前面的百分之八九十做完,最后那百分之十到二十对他们没那么重要。

世界上本来就有更讲究的人和更随意的人。战术龙卷风属于太随意的那一类,不在乎身后留下大量残渣,这一点完全不困扰他们。这类人会一直存在,尤其因为有些组织把速度看得高于一切。

最典型的场景是创业公司。他半开玩笑地说,很多创业公司整队都是战术龙卷风。

Gergely 也见过这种人,也见过他们走之后留下来的麻烦。

同一个位置在别人嘴里有不同的名字。有人叫他 10x 工程师,因为管理层看到的只是他产出更快。有人形容这种做法很野,指的是快速凑合。再往前十几年,人们叫他们黑客,强调的是动手快。

Ousterhout 对 10x 工程师有另一套定义。Google 说到的那种 10x 工程师,他脑子里出现的是另一种人,和战术龙卷风相差很远。

那种能给出非常干净的设计、用极少的代码就把事情做成的人,才是他认可的那一类。他们每天写下的代码可能比同事更少,实现出来的功能却多得多,稳定性和可演化性也更好。

问题在于这个词太含混,每个人说的都不是同一件事。说话的人多半不太懂技术,看到的是持续的产出,很难同时看到产出背后的代价。Ousterhout 在现实里见到的情形多半是这样,一位不太懂技术的 CEO 说「我的 10x 工程师」,指的其实是战术龙卷风。

CEO 看到的是源源不断的产出,那个人从不说不,一直在建东西。至于这些产出背后积累了多少技术债,以及这件事正在拖慢周围多少人,离代码远的角色很难看出来。技术债,指的是为了赶速度而欠下、将来要花时间偿还的那笔账。

Ousterhout 把这一层归结为战略与战术之分。有人看重短期里的一切,有人更看重长期。他对设计的看法完全围绕长期,也能理解别人对同一个词有另一套用法。

同一件事在两种视角下会得出相反的结论,这一点 Ousterhout 自己也承认。管理层看到的是产出和速度,写代码的人看到的是明天要花多少时间收拾今天留下的东西。两种编程方式的分野,摆开来看是这样:

维度
战术式编程
战略式编程
关注的时间尺度
短期里的一切
长期
看重的指标
交付速度、代码数量
可演化性、稳定性
交付后留下的东西
技术债
更干净的设计
之后由谁来还账
周围的工程师
后来的自己
Ousterhout 站在哪一侧
不认同
他的设计观完全围绕这一侧

三、软件设计是一道分解题:自顶向下,还是自底向上

软件设计到底是什么,这个问题听起来简单,答案却不那么好给。Gergely 承认,这个问题问得有点天真,可它并不好答。他聊过的开发者里,有的只待过业界,也有的在考虑某天转去做学术,他自己某个阶段也动过这个念头。

现实是,如今拿一个现成的模板就能开始做应用,再过不久,还能直接让 AI 工具生成一整个骨架。

Ousterhout 给了一个很干脆的起点:

软件设计就是分解,把一个庞大复杂的系统,拆成若干个可以相对独立实现的小单元。

Ousterhout 做演讲时经常向台下的听众提问,请他们说出整个计算机科学里最重要的思想。他自己的答案是分解(decomposition)。这是贯穿计算机科学所有工作的一条线索,怎样把庞大复杂的问题拆开。

实现是另一件事,去处理一个个单独的部件。而在处理这些部件的时候,可能还要再做一次设计,把它们继续拆成更小的部分。

设计这件事没有菜谱。Ousterhout 希望有,照着几步走、按这个顺序做,就能得到好设计。现实里大体上有两条路径,各有各的坑:

维度
自顶向下
自底向上
从哪里开始
从顶上开始,把系统拆成自认为相对独立的若干部分
先挑出系统里需要的某个功能,整体长什么样还完全不知道
再做什么
把这些部分继续分解
先把这一块做出来,再做下一块
停在什么地方
剩下可以动手设计的东西
试着把各块拼起来,设计一层层叠上去

经验较少的工程师,或者身处不熟悉领域的人,更常用自底向上这条路。

七十年代 Ousterhout 读研究生的时候,关于设计该走哪条路有过一场大讨论,人们争得很凶。他自己的看法是,纯粹走哪一条都很难做到。真实的设计过程是两者的混合,先想大块,再想一些小块,做出来发现效果不好,扔掉,再做,就这样来回迭代。

应对复杂度,总体上有两条路。一条是让某些复杂度彻底消失,比如把对特殊情况的处理消除掉,一件事只要那么设计,就不需要特殊处理了。这一类办法最有力,效果也最扎实。

但复杂度不可能全部消除,于是有了第二条路,模块化设计,把复杂度藏起来。具体做法是把相对复杂的东西挪到一边,让某个人专门去解决它、承担它,系统里其余的人都不必知道这份复杂度的存在。

两条路都在帮忙。一条给出消除复杂度的思路,另一条让大多数人不必知道大部分复杂度的存在。

为什么 TDD 和设计模式,都不算软件设计

有一个现象值得解释。过去二十来年,大量注意力曾经放在离设计很近的架构话题上,测试驱动开发、设计模式、四人帮那一套、工厂模式,都在那个名单里。

到了业界,引用这些模型和想法的人明显变少。Gergely 怀疑,也可能只是行业里谈得少了,甚至想得也少了。

Ousterhout 给的解释很直接。潮流总有涨落,兴奋一阵随后退潮,这可能是部分原因。但在他这里,那些东西本来就不算设计,测试驱动开发尤其不算。

设计模式是设计的替代品,意思是不去设计某样东西,而是从货架上取一个现成的下来。用得上的地方,它当然有用。某些领域里,如果一个模式确实适用,那就该直接用。

但它覆盖的范围只是软件设计中极小极小的一块。设计要做的事,比挑六个模式多得多。


四、设计两遍:第二个想法往往更好

书里有一整章讲这件事,标题就叫「设计两遍」。他在里面写过这样一段话:

很遗憾,我常看到聪明人坚持实现脑子里想到的第一个想法,这让他们没能发挥出真正的潜力,也让和他们共事的人感到挫败。

同时 Ousterhout 补了一句,设计两遍耗费的时间,并没有大多数人以为的那么多。

这个现象是 Ousterhout 在教书时发现的。他在斯坦福和伯克利都当过教授,这是世界上顶尖的两所大学,两边都有极其聪明的研究生。他注意到,这两所学校的学生普遍带着一种不太好的工作习惯。

原因在于他们从小到大一切都太容易。做什么都是最拔尖的,高中时比老师还聪明,大学时大概和教授一样聪明,是班里第一。脑子里想到的第一个想法,就足以拿到漂亮的成绩。

从来没有什么理由逼他们多想一遍。久而久之,他们形成了一种信念,认为自己想到的肯定没错。

等到问题越来越难,这个习惯就失效了。在顶尖大学做研究,题目非常难,没有谁的第一个想法就足够好。Ousterhout 常常要花力气逼学生往下想,常用的一招是这么问他们:

假设我告诉你,你刚提的这个方案不许做。你要是做了,我就停掉你的经费,把你从研究生院赶出去。那你的第二方案是什么?你是能想出一个来,还是只能说,好吧,我最好换个导师?

有意思的是,Ousterhout 通常在心里已经有更好的做法时才会这么问。等逼着学生回去想过、再回来的时候,第二个想法总是更好。

Ousterhout 自己身上就有一个相当漂亮的例子,来自 Tk 工具包的设计,那是 Tcl/Tk 系统的一部分。当时他需要定下用图形控件编程的 API 的形态。

Ousterhout 花了大概一趟长途航班的时间,做了两版设计。先做脑子里想到的第一版,随后对自己说,假设这一版不要了,有没有什么做法和它截然不同?于是又做了第二版。

两版比较之后,他选了第二版。Tcl/Tk 后来流行,原因之一就是 Tk 的 API 非常漂亮、简单、有力,而它是第二个进入他脑子的东西。

这件事一直激励着 Ousterhout。做新东西的时候,他通常会逼自己多想一种做法。哪怕觉得其中一种很糟,也把它想出来,跟已有的那版比一比,总能从中学到一些东西。说不定还会发现,那个糟糕的做法并没有那么糟。

同样的做法在工程组织里也出现过,只是换了个名字。Gergely 在 Uber 的时候,团队用设计文档和 RFC,要求作者写下为什么这么做、准备怎么做。RFC 指的是一份把方案写下来、交给同事评审的文档。

过了一段时间,他们开始加一项,叫「取舍」,也叫「考虑过的替代方案」。规则是至少写一条,能列出多条被考虑过、最终没选的方案就更充分。

加上这一项之后,设计、想法、计划都变好了,讨论也变好了,因为不选某条路的理由写得清清楚楚。

人们往往先写几条明显行不通的方案,这倒也无妨。但写着写着有时会突然发现,这条路其实很简单,直接用它就行,或者把两个方案合并起来。

代价的问题,Ousterhout 也回应了。多花一点时间是真的,天下没有免费的午餐。但这是相当高层的设计,并不需要把另一个替代方案完整做出来,只是在高层面上做比较。

Tk 的设计,Ousterhout 大概花了几天工夫去思考和比较两个方案,而实现 Tk 花了一年,甚至更久。算下来,设计大约只占整个系统建造时间的百分之一二。如果它能换来更好的设计,回报远远超过那百分之一二。

把这件事放到更长的时间尺度上看,团队是会学习的。一开始总是直接跳进编码,觉得这样更快。等复杂度上来了,才学到当初应该多规划一点。

再过一阵,风向又摆到规划过度那一侧。随后循环重新开始,这种摆动没完没了。人们至今还在谈论瀑布模型,那大概是二三十年前的事,如今其实不算个问题。瀑布模型指的是前期一次把计划做满、后面照着执行的做法。但快速迭代的团队对过度规划仍然反感,那看起来像是说得太多、做得太少。

Ousterhout 认同这种摆动,也给出了自己的边界。设计的根本在于它永远涉及取舍,任何一个想法推到极端,结果大概都不好,多做几版设计同样可以推过头。

真正优秀的设计者和普通设计者的分别,在于知道怎么做取舍,怎么把不同的想法、不同的阶段组合起来、平衡好。这类东西大概只能从经验里学。两种做法都试过,都看清了各自的坏处,最后才会对取舍有一种直觉。

回到那条分界线上,Ousterhout 说得更直接:

把优秀的设计者和不那么优秀的设计者区分开的,是他们知道怎么做取舍,怎么把不同的想法、不同的阶段组合起来、平衡好。


五、深模块与浅模块:两者的区别到底在哪里

这是书里被读者记得最牢的一个概念。书里对深模块的介绍是,它指的是那样一段代码、一项功能,或者说一个模块,接口相当简单,内里却很有深度,藏着大量功能与复杂度。

浅模块正相反,它有很宽的接口,做的事却不多,甚至透明到几乎不隐藏任何东西。

浅模块的代价落在调用者身上。接口宽,就意味着使用它的人要先学很多东西,还得随时记住那些细节,认知负担一直挂在使用方这一侧。认知负担,指的是使用者脑子里必须一直装着、腾不出来的那部分东西。

两个概念并排放,差别一眼看得出来:

维度
深模块
浅模块
接口
相当简单
很宽
内部
藏着大量功能与复杂度
做的事不多,几乎不隐藏任何东西
使用者的负担
几乎没有认知负担,学起来容易
要先学很多东西,还得随时记住那些细节
代价落在谁身上
模块内部
调用者
在复杂度上的角色
对抗复杂度的杠杆
把复杂度推给使用方

书里强调,深模块对好的软件设计至关重要。Ousterhout 也解释过这个判断来自哪里,一切还是回到复杂度。书里所有内容都源自同一个问题,怎样消除复杂度,或者怎样管理它。深模块就是对抗复杂度的那个杠杆。

它起作用的方式很具体。模块提供一个非常简单的接口,使用它的人几乎没有认知负担,学起来很容易。与此同时,模块内部藏着大量功能和复杂度,外界完全看不到。

「深」这个说法想抓住的正是取舍。一边是接口的复杂度,另一边是模块里功能的多寡。设计要做的事,是用尽可能简单的接口,换到尽可能多的功能。这个取舍就是「深」字的全部含义。


六、通过定义来规避错误:错误处理的分寸怎么把握

书里有一章的标题叫「通过定义来规避错误」,讲的是错误处理。任何写过大量代码的人都知道,错误处理是复杂度的巨大来源。它指的就是那些特例,那些必须应付的古怪特例。错误处理很容易给软件压上沉重的负担。

Ousterhout 不断问自己的是同一件事,能否减轻这份影响。有些情况必须处理,有些异常根本躲不掉,它们是系统固有的,只能应对。也有些情况没那么重要。异常,指的是程序跑到一半出的岔子,需要单独写一段逻辑去应付。

那一章想论证的是一件事。抛出的异常更多,并不等于代码更好。有时候异常确实必要。

但 Ousterhout 见过不少设计者以为,从一个类里抛出越多的异常,自己就越像一个好程序员,显得更谨慎、更细致。

问题在于,每抛出一个异常,都是在把复杂度压给这个类的使用者。减少抛出的异常数量、减少制造的特例,系统复杂度就会下降。

书里把这句话说得很直白:

每抛出一个异常,都是在把复杂度压给你的类的使用者。

那一章里他举了不少例子。其中有些只需要对系统设计稍作改动,整整一类错误就消失了,根本不可能发生,也就没有错误需要处理。

Ousterhout 特意加了一句提醒。这种情况只是偶尔出现,而且必须非常小心。在他教的软件设计课上,每一届都肯定有学生误解这一点。

第一个项目里,学生的异常处理几乎为零。他们做的是一台机器可能崩溃的分布式服务器,连网络错误都不检查。他问,这里没有任何错误检查,如果某个系统崩了会怎样?

学生回答,我们把那些错误规避掉了。他说,不对,错误还在那里,学生只是忽略了它们,这样做不行。这一章很容易被带到错误的方向上去,它像调料,放一点点,菜的味道很好,放多了,整锅就毁了。

Gergely 对错误处理这个话题也有话要说。错误、异常、事情出岔子,这类话题在工程团队里谈得实在太少。

Ousterhout 做设计时会去翻故障复盘,很多次事故向上追溯,原因都落在错误处理上。故障复盘,指的是事后把事故原因一步步倒推出来的那份记录。在 Uber 是这样,在别的公司也一样。错误从某个系统传出来,他们却把它错误地映射成了成功。

这类边缘情况很多,误解了错误,或者误解了非预期的响应,本质上是同一件事。当时他们很难把问题定位清楚,也试过一些办法,比如把响应的映射做得更好,上白名单、上黑名单。

更常见的情形在规划阶段。人们总是偏乐观,讲的全是系统怎么运作、彼此之间怎么通信。

Ousterhout 很少见到有哪次规划会议,大家坐下来认真想,什么情况可能出问题,出了问题怎么捕获,怎么恢复。异常处理不在人们心上,往往是等人发现潜在问题之后,战术性地顺手处理掉的。

Ousterhout 给过一条通用建议,落到操作上可以拆成三步:

  1. 设计一个模块、思考要透过接口导出哪些特例和异常时,先去想调用方拿到这些异常之后要怎么处理。
  2. 把自己放进使用者的位置,看调用方实际上有几种处理方式。
  3. 如果本来列了 10 种不同的异常,而使用者只有两种处理方式,就把它们归并成两种,不留下十种。

关键是从调用方的立场去想这件事。

同理心与视角切换:把自己放进使用者的位置

顺着这条建议往下,他讲到了自己认为优秀设计者最重要的特质之一,有能力切换心态,从非常不同的视角去看同一件事。

设计一个模块的时候,他脑子里装着这个模块的全部细节。但他可以切换心态,去想这个模块的使用者,随后意识到,使用者不该对这些细节有任何感知。

使用一个模块的时候,他也不该利用自己可能知道的内部情况,只该用接口里提供的东西。某一刻沉浸在这个视角里,下一刻把它完全放下,换一个截然不同的视角去看,这种能力非常强大。好设计就是这么来的。

Gergely 把这件事翻译成了一个更常见的词。具备同理心,能把自己放进另一个角色的位置,不管那个角色是客户,还是将来接手这个模块的另一位开发者。这种能力会让人成为更好的软件工程师。

Ousterhout 正要说出同理心这个词。这套能力在社会情境里的价值,和在工程情境里的价值一样大,都是从别人的视角看事情。

Ousterhout 喜欢计算机科学的原因之一就在这里。人们觉得做计算机的是一群书呆子,可是计算机系统里用的很多思想,在社会系统里也有很有意思的类比。

Gergely 在自己的职业生涯里也验证了这一点。从学写代码,到上大学,到拿第一份工作,他最初以为难的是编码。

但 Gergely 聊过的每一位开发者、每一位工作超过一定年限的工程师,都会讲到同一个话题,软件工程和编程里最难的部分是人。

语法学会了,调试也会了,回头再想,做过的项目里最难的是哪一个,最大的麻烦是什么?答案往往是沟通不畅,彼此误解,需求说明出了问题。当然也有线上故障,可根本原因通常是没有预料到这件事会发生。


七、白板辩论法:让所有人的理由都上墙

在动手造一个系统之前,人们通常会把计划写下来。Amazon 以写作文化出名,一群人进到会议室里,先安静读完一份计划,再开始讨论。有的公司用共享文档,像 Figma 这样的公司用自己的工具画图,直接在图上评论。

这些做法的共同点是,在动手造系统之前,先把计划讲清楚,再让别人的意见进来。

问题也在这里。计划摆出来之后,多数人倾向于点头通过,真要有人站出来认真挑毛病,需要另外一套办法。

Ousterhout 自己的做法更朴素。他参与过的所有项目都会做设计评审,形式比较随意,没有冗长的书面文档,就是聚在一起谈想法。设计评审指的是把方案摊开来,让一屋子人当场挑毛病。做创业公司的时候也一样。能拉来多个头脑想同一道题,结果一定比一个人想更好。

这件事对聪明人来说另有一道坎。他们人生中很长一段时间所处的环境,让他们无法指望从别人那里得到有用的输入,成绩全部由自己决定。

等到在最高层面和一群同样聪明的人一起工作时,两个头脑显然胜过一个。所以他要劝学生放下过去那套习惯。

Ousterhout 自己完全赞成这种碰撞,要提防的只有一件事,别掉进分析瘫痪,得找到合适的场合、合适的投入时间。分析瘫痪指的是反复讨论、迟迟不肯落地的状态。把握这个度是一门手艺。

Ousterhout 直说自己喜欢白板,也喜欢开面对面的会。Gergely 补了一句,这类讨论最吸引人的地方,是人能当场拿到一个可以立刻试的办法。

Ousterhout 提到另一个变化。远程办公爆发过一阵,现在又有些收缩。2020 年以前,那些势头最猛的创业公司里到处是白板和可擦写板。

一个人正做着自己的事,旁边有人说了一句话,他停下来说等等,走到白板前就开始画。画完可以擦掉,有时留着,有时拍张照。触发点可以是任何事,可能只是有人听岔了。

创业公司试图用数字化工具复制它,但面对面把想法摊开来这件事有种特别的东西。聊软件这类话题时,方框和箭头确实管用。

Ousterhout 在职业生涯里还发展出一套用白板解决复杂问题的办法。这些问题未必是技术问题,也可能是公司里的管理问题。

Ousterhout 观察到,人们开会时常常各说各话,反复重复同样的主张和反对意见,绕来绕去,永远得不出结论。

遇到这种情形,他会站到白板前,把所有支持的理由和反对的理由一条条列出来。规则有三条:

  1. 任何自认为合理的理由都可以提,没有人能要求删掉它,说它站不住脚。
  2. 每条理由都上白板,所有人的理由都算数,每个人都有权贡献。
  3. 但不许重复白板上已经写着的理由。

理由全部摆在那里,所有人都看得见,讨论会自然地走到一个点,所有人都没什么可说的了。会议就此缩短。

之后他会做一个意向投票,这一步常常出人意料。意向投票指的是每个人按自己看重哪几条理由来加权,最后站 A 还是站 B。人们刚才还争得不可开交,旁观者会以为分歧大得无法调和。

做意向投票时,每个人按自己认为合适的方式给这些理由加权,自己决定看重哪几条、不看重哪几条,最后选 A 还是选 B。结果几乎总是一个非常强的共识。

Ousterhout 会坐在那样的会议里,心里想着分歧这么大,一投票出来,全场一致同意。Gergely 听到这里,说想找机会试一试,关键就在那一块白板。

Ousterhout 补充了这套办法的适用条件。它更适合用在一种环境里,那里的人总体上都聪明、讲道理、目标一致,比如创业公司,或者一支工程团队。

放到国会里,放到对立的政党之间,行得通吗?行不通。好在大多数人的工作并不在那样的环境里。

先设计,还是先做原型:两派主张怎么选

设计上有两派主张。一派认为前期少做设计,甚至完全不做,先造点东西出来,让代码自己决定方向。Gergely 问他站哪一边。

Ousterhout 站的位置更靠近设计这一侧。设计贯穿整个开发过程,前期要做,编码时要做,测试时要做,修 bug 时也要做。

两边的主张摆在一起是这样:

问题
两派的主张与 Ousterhout 的立场
前期要不要做设计
一派主张少做、甚至完全不做,先造点东西出来,让代码自己决定方向
Ousterhout 站哪一边
更靠近设计这一侧
设计要做多久
贯穿整个开发过程,前期、编码、测试、修 bug 时都要做
这是不是瀑布模型
不是。软件系统太复杂,人无法预测自己的设计决策会带来什么后果
前期设计图什么
先立几个假设再动手,这一点非常重要
要有什么准备
真正上手之后计划会散架,会发现大量问题,一旦发现就准备修改
完全不做设计的代价
把时间浪费在大概率没用的代码上
什么时候可以不设计
人实在太年轻、太没经验,对怎么做设计毫无头绪,只能先写一点从代码里学

Gergely 在这里讲了一个他记了很多年的比喻。造软件有点像身处一片地形之中,要行军到某个位置。目标看得见,地形却是未知的。

有时走过去一路和预想的一样。有时走着走着,一块巨石不知从哪儿出现,翻过去,又来一块。有时走着走着,一颗地雷在面前炸开,突然多出一个大坑,而事先并不知道这里是雷区。

Gergely 对这个比喻很有共鸣,因为很多事取决于未知的未知有多少。做法也因此截然不同。做的技术栈或产品是自己熟悉的,和完全陌生的,是两回事。用的是刚发布的新技术,或者某个框架的测试版,可能有问题也可能能用,又是另一回事。

Ousterhout 自己的经验更偏向熟悉带来的好处。并不是运气好碰上了一片平地,而更可能是,以前走过这片山脉,所以知道哪些路能走。

比如要为一个新设备写驱动,而此前已经为五个设备写过驱动,那就大致知道主要问题在哪里,过程也会顺利、可预测得多。反过来,身处全新环境时,他说也许只是自己运气不好,但他从没有一次在新环境里走得顺利过,太难预测。

Gergely 也承认自己没有。勉强算得上新环境的情况,其实也不算真新。比如框架出了新版本但没大改,或者语言发布新版本、向后兼容,重要的部分都没动,只是多了一个新特性。这类情况有点假,跟以前差不多。


八、Ousterhout 和《Clean Code》的三处分歧:短方法、TDD 与注释

Ousterhout 和 Robert Martin,也就是人们常说的 Uncle Bob,在网上有过一次很有意思的讨论,围绕《Clean Code》这本书。两人对不同章节各自写了看法,其中几处意见相左。这三处分歧恰好能把 Ousterhout 的设计观照得更清楚。

这本书的中文版是《代码整洁之道》,人民邮电出版社 2020 年出的第 2 版。

三处分歧摆在一起,轮廓就清楚了:

分歧
Robert Martin 的主张
Ousterhout 的回应
短方法与深模块
推崇短方法,一个方法只做一件事,三行的方法优于五行的方法
方法长度本身并不最重要,重要的是深度
测试驱动开发
先写测试,再写让测试通过的代码
和设计冲突,它鼓励极小增量的设计
注释
注释越少越好,代码本身应当说明一切
代码做不到说明一切,注释最重要的位置是接口

分歧一:短方法与深模块

第一处是短方法,以及一个方法只做一件事。Robert Martin 非常推崇这一点,理由是它会带来更干净、职责更清晰的代码。

Ousterhout 的看法差得很远。设计的核心是取舍,任何一个想法推到极端,结果都不好。在他看来,《Clean Code》在很多地方就做到了极端,这是他最大的总体担忧。

方法长度是其中之一。两人其实都同意,过长的方法比短方法更难理解和处理,所以短本身有价值。但对他来说,方法长度本身并不最重要,重要的是深度,也就是把复杂度藏得够不够深。

《Clean Code》把短当成了没有上限的绝对善,越短越好。按它的说法,三行的方法优于五行的方法。

这样做的问题在于,也许那一个方法确实稍微好懂一点,但方法变短,方法的数量就变多,接口也随之变多。到最后,接口带来的复杂度反而让整个系统比原来更复杂。

更让他介意的是另一种情形。如果方法彼此纠缠,多个短方法互相依赖到不看着另一个方法的代码就读不懂其中一个,按那种风格,这也没有关系。

在他看来这没有任何好处,只会让事情更糟。如果两件事关系确实紧密,把它们合在一起反而更好。

Gergely 听到这里做了个小结。方法是短的、只做一件事的,这样的方法可以有很多个;同样也可以把它们合起来,做成更大但更合理的方法。两边都成立,区别只在于边界划在哪里。

这个概念恰好抓住了其中的取舍,也就是深度。要的是功能多,接口相对简单。有了这个标准,就不会朝任何一个方向跑偏。而像单一职责原则、只做一件事原则,它们只朝一个方向使劲推,没有任何边界,最后一定会出问题。单一职责原则指的是一个模块只负责一件事。

Gergely 顺着这个思路想到了业界在微服务上走过的路。他在 Uber 时,公司有五千多个微服务这件事被到处宣扬,他们手里确实有大量非常小的服务。微服务指的是把系统拆成许多各自独立部署的小服务。

几年下来,至少在他所待的支付领域是这样。小服务启动一个很容易,于是接二连三地出现,每个只做一件事、两件事或者几件事。

过了一段时间他们发现,维护起来真的太难,很难知道哪块逻辑住在哪里,大量的来回来去。于是开始把它们合并,做出中等规模的服务,各自负责一个领域。

一个极端听起来总是很好,一开始也确实有它的好处,时间一长就不实用了。反过来同样成立,做一个什么都干的巨型服务也不好。

Uber 以前有过这么一个东西,它叫 API,什么都干。后来它被拆成更小的部分,随后又合回来,变成按领域划分、规模合理的若干服务。

这样人脑子里才装得下,才知道这里有哪些部分,能走进去理解其中某一个部分。所以存在一个度的问题,什么时候这样划分才合理。

Ousterhout 承认两边都会犯错,但如今人们往往在过度分解这一侧犯错。在他看来,《Clean Code》主张的就属于过度分解,而它带来问题。

Ousterhout 常跟学生讲,很多时候把东西合起来,反而能让它更深。如果两件事关系本来就紧密,把它们合进同一个类、同一个方法、同一个模块或者子系统,会得到功能叠加、整体 API 更简单的结果。

这样也不会留下两个彼此有大量依赖的独立事物,那种情况很糟糕。当然,合并也可以做过头,一切都是取舍,但把单元做大一点,往往能让事情变好。

分歧二:测试驱动开发

第二处分歧是测试驱动开发。Robert Martin 非常推崇用 TDD 写代码,先写测试,再写让测试通过的代码。这个方法在 2000 年代初很流行,后来有点过时。他自己不推崇 TDD,理由很直接,它和设计是冲突的。

测试本身他并不轻视。他喜欢单元测试,做的每件事都写单元测试,认为它们不可或缺。作为负责任的开发者,就该写覆盖率很高的单元测试,这一点两人有共识。

分歧在于开发过程围绕什么组织。他希望开发过程以设计为中心,开发中的每一件事都应当围绕如何得到更好的设计来安排。

TDD 与此冲突,因为它鼓励极小增量的设计。它的循环是这样:

  1. 写一个测试。
  2. 实现功能,让这个测试通过。
  3. 再写一个测试,再实现。

整个过程中,没有任何一个环节鼓励人退后一步。没有人去打量整体任务和全局图景,也没有人去想这些碎片该怎样拼在一起。最后落到手上的,是十个各管一段的方案,而不是一个能一次解决十个问题的干净架构。

这样做的风险是产出一种极端战术化的开发风格,最后得到的是一团糟。

两人为此来回争论了很久。他认为 Bob 推崇 TDD 的主要理由是,它能保证测试被写出来,因为测试必须先于代码。

这一点他也同意,测试应该写。但对方没能说服他,先写测试除了保证测试被写出来之外还有什么特别的好处。他看不出任何理由能让设计变好,却能看出很多理由会让设计变坏得多。

Ousterhout 抛出了一个更根本的问题。开发的单元应该是什么?开发总是按一块块工作推进,这些块又该多大?他的答案是,这个块应该是抽象,而不是单个测试。测试的粒度太小了。抽象指的是把一类问题的共同部分拎出来,用一套统一方案覆盖住的那一层。

想得足够大,才能权衡取舍,才能给出一个比较通用的方案,一次解决很多问题。他还顺带提到了设计里最重要的要素之一,尽可能把自己推向通用,避免特化。

而 TDD 的做法鼓励人为通过某一个测试做非常特化的实现,而不是去想那个能一处解决很多问题的通用方案。

Gergely 回忆了自己见识 TDD 的一次经历。有一次他们做 TDD 集体编程,键盘在几个人手里传递,一个人写测试,另一个人让测试通过。

Gergely 感觉有点做作,因为一旦多写了点不需要的代码,旁边的人就会说,别写那个,只要让测试通过就行。

当然他相信有些场景它是有用的,也许在更正式的做法里,也许用在以扩展既有代码为主、业务逻辑很重的地方。但它慢慢变得不那么流行,大家不再多谈,也许是有原因的。

Ousterhout 也给出了一个自己会推荐先写测试的场合,修 bug 的时候。他会主张先写一个能检出这个 bug 的测试,再去修。

不过他承认自己是作弊的,先写修复,再写针对修复的测试,随后把修复回退掉,确认测试会失败,这样他就知道了。

分歧三:注释

第三处分歧是注释。Uncle Bob 的观点是,代码里的注释应该越少越好,代码本身应当说明一切。

Ousterhout 的回应留了余地。如果代码真能完整说明自己,那当然很好,他不会反对。但依他看,代码做不到,也永远不会做到。有太多东西是代码本身描述不了的。

他把这个判断说成了一句:

如果代码真能完整说明自己,那当然很好,我不会反对。但依我看,它做不到,也永远不会做到。

他们讨论过具体的例子。对方指着一处代码说,看,这段代码不需要注释,对吧?他回答,这里人们会问的五个问题,代码里一个都没有给出答案。对方对此就有点含糊其辞了。

在他看来,对方对注释有非常强的偏见。其中一个论点是,人们被过期注释误导所浪费的时间,多于因为缺少足够注释而浪费的时间。这显然和他的经验不符。

过期注释偶尔确实存在,但很少见,而且即便过期,里面通常也保留了有用的信息。

另一个论点是,如果项目只由几位开发者经手,他们不需要注释,因为大家都记在脑子里。这一点他不能同意。也许对方的脑子比他的好,但他没办法把所有细节都记在脑子里,代码写完几周之后他就忘了。有太多东西是代码本身拿不到的。

Gergely 问起他自己的注释做法。首要原则是,注释应该告诉读者代码里看不出来的事。外面有大量糟糕的注释,只是把代码里显而易见的东西重复一遍,这种场合完全不需要注释。

在他的课程项目里,他经常把学生的注释删掉,告诉他们,这条注释只是把代码重复了一遍,不需要它。注释最重要的位置是接口。

接口之所以存在,是因为设计者不希望别人为了使用它去读那个东西的代码,只想让人看接口。而一个模块的函数签名,根本不可能承载使用者需要的全部信息,所以这里的注释极其重要。

排在其后的是类成员变量的注释,这需要相当详尽的说明。至于方法内部,他通常不太需要注释。如果知道这个方法要做什么,代码本身通常已经说得比较清楚了。

Ousterhout 会在事情棘手的地方写注释,或者写下那些当初发现 bug 时才明白的反常之处。但他的方法经常一条内部注释都没有,只有接口注释。

Gergely 把话题推到当下。AI 工具生成越来越多的代码,而且经常整段整段地生成方法,这场关于注释的争论可能会重新热起来。

Gergely 注意到,当他让某个 AI 助手生成一段做某件事的代码时,它写完了代码,还常常顺手加上行内注释,解释这段代码的作用。这种情况下,注释反而帮了他,因为这些工具会胡编,他确实想弄清这段代码的实际行为。

至于模型为什么这么做,也值得追问。它们多半是在网上现成的代码上训练的,开源代码,或者来源不明的授权,大概是在照抄见过的写法。不过通常情况下,他同意 Ousterhout 对注释的判断,只是在这个场景里,注释也许不是坏事。

Ousterhout 对这一点很谨慎,他说这只是一句猜测,也可能是在为自己的坏习惯找理由。但他确实发现,AI 工具在某种程度上弥补了注释的缺失。

Ousterhout 最近在 Linux 内核里工作,做一个新的网络传输协议。Linux 内核不是糟糕的软件,但在他看来注释严重不足。他花了很多时间试图弄清 Linux 内部的运作、接口的形态,以及自己该从哪里接进去。

ChatGPT 成了他最常用的帮手。它在回答 Linux 内核问题上能力强得惊人,那些问题他用 Google 搜索或者别的办法都查不到答案。它不总是对的,有时会胡编,但经常是对的。即便错了,通常也能把他带到正确的附近,帮他找到该去哪儿查。

Ousterhout 认为 AI 工具也许能帮上忙。可即便如此,如果 Linux 的开发者当初稍微花一点点时间写上注释,事情会比用 ChatGPT 去猜容易得多。

所以他不认为对注释的需求会彻底消失,也不该把 AI 工具当作不写注释的借口。现在还不到那个时候。


九、设计能不能教:一门课、一本书和一个没做完的项目

《A Philosophy of Software Design》这本书的来路,要经过他在斯坦福开的那门课。Ousterhout 喜欢写代码,是少数至今仍写大量代码的教授之一,尽量每年写上 5000 到 10000 行。

Ousterhout 也喜欢设计,认为设计是人类历史上出现过的最惊人的创造形式之一,是一个迷人、优美、充满挑战的领域。

Ousterhout 经常琢磨什么是好的设计,怎样才能设计出好的软件,有哪些技法。时间久了,他震惊地发现,没有人教这门东西。

除了他后来在斯坦福开的那门课,据他所知,全世界没有任何一门课以软件设计本身、而不是工具或流程,作为课程的主要元素。

这件事在他心里硌了十几年。回到斯坦福之后,他手里的业界经历更多,软件开发经验更多,想法也更多,于是决定不管结果如何,先试着开一门课。开课逼着他梳理自己的想法,试着把它们归纳成几条简单的原则。

Gergely 从招聘的角度补充了一点。科技公司通常不会在面试应届生、或者只工作过几年的人时考察软件架构,一般只考编码,随后才是复杂系统的架构与设计。

原因是,人得在行业里干上几年,才会对这件事有感觉。Gergely 感觉 Ousterhout 把这件事倒过来了,用一门课尝试证明,设计是可以教的,哪怕对象是刚学会写代码的学生。

这门课怎么上

Ousterhout 借用了高中学英语写作的模式。那时的作文课是这样的,拿到一个题目,写一篇东西,老师逐句批注,学生重写再交,可能要改好几轮。

其中关键的地方有两点:

  1. 拿到反馈,有人愿意批评自己的作品。
  2. 重做,把反馈吸收进去。

正是在重做的过程里,学生才真正把反馈和那些概念内化。

这门课持续一个学季,学生大致分三个阶段做出一个像样的东西,中间穿插大量的代码评审。所有团队的每一行代码,他都会读。

前两个项目恰好是 Raft 共识协议,分两个阶段做。Raft 指的是一套让多台机器对上同一份数据的算法。事实证明,它里面有一些非常有意思的设计问题,学生常常被绕进去,犯下大量错误。

整个过程从零开始,一开始 Ousterhout 不给任何线索、提示和结构,学生必须完全靠自己动手。这是他们有生以来第一次做这种事,既好玩又吓人。

接下来是密集的评审。学生在课上互评,读同学的项目并给反馈。他会读每一行代码,和每个团队花大约一个小时。

学生写的代码大约两三千行,通常能收到他 50 到 100 条评论。之后他们回去重做。学生的顿悟时刻就出现在这里。

拿到反馈的时候,他能指出代码很复杂。代码复杂这件事人们一般都知道,但他能指出它为什么复杂,是因为没有遵循课上一直在讲的那条原则。他也能指出,如果应用那条原则,可以怎样把它变简单。

第二个项目交上来,比第一个好太多。学生自己也很兴奋,因为他们能感觉到自己的力量,能感觉到自己让这个东西变好了很多。

Ousterhout 原本担心学生会怎么看这门课,结果学生的体验极其正面。一个学季下来,可以看出他们思考软件的方式发生了很显著的变化。

学季结束时他会提醒学生,等进了公司,会发现公司里的人不懂很多东西,甚至包括比他们资深得多的工程师。接下来他们会讨论该怎么应对,因为作为新人,未必有多少能力去改变一家公司。

Ousterhout 完全相信设计是可以教的。经验仍然需要,上完一个学季的课不会立刻成为一流程序员。但这门课能把这个过程启动起来,给人以一种思考软件的新方式,是别的课上从没想过的。

这句话他说得很肯定:

我完全相信设计是可以教的。经验仍然需要,上完我一个学季的课,你不会立刻成为世界级程序员。但我认为这门课能把这个过程启动起来,给你一种思考软件的新方式,是你在别的课上从没想过的。

Gergely 在这一点上补了自己的经历。他刚入行的时候,一位资深开发者交代任务,说这件事要做,规划已经做完了,技术选型也定下了,让他负责实现。他当时就有不好的预感,但还是照做了。

事情越变越糟,越来越复杂,问题出在某个自研数据库上,它有性能问题。后来那位资深开发者抽身走了。几周之后客户很不满,因为一个页面加载要十五秒,完全不能接受。

Gergely 用一个周末,大概工作了二十多个小时,按自己的做法把整个东西重写了一遍,所有问题都解决了。当时他的感觉是自己可能疯了,因为一位有经验的人明明让他那么做。

他那时没有词汇去描述问题,对自己也没有那份信心。这种事每个人最终都会经历,得自己烧过一次。

刚入行的头几年,人只能靠吃亏来学。走完这一遭之后,再遇到同样的情况,人就能把话说清楚了,也能更有底气地讲明为什么那样做有问题。Gergely 觉得这门课给了学生一条捷径,让他们避开一些坑。

犯错是有效的学习方式

Ousterhout 把这件事看得更透。几乎所有学习都跟犯错有关,最有效的方式是犯错,理解它为什么错,随后把它改对。

在他看来,教育就是创造一个安全的空间,让人可以犯错并从错误里学。他在课上把这件事讲给学生听,也提前打过招呼。

「你们会收到大量的批评,代码里每一个小毛病我都会挑出来。有些地方被挑出来,你们可能会生我的气,但我还是要把每一处问题都指给你们看,因为我要你们知道它们的存在。」

Ousterhout 希望学生从这门课里得到的一件事,是意识到这是一种非常有效的经历。在他看来,有人来挑战他、审视他的代码、给他反馈,其实是一件好事。

因为外面很多开发者对批评很敏感。他们担心,有人批评自己的代码,就意味着自己不是个好程序员。他们也会想,如果非要想到第二个方案,是不是说明自己不够聪明。

只有笨人才要把事情做两遍,聪明人第一遍就做对。于是他们抓住这个念头不放,绝不承认自己的第一个想法不够好。犯错这件事本身是重要的,也是建设性的,作为工程师应当尊重它。

Ousterhout 手上正在做的事

他仍在大量写代码。对话快结束时他提到,回去就要接着写。

一位叫 Behnam Montazeri 的博士生,六七年前的博士论文做了一个新的传输协议,叫 Homa,面向数据中心的场景。论文里的结果非常漂亮,在很多有意思的场景下,它比 TCP 快 10 到 100 倍。

Ousterhout 不愿意让它像大多数博士课题那样做完就没人管。于是他把它接过来当成自己的项目,想看看它能不能被广泛使用,替代数据中心里的一部分 TCP 流量。

他写了它的 Linux 内核实现,持续改进,现在正在把它往内核里合并。手头要处理的是内核开发者给出的大量评论,努力把他们指出的问题都修掉。他特意说,这正是代码评审的实例,也说明代码评审有多大价值。

这是他第一次参与 Linux 内核。事先有人提醒他,说这件事可能又难又痛苦,还很耗时。确实花了不少时间,从第一次提交算起,到现在可能快六个月。

但他必须说,那边的人非常讲道理,他收到的反馈质量很高。他改了不少东西。那些问题要么是他误解了内核的某个机制,要么是对方指出了更好的做法,要么是对方发现了他设计里的复杂之处。

到目前他对这个过程没有意见,其实挺满意,Homa 因此变好了很多。他希望最终能走完流程,进入内核。

被问到这些经历值不值,他的态度很干脆。他认为这些都是很好的学习,换成别的方式,他还不愿意。

第二版改了什么

这本书初版于 2018 年,到现在快七年。有人问他,如果今天重写,哪些部分会补充、重写,或者干脆删掉。

Ousterhout 先交代了书的来历。他当初并没有打算写这本书,书是那门课之后才有的。他开始就这门课做演讲,听众说:「你该把它写成一本书。」2017 年或 2018 年他有一年学术休假,就用那段时间把书写了出来。

写完之后,有些看法他调整过,第二次修订版已经把其中大部分改掉了。他希望读者手上的是第二版,如果书是最近两年买的,那很可能还是第一版。

书的内容没有大改,最大的变化是更强调通用、消除特化这个想法。这一点来自那门课,是多年观察课程项目得来的,并没有写进第一版。他意识到,这个想法对教学生写出不那么复杂的代码非常根本。

眼下没有什么大东西他想改。他反而点出了书里最危险的部分,异常处理那一章。那一章很容易被误解,被应用到他自己也不认同的方向上去。如果从头重写,他可能会对那一章多想一想,把措辞改得更谨慎,帮读者避开误读。

他也提到,书里有些判断是那门课磨出来的,来自多年对课程项目的观察。

写书时他抱有一个期望,希望读者带来一些他没想到的新想法,他能从中学到东西,进而生出更多想法。到目前为止,这件事没有怎么发生。

Ousterhout 很希望有人跑来对他说一句:「你完全错了,书里这个想法完全错了,原因是这个,你应该那样做。」这类反馈比他期望的少得多。

对于这一点,Gergely 给了一个解释。那门课本身很特殊,现实中的软件设计通常是另一副样子。遇到一个问题,做出一个方案,也许在白板上探讨一些取舍,随后把它建出来,接着往前走。之后可能再回来修一修,长期维护,也在这个过程中了解它。

人不会有那种理想的情境,比如有两三支不同的团队做不同的东西,随后把它们放在一起比较。在业界没有这种余裕,即便那样做有点浪费。所以从来不会有机会把同一件事重复做一遍。

Gergely 猜,在那门课上这件事做得到。一群学生走一遍,同一个问题,大家用不同的方式解决,差别就看得见,学生自己也看得见。这种设置也许只能发生在学术界,或者非营利机构,某个允许这么做的地方。

也正因为这样,这本书才特别。很多软件架构书走的是同一个路数,作者在若干个项目里发现了一些东西,而那些项目都是孤立的例子。这本书不一样,它说的是在学生身上反复看到某种做法效果更好,照做的人结果一次比一次好。

Ousterhout 补充了那门课的规模。课上有 9 支队伍,每支两人,做同一批项目,随后互相评审。

代码评审的时候,每个学生会挑两份别人的项目来读,读其中很大一块内容,不必读完整,随后在课上讲评。搭档也在评审另外两个项目。加起来,两个人基本看过班上其余工作的一半。

这也是一个很好的学习来源。学生很喜欢读彼此的代码并琢磨它们。

第一个项目之后,他们跑来问,修订自己的项目时,能不能借用另一个项目里的想法,因为通常课上不许用别人的想法。他说,当然可以,他鼓励学生去拿,去消化吸收所有人手里更成熟的想法。

在这一点上,学术界做得可能比业界更好,因为业界没有时间做修订。没法让一个人每周投入十五个小时、连续十周去做这样一件事,他们另有太多别的事要做。但产出非常好。

Ousterhout 希望推动的事

有人问他,软件设计乃至软件工程的历史上,有哪些想法可能在不久的将来复兴。

Ousterhout 说自己不确定。他想不出哪个曾经出现又消失的想法会重新回来。有些想法之所以消失,是因为人们学会了更好的做法。但愿大家是在越做越好,而不是在好和坏之间来回摆。所以他想不出有什么被遗忘的想法会重新登场。

被问到推荐书目时,他很坦率,说自己喜欢的东西不多,有那么几样,但记不住书名了。这个回答本身也说明,专门写给软件设计这个题目的书确实稀少。他个人的页面里有一个链接指向软件设计这本书,页面上有一小段内容,底部列了几本别的推荐。

至于希望读者为他做点什么,他的回答是欢迎建设性的批评。Ousterhout 从不说自己掌握了全部答案,他有的只是自己的经验和自己认为学到的东西。软件设计这件事还远没有做完,另有大量东西等着被发现。

如果有人认为他说的某处是错的,他很想知道哪里错了,以及为什么错。

Ousterhout 想做的事只有一件,无论是在课上还是在书里,推动开发者群体对软件设计多一分觉知,让人们去想它、谈它。如果能做到,他希望大家建造的软件在设计水准上能整体提高。


回到最开始的那个判断,AI 把写代码变便宜之后,省下来的时间花到哪里去,是个人的选择,也是一支团队的习惯。

Gergely 最后补了一句观察。更多的代码正在被生成出来,这是大势所趋。设计在各个层面都会变得更要紧,资深的工程岗位如此,自己动手做东西的人也一样。

Ousterhout 给过一个成本最低的动作,任何人第二天就能照着做。动手之前,先逼自己写出第二个方案,哪怕它看起来很糟,也写下来,跟第一个比一比。

Ousterhout 算过这笔账。在 Tk 那样的系统里,设计大约只占整个建造时间的百分之一二。换来的,是一个用了很多年、被许多人称赞漂亮而有力的接口。

术语勘误表

英文
中文
说明
Tactical Tornado
战术龙卷风
出活快、留下大量残局的高产程序员
tactical / strategic programming
战术式 / 战略式编程
前者盯短期产出,后者盯长期可演化
deep module
深模块
接口简单、内部藏住大量功能与复杂度
shallow module
浅模块
接口很宽、几乎不隐藏任何东西
complexity
复杂度
全书要解决的那个根本问题
decomposition
分解
书里对软件设计的定位,把庞大系统拆成能相对独立实现的小单元
designing it twice
设计两遍
逼自己做出第二个方案再比较
define errors out of existence
通过定义来规避错误
重新设计接口,让整类异常根本不会发生
exception
异常
抛得越多,压给调用方的负担越重
interface
接口
使用者需要知道的全部信息
implementation
实现
接口背后的那段代码,外界看不到
module
模块
一组功能与它对外暴露的接口
unit test
单元测试
Ousterhout 提倡写,但不主张先于设计写
design review
设计评审
把方案摊开让人挑毛病
intent vote
意向投票
白板辩论法收尾时给理由加权
waterfall model
瀑布模型
前期一次规划到底的做法
prototype
原型
先造出来再决定设计的那条路
technical debt
技术债
战术式编码欠下、后来要还的代价
signature
函数签名
参数与返回值,承载不了全部信息
member variable
成员变量
类里的状态,需要详尽注释
special case
特例
复杂度最集中的来源之一
abstraction
抽象
Ousterhout 建议的开发单元,比单个测试大
information hiding
信息隐藏
把复杂度挪到一边,让专人承担
code review
代码评审
课程与 Linux 内核都在用的机制
upstreaming
上游合并
把补丁并进 Linux 内核主干的过程
Homa
Homa 传输协议
面向数据中心的传输协议,比 TCP 快十到一百倍
Raft
Raft 共识算法
Ousterhout 发明的共识算法,用于分布式数据库与消息系统
Tcl / Tk
Tcl 脚本语言 / Tk 工具包
Ousterhout 创造的脚本语言与图形界面工具包
TDD
测试驱动开发
先写测试再写实现,Ousterhout 认为与设计冲突
Clean Code
《代码整洁之道》
Robert Martin 的书,三处分歧由此而来
10x engineer
10x 工程师
Ousterhout 对这个词的定义与流行用法不同
empathy
同理心
切换视角,站在使用者位置看设计
whiteboard debate
白板辩论法
所有理由都上墙,不许重复,最后意向投票

素材来源说明

本文基于 The Pragmatic Engineer 播客访谈《The Philosophy of Software Design》改写,主持人为 Gergely Orosz,嘉宾为斯坦福大学教授 John Ousterhout。

中译本

这本书有两个中文译本,一个是官方授权的简体版,一个是社区民间译本:

译本
译者
获取方式
《软件设计的哲学(第2版)》
茹炳晟、王海鹏
人民邮电出版社 2024 年 11 月出版,ISBN 9787115655615
《软件设计的哲学,第二版》民间译本
yingang
仓库 github.com/yingang/aposd2e-zh,在线版 yingang.github.io/aposd2e-zh

这两条译本信息建议收藏,日后翻这本书时能省一趟检索。整篇内容对读者有用的话,也不妨先收藏,等下次动手改项目时再翻出来对照一遍。

#软件设计 #软件架构 #AI编程 #代码复杂度 #深模块与浅模块 #技术债 #设计两遍 #测试驱动开发 #程序员成长 #软件设计哲学

相关学习资料