跳到主要内容

代码写给人看 — 读的账比写的账大

这一章讲四件事: 为什么读代码的时间远多于写、这个事实逼出什么优先级; 怎么让一个函数像书的目录一样读;名字为什么是「面向阅读者的界面」、怎么当场验名字; 以及语境怎么让代码变成一本能翻的书。 主走查是一个混层的函数:同一坨代码,拆层前后各读一遍。

1. 先算账:读比写贵,而且贵得多

第 01 章说过「读远比写费时间」1。这一章先把这笔账算细,因为所有可读性原则都压在这笔账上。

作者在「交流」一节给了一个时间结构:软件从开发到寿终正寝, 期间有很多人一遍又一遍地读其中的代码;而且不用等到维护—— 开发阶段程序员就要一边回顾前面写的代码、一边写新代码, 「通过代码,程序员与片刻之前的自己实现了交流」2

一段代码的一生大致是这样(时间比例是编的,用于演示量级):

写 ▌ 1 份时间
读 ██████████████████ 15~20 份时间
(自己回看 + 同事评审 + 接手人排障 + 加功能前的理解 + ……)

图说:比例是演示值;书里的论断是定性的「读远多于写」,
定量数字见本库另一本已拆的书《软件开发的 201 个原则》。

既然读占大头,优先级就排出来了,而且是一条链:

读的效率 > 写的效率(多花时间写得清楚,值) 读的效率 > 执行的效率(为了让机器跑快把代码写得难懂,亏)3

第二条尤其反直觉,但它有推理:代码可读性高,后面想提高执行效率时也更容易下手4; 反过来,先牺牲可读性换速度,等于把未来每次优化都变得更难。 书里的原话是:不能让人读懂的代码不是好代码5

2. 代码是唯一的线索:为什么必须表达意图

这一节回答:为什么「可读性」不是加分项,而是唯一通道。

想知道一个软件到底是怎么运行的,能看什么?作者把常见文档挨个排了一遍6:

文档它告诉你什么为什么靠不住
需求定义文档需要什么东西只说「要什么」,不说「怎么运行」
基本设计文档用什么样的软件来满足需求粗粒度,离运行很远
详细设计文档成品是什么结构代码是动态变的,文档做不到同步;而且不是每个项目都有

排完只剩一个结论:代码是我们正确、完整地了解软件运行方式的唯一线索7。 文档会撒谎(因为过期),代码不会——代码就是正在运行的那个东西本身。

所以写代码就是在写「给人读的运行说明书」,作者把它叫 PIE 原则 (Program Intently and Expressively,编程要表达出意图),并类比写诗和写信: 写的时候要想着读的人8

意图表达不出来会怎样?作者给了现象级描述:打地鼠式的开发。 修改一个「会在各处随机出现问题」的软件毫无乐趣可言——问题像地鼠一样从意想不到的地方冒头, 砸完一个冒下一个9。读不懂代码的团队,最后就是在这样的软件上打地鼠。

走查起点:一段读不懂的代码

本章的主走查对象,一个函数(函数=可被反复调用的一段命名代码;这是演示编的例子, 书刻意不放代码,原文只给原则):

function process() {
// 校验会员状态
String s = db.query("select * from user where id=" + uid);
if (s == null) throw new Err("no user");
// 折扣计算
double d = s.vip ? 0.9 : 1.0;
if (coupon != null) d = d * 0.95;
// 入库
db.exec("update order set price=" + price * d + " where id=" + oid);
}

读这段代码的人要同时端着三件事:数据库怎么读、折扣怎么算、数据怎么写。 下面两节给出两把拆解它的工具。

3. SLAP:让函数像书的目录

这一节回答:怎么把「一锅炖」的代码整理成可以翻的目录。

SLAP(Single Level of Abstraction Principle,单一抽象层次原则)的主张只有一句: 同一个函数里的代码,要处在同一个抽象层次上10

「抽象层次」要当场讲明白。同一个业务,可以从不同高度描述:

  • 高层次:「给订单打上会员折扣」——说的是做什么;
  • 中层次:「算折扣率,再写回订单」——说的是分几步做;
  • 低层次:「拼一条 update 语句,转义引号,发给数据库」——说的是具体怎么做

一段好的代码,每一层里只出现同一个高度的说法。把第 2 节那个函数拆开:

function 打会员折扣(订单) { ← 1 级:目录
确认会员身份(订单);
应用折扣(订单);
保存订单(订单);
}
function 应用折扣(订单) { ← 2 级:目录
d = 基础折扣率(订单.会员); ← 3 级:正文
if (订单.有券) d = d * 0.95;
订单.折后价 = 订单.原价 * d;
}

图说:读 1 级像翻目录,想知道「应用折扣」的细节才下到 2 级。
每一层内部没有高度跳跃。数字与函数名均为演示。

作者给这个状态起的比喻是图书:高级到中级的处理相当于目录, 最低级的处理相当于正文;代码统一之后,可以像翻书一样从目录跳到任意一页, 也可以当参考资料从任意一页开始读11

为什么有效:抽象度跳变会打断理解

书里给的机制很具体:代码顺着读时,大脑按当前层次建立了预期; 读到一半抽象度突然改变,流畅感戛然而止,之前的理解也会被扰乱12。 原书用「连接数据库」(低级)和「执行业务逻辑」(高级)混在一个函数里当反面例子: 读者刚建立「这段在处理业务」的心智,突然被拽去管数据库连接的细节13

操作要领,作者给了两条14:

  1. 把函数结构化:每个函数由「调用比自己低一级的函数」组成,这种函数叫复合函数(composed method);
  2. 复合函数尽量小:小到哪怕只有一行,只要名字能表达意图,就值得是一个函数—— 因为名字本身在替读者省事。

SLAP 不只管函数。作者把它推广到类设计(类=面向对象语言里「数据+操作」的打包单位): 高层的概念放进抽象类(只定义「有什么」的模板),低层概念放进它的继承类(按模板填细节的具体版本), 低层概念不许倒灌进抽象类15

4. 名字很重要:面向阅读者的用户界面

这一节回答:命名为什么是设计行为,以及怎么当场验收一个名字。

作者把命名列为「编程中最重要的课题」,给了两组论断16:

  • 命名这个动作:取出了合适的名字,说明这个元素已经被正确理解、正确设计; 取不出好名字,说明你还没真正想清它的作用。「取了一个合适的名字就表示设计已经完成了一大半」;
  • 名字本身:写代码的人和读代码的人很少能站在一起实时对话, 程序员之间主要靠代码交流,而名字传递的信息最多

「名字是面向代码阅读者的用户界面17——这个说法值得停一下。 界面是人机之间的翻译层;好名字让读的人不必打开函数内部就知道它干什么, 从而跳着读代码(只精读关心的那几个函数)18。 反过来,名字含糊,读者就被迫深入每个函数内部,随着调用层数加深,负担层层加重19

心理映射:名字欠的债,读者来还

作者专门造了一个词:心理映射——读代码的人看到名字后, 先要在心里把它转换成自己已知的东西,才能继续往下读20。 转换是纯开销,而且中断主线:读者本来在追踪「这段代码在干什么」, 现在被迫分神去猜「这个缩写是什么」。

两个最常见的心理映射源21:

  1. 领域之外的独创名:用了个自造的词,谁也不知道指什么;
  2. 单字母变量名:名字只是个占位符,读者必须自己脑补它代表什么。

作者的解法朴素:用大家共认的标准术语命名——共识白拿,不要独创22

验收一个名字:环回检测

怎么知道自己起的名字合格?作者给了可操作的验法,环回检测(也叫「名字可逆性」): 名字必须能还原出它所指内容的说明文本。做法是绕一圈: 先写说明文本 → 从说明文本想名字 → 再从名字倒推说明文本,绕回来对得上就是好名字23

书里带了一个完整的走查,值得原样走一遍。要命名的功能,说明文本是 「一种用语音来操作软件的功能」24。第一个候选把说明文本直接截成了「语音识别」——意思是听懂人说什么;可听懂不等于照做,所以判 ×。四个候选的判定:

候选名字从名字倒推回去,得到什么判定
语音识别功能识别语音……然后呢?倒推不出「用语音操作」× 丢掉了操作
语音操作功能操作语音?像在说「操纵声音」△ 关系不明
语音控制功能控制……语音?△ 易读成「限制语音」
语音命令功能用语音下达命令——基本还原了说明文本○(仍可能被读成「输出语音」,提醒见下)

走查的教训比结论更值钱:直接抄说明文本里的词,凑不出好名字—— 「识别」「操作」「控制」都在原文里,单独拎出来全都偏25

动手清单:命名六条

作者给的命名要领,每条都有明确的读者视角26:

  1. 名字尽量多含信息——把名字当简短的注释写,多备几个候选挑最合适的;
  2. 不许有歧义——起完名多问自己几遍「还能读成别的意思吗」;
  3. 说明效果和目的,不说手段——读者要的是「它为我做什么」;
  4. 先写测试后写实现,用「调用方视角」反验名字顺不顺手;
  5. 能念出来——团队对话里要能口头提到它,念不出的名字在脑子里也转不动;
  6. 能搜索——单个字母或数字的名字,搜出来满屏都是,等于没法搜。

名字为什么值得花力气:约书亚树原则

为什么命名值得下这么大功夫?原书在讲反模式的那一章收了一个心理学故事,补上了最后一块理由: 有个人在图书馆翻植物图鉴,认识了「约书亚树」——一种他以为自己从没在镇上见过的树; 回家的路上,他发现镇子里到处都是约书亚树27。 这条原则的结论是:人知道了名字,才看得见东西;没有名字的东西,视线扫过去也留不下痕迹28

对代码这种看不见摸不着的东西,这件事被放大:设计模式(把代码里反复出现的组织套路起名归档的一套目录) 最大的成果,恰恰就是给那些「人人都在用、却说不出名字」的窍门起了名字—— 从此它们才能被点名、被传授、被复用28。 落到团队,原书给的处方是通用语言(Ubiquitous Language,团队共有的、对同一件事只用同一个词的语言): 近似的词会招来混乱;而且这个词不能只停在对话里,还要写进代码29—— 名字不是作者一个人的事,是整个团队的公共设施。

5. 语境:代码这本「书」的章节结构

这一节回答:SLAP 管函数内部,那函数之间、模块之间靠什么组织。

作者借了一个词:语境,就是平常说的上下文——上下文指的是周围的情况与背景。 用在代码上很具体:写代码时要给读的人创造语境——让模块名(模块=数据与函数的打包单位)足以说明这块代码是干什么的,模块里的函数名再以模块名为语境、说明每个函数处理什么30。 这样,「模块名之于函数」就像「章标题之于段落」:读者先读标题,再决定要不要读正文31

为什么这么做有效?作者引了一个心理学实验,自己复刻了一遍,值得照着玩: 把十个词的字序打乱,让你还原。第一组不分类,第二组预先说明「都是食物」—— 第二组会明显更快,哪怕打乱方式完全一样。大脑只是多收到一条线索 「接下来要处理这类东西」,处理能力就急剧提高。这个提前给出的线索,叫阅读前导32

无分类 食物
秋节佳中 → ? 腰爆炒花 → 爆炒腰花
票高车铁 → ? 子鸡辣丁 → 辣子鸡丁
…… ……

图说:右列多了「食物」这个前导,联想速度立刻不同。
模块名就是代码的阅读前导。词例为书中原例。

落到代码上:读代码最贵的方式是自下而上——从一行行语句开始猜整体; 合理方式是自上而下:先读模块名、函数名这些「标题」,带着语境进正文33。 所以写代码的人有一个对应义务:认真安排阅读前导——名字就是标题,标题取不好,正文再清楚也救不回来。

注释的位置:说「为什么」,不说「是什么」

语境工程还剩一块拼图:注释。作者的理想很明确——可读性高到没有注释也能读懂的代码才是理想代码; 但代码文本天生只能表达「做什么」「怎么做」,「为什么这样做」必须交给注释34。 这条与第 01 章的「代码即设计」严丝合缝:设计理由是代码唯一表达不了的部分, 注释正是给设计理由留的位置。

6. 清晰的底线:第二次读不懂就要动手

本章最后一条原则是个底线判据,来自 UNIX 思想的「清晰原则」: 代码不应该巧妙,而应该清晰——以大幅提升复杂度(读懂它要花的脑力)为代价换取一点点性能提升,是丢了西瓜捡芝麻35

它给了可执行的判定:同一段代码,第一次没读懂,可能是碰巧;第二次还需要解读,就必须处理了—— 加注释,或者把代码改得更直白36。这是把「可读性」从审美变成流程的一步: 读不懂不再是可以忍过去的小事,而是挂账的工单。

与之配套的是另一条:「清晰原理」要求逻辑能自证正确——让人一眼判断出没有问题; 做不到的地方「不择手段」:注释、文档、图,哪个能用用哪个; 而且用着用着你会发现,与其事后补证明,不如一开始就把逻辑写清楚37

7. 作者的判断与证据

书里给了证据的:

  • 阅读前导实验(食物组 vs 无分类组)是可当场复刻的心理学现象,作者在正文里带着读者做了一遍32;
  • 「读比写多」「维护成本占大头」是软件工程的通行经验,作者未给数字,列为论断而非实证。

作者的推测,要分开看的:

  • 「命名完成=设计完成一大半」是经验判断,极有价值但无法测量;
  • 「聪明人思考也是一步一步的」(本章未展开,第 09 章讲)这类段落带心气鼓励性质,证据强度低于其他节。

8. 边界与局限

  • 可读性的判据因团队而异。 什么算「同一抽象层次」有主观成分;SLAP 拆得极端时,函数碎成一屏几十个,跳转成本上升——书里没有讨论拆分过度的反例(「模块太多」的坏味在第 06 章补上这一面)。
  • 名字的寿命比代码短。 代码活着,含义会漂移,好名字会过期;书里在「坏味」一节(第 06 章)承认「名称并不是一成不变的」,本章的命名清单是起点不是终点。
  • 「执行效率让位于读」有例外。 书里自己在性能章节(第 10 章)承认少数耗时的「热点」代码值得专门优化(调整代码让程序跑得更快)——但顺序仍是先可读后优化,例外要先用测量证明。
  • 文学编程(代码与文档写在同一文本、由工具分别生成文档与可执行代码)是本书给出的极端方案,作者明说它没普及、负担重,只有思想以 PIE 形态留存38——读者不必实践,但值得知道上限长什么样。

9. 可带走的

  1. 读比写贵,所以多花时间写清楚永远是赚的——写快一点省下的时间,读的时候连本带利还回去;
  2. 代码是唯一不会撒谎的文档——想了解软件,读代码;文档负责「为什么」,代码负责「是什么、怎么做」;
  3. SLAP 自查法:把函数里每行代码标上层次(做什么/分几步/具体做),同一函数里混层就拆函数;
  4. 一行函数也值得起——只要名字表达了意图,它就在替读者省一次深入;
  5. 命名走环回:说明文本 → 名字 → 再倒推说明文本,对不上就重取;名字说目的,不说手段;
  6. 消灭心理映射:不用领域外的自造词,不用单字母名(循环计数器除外);
  7. 先写标题再写正文:模块名、函数名是阅读前导,取名的顺序应该在写实现之前;
  8. 第二次读不懂就改——把「看不懂」当工单,不当认命。

10. 原文地图

主题原书章原文位置
代码是交流场所、与片刻前的自己交流3.2 交流text/08-ch03-02-3-2.txt:21(搜「片刻之前的自己」) · text/08-ch03-02-3-2.txt:32(搜「从计算机转移到」)
代码是唯一线索、文档靠不住2.4 PIEtext/06-ch02.txt:395(搜「唯一线索」) · text/06-ch02.txt:402(搜「更何况并非每个项目」)
读的效率优先于写与执行2.4 PIEtext/06-ch02.txt:410(搜「读代码的次数远比写」) · text/06-ch02.txt:417(搜「执行代码的效率」)
打地鼠式开发2.4 PIEtext/06-ch02.txt:429(搜「打地鼠」)
注释表达为什么2.4 PIEtext/06-ch02.txt:441(搜「理想的代码」) · text/06-ch02.txt:443(搜「还需要用到注释」)
SLAP 主张与图书类比2.5 SLAPtext/06-ch02.txt:517(搜「抽象化概念」) · text/06-ch02.txt:523(搜「优秀的图书」)
抽象度突变打断理解2.5 SLAPtext/06-ch02.txt:557(搜「流畅感」)
复合函数、一行也可成函数2.5 SLAPtext/06-ch02.txt:566(搜「复合函数」) · text/06-ch02.txt:569(搜「一行」)
SLAP 推广到类设计2.5 SLAPtext/06-ch02.txt:579(搜「抽象类」)
命名是最重要课题、设计完成一大半2.7 名字很重要text/06-ch02.txt:765(搜「最重要的课题」) · text/06-ch02.txt:773(搜「一大半」)
名字是用户界面、跳着读2.7 名字很重要text/06-ch02.txt:786(搜「用户界面」) · text/06-ch02.txt:793(搜「跳着阅读」)
心理映射2.7 名字很重要text/06-ch02.txt:855(搜「心理映射」) · text/06-ch02.txt:865(搜「一个字母」)
环回检测与语音命名走查2.7 名字很重要text/06-ch02.txt:873(搜「名字可逆性」) · text/06-ch02.txt:882(搜「语音识别功能」)
命名六条2.7 名字很重要text/06-ch02.txt:832(搜「简短的注释」) · text/06-ch02.txt:839(搜「效果和目的」) · text/06-ch02.txt:846(搜「念出来」)
约书亚树原则(名字为什么值钱)7.6 约书亚树原则text/74-ch07.txt:489(搜「到处都是约书亚」) · text/74-ch07.txt:491(搜「知道某个东西的名字」) · text/74-ch07.txt:506(搜「通用语言」)
语境、阅读前导实验6.6 语境text/73-ch06.txt:627(搜「自上而下地编写」) · text/73-ch06.txt:749(搜「急剧提高」) · text/73-ch06.txt:758(搜「阅读前导」)
自上而下型阅读6.6 语境text/73-ch06.txt:755(搜「阅读方法称为」)
不应巧妙而应清晰3.39 清晰原则text/45-ch03-39.txt:8(搜「不应该巧妙」)
第二次解读就要处理3.39 清晰原则text/45-ch03-39.txt:25(搜「再三解读」)
逻辑自证正确、不择手段3.35 清晰原理text/41-ch03-35.txt:12(搜「不择手段」)
文学编程没普及、思想留存2.4 PIEtext/06-ch02.txt:495(搜「并没有得到普及」) · text/06-ch02.txt:498(搜「保留了下来」)

Footnotes

  1. 出处:「代码必然被修改」第 206 段(text/05-ch01.txt:206,搜「比写要费时间」)。

  2. 出处:「3.2 交流」第 21 段(text/08-ch03-02-3-2.txt:21,搜「片刻之前的自己」);维护成本与读的频度见第 16 段(text/08-ch03-02-3-2.txt:16,搜「维护成本」)。

  3. 出处:「2.4 PIE」第 413 段(text/06-ch02.txt:413,搜「读代码的效率」)。

  4. 出处:「2.4 PIE」第 417 段(text/06-ch02.txt:417,搜「执行代码的效率」)。

  5. 出处:「2.4 PIE」第 421 段(text/06-ch02.txt:421,搜「好代码」)。

  6. 出处:「2.4 PIE」第 398 段(text/06-ch02.txt:398,搜「需求定义文档」)。

  7. 出处:「2.4 PIE」第 395 段(text/06-ch02.txt:395,搜「唯一线索」)。

  8. 出处:「2.4 PIE」第 387 段(text/06-ch02.txt:387,搜「写诗」)。

  9. 出处:「避免打地鼠式的开发」第 429 段(text/06-ch02.txt:429,搜「打地鼠」)。

  10. 出处:「2.5 SLAP」第 517 段(text/06-ch02.txt:517,搜「抽象化概念」)。

  11. 出处:「2.5 SLAP」第 523 段(text/06-ch02.txt:523,搜「优秀的图书」)。

  12. 出处:「2.5 SLAP」第 557 段(text/06-ch02.txt:557,搜「流畅感」)。

  13. 出处:「2.5 SLAP」第 571 段(text/06-ch02.txt:571,搜「连接数据库」)。

  14. 出处:「2.5 SLAP」第 566 段(text/06-ch02.txt:566,搜「复合函数」)与第 569 段(text/06-ch02.txt:569,搜「一行」)。

  15. 出处:「SLAP 的适用范围」第 579 段(text/06-ch02.txt:579,搜「抽象类」)。

  16. 出处:「2.7 名字很重要」第 765 段(text/06-ch02.txt:765,搜「最重要的课题」)与第 773 段(text/06-ch02.txt:773,搜「一大半」)。

  17. 出处:「2.7 名字很重要」第 786 段(text/06-ch02.txt:786,搜「用户界面」)。

  18. 出处:「2.7 名字很重要」第 793 段(text/06-ch02.txt:793,搜「跳着阅读」)。

  19. 出处:「2.7 名字很重要」第 804 段(text/06-ch02.txt:804,搜「深入阅读」)。

  20. 出处:「避免心理映射」第 855 段(text/06-ch02.txt:855,搜「心理映射」)。

  21. 出处:「避免心理映射」第 862 段(text/06-ch02.txt:862,搜「标准术语」)与第 865 段(text/06-ch02.txt:865,搜「一个字母」)。

  22. 出处:「避免心理映射」第 863 段(text/06-ch02.txt:863,搜「共识」)。

  23. 出处:「环回检测」第 873 段(text/06-ch02.txt:873,搜「名字可逆性」)与第 871 段(text/06-ch02.txt:871,搜「环回」)。

  24. 出处:「环回检测」第 882 段(text/06-ch02.txt:882,搜「语音识别功能」)。

  25. 出处:「环回检测」第 891 段(text/06-ch02.txt:891,搜「理想的」)。

  26. 出处:「2.7 名字很重要」第 832 段(text/06-ch02.txt:832,搜「简短的注释」)、第 839 段(text/06-ch02.txt:839,搜「效果和目的」)、第 843 段(text/06-ch02.txt:843,搜「先写测试」)、第 846 段(text/06-ch02.txt:846,搜「念出来」)、第 849 段(text/06-ch02.txt:849,搜「搜索出来」)。

  27. 出处:「7.6 约书亚树原则」第 489 段(text/74-ch07.txt:489,搜「到处都是约书亚」)。故事出自《卓有成效的程序员》引用的图鉴轶事:认识了约书亚树的人,回家路上发现镇子里到处都是它。

  28. 出处:「7.6 约书亚树原则」第 491 段(text/74-ch07.txt:491,搜「知道某个东西的名字」)与第 501 段(text/74-ch07.txt:501,搜「设计模式的最大成果」)。 2

  29. 出处:「7.6 约书亚树原则」第 506 段(text/74-ch07.txt:506,搜「通用语言」)与第 517 段(text/74-ch07.txt:517,搜「用到代码里」)。通用语言(Ubiquitous Language)出自埃文斯《领域驱动设计》,原书在这条反模式条目里转述。

  30. 出处:「6.6 语境」第 627 段(text/73-ch06.txt:627,搜「创造语境」)与第 627 段(text/73-ch06.txt:627,搜「自上而下地编写」)。

  31. 出处:「6.6 语境」第 633 段(text/73-ch06.txt:633,搜「章标题」)。

  32. 出处:「6.6 语境」第 749 段(text/73-ch06.txt:749,搜「急剧提高」)与第 758 段(text/73-ch06.txt:758,搜「阅读前导」)。 2

  33. 出处:「6.6 语境」第 755 段(text/73-ch06.txt:755,搜「阅读方法称为」)。

  34. 出处:「要写注释」第 441 段(text/06-ch02.txt:441,搜「理想的代码」)与第 443 段(text/06-ch02.txt:443,搜「还需要用到注释」)。

  35. 出处:「3.39 清晰原则」第 8 段(text/45-ch03-39.txt:8,搜「不应该巧妙」)。

  36. 出处:「3.39 清晰原则」第 25 段(text/45-ch03-39.txt:25,搜「再三解读」)。

  37. 出处:「3.35 清晰原理」第 12 段(text/41-ch03-35.txt:12,搜「不择手段」)。

  38. 出处:「文学编程」第 495 段(text/06-ch02.txt:495,搜「并没有得到普及」)与第 498 段(text/06-ch02.txt:498,搜「保留了下来」)。