读前:这一章要回答什么
一个模块牵涉的因素那么多,哪些应该放在设计的中心、哪些应该藏起来?拿不准的时候怎么办?
这是全书最短的几章之一,但它的位置很特别:它不提出新技巧,而是把前面的很多章收拢到同一个动作上,就是把重要的和不重要的分开。接口只放使用者在意的东西(第 4、5 章),名字挑信息量最大的几个词(第 14 章),性能要紧时就围绕关键路径组织代码(第 20 章),这些都是这个动作的具体形式。
所以读这一章的最好方式是"回头看":每读到一个观点,就想想前面哪一章其实在讲同一件事。作者最后给这种能力起了一个名字:好品味(good taste)。
- 开头两段与 21.1 节:作者举了抽象、命名、性能三个例子,说明前面各章都在"分开重要与不重要"。读完试着自己再补两个(提示:第 8 章的配置参数、第 10 章的异常)。21.1 节的关键词是杠杆(leverage),留意作者举的两种杠杆有什么不同。
- 21.1 节后半:作者给经验不足的开发者一个具体建议:先立一个假设。注意作者强调的不是"猜对",而是事后怎么复盘。
- 21.3~21.4 节:三种突出方式之后有一句"反过来也成立",它把"突出"变成了一个诊断工具。21.4 节的两种错误,各自对应前面哪些红旗?
核心观点
围绕重要的东西组织系统
重要的东西要突出、要显眼;不重要的东西尽量藏起来,把它对系统其余部分的影响降到最小。
找杠杆
一个问题的解法能顺带解决许多问题,或者知道一条信息就能理解许多事(例如一个不变量),那里通常就是重要的东西。
拿不准就先立假设
说出"我认为这个最重要",按它去建,然后复盘它为什么对、为什么错,下一次会判断得更好。
让重要的东西尽量少
能给默认值就给,能自动算出来的配置就别让人填,能在底层处理掉的异常就别往上抛。确实重要的,也要减少它起作用的地方。
突出的三种方式
显眼(prominence):放在接口文档、名字、常用方法的参数里;重复(repetition):关键概念反复出现;居中(centrality):放在系统核心,决定周围的结构。
两种错误
把太多东西当成重要的(杂乱、认知负担、浅类);没认出真正重要的东西(信息被藏、功能缺失、未知的未知)。
The phrase "good taste" describes the ability to distinguish what is important from what isn't important.— 第 21.5 节
心智模型:两条轴、四个格子
把"这件事实际上重不重要"和"设计里有没有突出它"当成两条独立的轴,就得到四种情况。两条对角线分别是做对了和做错了;第 21.4 节的两种错误正好是两个错的格子。
练一下手感:一个上传客户端
假设你要给团队写一个"把本地文件上传到对象存储"的客户端库。下面是设计时会碰到的十个因素。先替每一个选一个位置,再点"对照"看本站的看法。没有唯一正确答案,重要的是理由。
重要还是不重要?
原创例子:同一个上传接口,两种重点
struct UploadOptions {
size_t chunkSize; // 分块大小
int threads; // 并发数
int retryCount;
int backoffMs;
std::string checksumAlgo; // "md5" / "crc32c"
std::string tmpDir;
bool verifyAfterUpload;
bool overwrite;
};
Status upload(const std::string& localPath, const std::string& bucket,
const std::string& key, const UploadOptions& opt);
调用者必须给每个字段都填一个值,于是大家从别处复制一份配置过来。真正危险的 overwrite(第 9 行)混在一堆调优参数中间,和"分块大小"看起来一样重要;"返回成功是否意味着对象已完整可读"这一点则根本没写。两种错误同时出现了:调优参数被当成了重要的东西,覆盖语义和完成保证却没被突出。
// 把本地文件上传为 dst。返回 OK 时,对象已完整写入并通过校验,可以立即读取。
// 目标已存在时怎么办由 ifExists 决定:这是唯一需要调用者想清楚的选项。
enum class IfExists { Fail, Replace };
Status upload(const std::string& localPath, const ObjectName& dst,
IfExists ifExists);
// 分块、并发、重试和校验由库根据文件大小和网络状况自动决定。
// 极少数需要干预的场景(例如压测)才用这个重载。
Status upload(const std::string& localPath, const ObjectName& dst,
IfExists ifExists, const UploadTuning& tuning);
重要的东西被突出了:覆盖语义成了没有默认值的必填参数(显眼),完成保证写在接口注释的第一句。不重要的东西被藏了起来,或者自动决定(让重要的东西尽量少)。代价是库的实现变复杂了:它要自己根据文件大小选分块、自己处理重试。这正是第 8 章说的"把复杂性往下拉"。
边界与反方:什么时候别生搬硬套
同一个细节,对一类使用者不重要,对另一类可能至关重要。上面的 UploadTuning 对普通业务方无关紧要,对做压测的团队却是核心。所以"藏起来"不等于"删掉":罕见但真实的需求要有入口,只是不挡在常用路径上。藏过了头就会落到第二种错误,第 10 章"做过头"那一节说的也是这件事:真正重要的东西必须暴露出来。
作者说,一个想法如果到处都看得见、反复出现、影响系统结构,那它就是重要的。本站补充:这句话也可以用来查问题。如果某个你认为无关紧要的细节(某个内部格式、某个魔法常量)出现在几十个文件里,那么不管你怎么想,它已经变重要了,因为改它的代价很大。这时该做的是把它收回一个模块里(第 5 章的信息隐藏),而不是给它补更多文档。
很多团队把"可配置"当成美德,觉得参数越多越灵活。按这一章的标准,每多一个必须由人决定的参数,就多一件"重要的事"。作者在 21.2 节的建议正相反:能自动算出来的配置,就不要再让管理员去管。本站补充:判断标准不是"有没有人可能想调",而是"大多数使用者需不需要想这件事"。
以下不在书里,来自书中给出的读者讨论组。本章的两句话,Ousterhout 2018 年就在讨论组里用过,而且各有一个具体的场合:
- 重要的东西藏起来,是在害人。有读者主张用广播消息取代对象之间的直接调用,理由是这样"没有依赖"。Ousterhout 回答(2018-08):理想的设计让开发者在任一时刻需要知道的东西尽量少;但一件事如果确实重要,就必须把它讲清楚、放在明处。谁依赖谁是重要的,把它藏进消息里,只是让开发者更难受。
- 让重要的东西变少。有读者担心自己抛的异常种类太多。Ousterhout 回答(2018-07):上层确实要区别对待的情况,就必须如实反映出来,想藏只会出问题;能做的是减少重要的东西,比如把几种情况用同一种方式处理,它们之间的区别就不再重要了。
两处合起来就是本章的做法:先判断什么重要,再让重要的东西尽量少、尽量显眼。
以下不在书里,来自 Ousterhout 在 X(原 Twitter)上的回帖。2021 年有人说写作和编程之间有联系,Ousterhout 表示同意,并说自己会这样告诉学生(2021-03):能写出好的代码内文档、能设计出干净的类,多半也就能写好论文;两件事用的是同一套办法,找出最重要的东西,然后把力气集中在它上面。本章 21.5 节把"分清什么重要"推广到了软件之外,点名提到技术写作;这条回帖是同一个意思的一个具体场合。
练一练
下面三道题来自训练场。先自己判断再看解析,答题记录会进入间隔复习。
自测题
小结与读后一句话
- 好设计的核心之一是分开重要和不重要:围绕重要的东西组织系统,不重要的尽量藏起来。
- 判断方法:找杠杆;准备多个选项来比较;拿不准就立假设、事后复盘。
- 先让重要的东西尽量少(默认值、自动计算、在底层处理掉),再减少它起作用的地方。
- 突出的三种方式:显眼、重复、居中。反过来,到处出现的东西就是重要的。
- 两种错误:把太多东西当成重要的;没认出真正重要的。区分两者的能力就是"好品味"。
第 22 章是全书的结语。本站把它做成了一张全书复习页:用一张图把前面所有章节串起来,并指向速查、训练和项目。
读后一句话:用你自己的话写下这一章最重要的一个观点,最好结合你正在做的某个模块:它最重要的是什么?