小马的世界

读书笔记-软件设计的哲学【21】决定什么才是真正重要的

2026-06-28 · 11 min read

软件设计中最重要的原则之一,就是 区分哪些事情重要,哪些事情不重要

系统应该围绕那些真正重要的事情来组织。对于那些不那么重要的事情,应尽可能减少它们对系统其他部分的影响。重要的内容应该被突出,使其更加明显;而不重要的内容,则应该尽可能隐藏起来。

本书前面许多章节的思想,其核心其实都是在做同一件事情: 把重要的东西与不重要的东西分离

例如,在设计抽象(abstraction)时就是如此。模块的接口(interface)体现的是模块使用者真正关心的内容;而那些使用者并不关心的细节,则应该隐藏在实现(implementation)内部,使它们尽可能不被注意到。

再例如,在给变量命名时,我们希望只用少量几个单词,就尽可能完整地表达变量最重要的信息;变量名中体现的,应该正是这个变量最值得关注的特性。

如果一个模块真正重要的是性能,那么整个模块的设计也应该围绕性能目标展开。在 20.4 节的例子中,这意味着寻找一种设计,使性能关键路径(performance-critical path)尽可能减少方法调用和特殊情况判断,同时仍然保持代码干净、简单且易于理解。

如何判断什么才重要?

有时候,重要的事情来自系统之外的约束,例如第 20.4 节中的性能要求。

但更多的时候,真正重要的内容需要由设计者自己判断。

即使系统存在外部约束,设计者仍然必须进一步思考:为了满足这些约束,到底什么才是真正重要的。

判断什么重要时,一个有效的方法是寻找 杠杆效应(leverage) ——也就是说,一个方案不仅解决当前问题,还能够顺带解决许多其他问题;或者,一条信息不仅本身有价值,还能帮助理解大量其他内容。

例如,在第 6.2 节讨论文本存储时,我们介绍了一种 通用接口 ,用于插入和删除字符区间。

相比之下,像 Backspace 这样的专门接口只能解决一个具体问题,而通用接口却能够解决许多不同的问题,因此具有更大的杠杆效应。

对于文本类(text class)的接口来说,调用是否来自 Backspace 键其实并不重要;真正重要的是 需要删除一段文本

不变量(Invariant) 也是杠杆效应的另一个典型例子。

一旦知道某个变量或数据结构满足某个不变量,就能够预测它在许多不同场景下的行为。

如果存在多个可选方案,那么判断什么最重要会容易得多。

例如,在给变量命名时,可以先在脑海里列出所有与该变量相关的词语,然后从中挑选最能够表达核心信息的几个词,用它们组成变量名。

这正是前面介绍过的 “设计两次(Design it Twice)” 原则的一个例子。

有时候,哪些事情最重要并不是显而易见的,尤其对于经验还不丰富的开发者来说更是如此。

遇到这种情况时,我建议先提出一个假设:

“我认为这件事情最重要。”

然后按照这个假设去设计系统,再观察最终效果。

如果你的假设是正确的,那么就分析为什么它是正确的,以及过程中有哪些线索帮助你作出了正确判断,以便今后继续利用这些经验。

如果假设错了,也没有关系。

分析为什么会判断错误,以及当时是否存在某些线索,本来可以帮助你避免这个错误。

无论结果如何,你都会从这次经历中获得经验,并逐渐能够作出越来越好的判断。

尽量减少重要的事情

真正需要关心的事情越少,系统通常就越简单。

例如,应尽量减少创建一个对象时必须指定的参数数量;或者提供能够覆盖大多数使用场景的默认值。

对于那些确实重要的事情,也应尽可能减少它们需要出现的位置。

一个模块内部隐藏的信息,对于模块外部代码来说其实并不重要。

如果某个异常情况能够完全在系统较低层解决,那么整个系统其他部分就无需关心它。

同样,如果某个配置参数能够根据系统运行状态自动计算,而不是要求管理员手动选择,那么 这个配置参数对于管理员来说就已经不再重要了。

如何突出真正重要的事情

一旦确定了哪些事情重要,就应该在设计中 突出它们

一种方法是提高 可见性(prominence)

重要的内容应该出现在最容易被看到的地方,例如接口文档、名称、或者经常调用的方法参数中。

第二种方法是 重复(repetition)

重要的思想应该反复出现,使读者不断接触到它。

第三种方法是 中心化(centrality)

真正重要的内容应该位于整个系统的核心位置,让系统整体围绕它组织。

例如,操作系统中的设备驱动接口(device driver interface)就是一种中心化设计,因为数百甚至数千个驱动程序都依赖它。

当然,反过来也同样成立:

如果某个概念经常出现、不断被重复引用,或者会显著影响整个系统结构,那么它一定是重要的。

同样,对于那些不重要的事情,则应该降低其存在感(de-emphasize)

它们应尽可能隐藏起来,不应频繁出现在开发者面前,也不应该影响整个系统的整体结构。

常见错误

在判断什么重要时,常见有两类错误。

第一类错误,是 把太多事情都当成重要的事情。

这样一来,不重要的内容会充斥整个设计,增加复杂性,也增加开发者的认知负担(cognitive load)。

例如,一个方法包含了大量实际上绝大多数调用者都不会用到的参数。

又例如,第 26 页讨论过的 Java I/O 接口,它迫使开发者必须区分 带缓冲(buffered)不带缓冲(unbuffered) 的 I/O。

然而,这种区别几乎从来都不重要——开发者几乎总是希望使用缓冲,而不会愿意花时间专门决定这一点。

浅类(shallow classes) 往往正是由于把太多事情都当成重要内容而产生的。

第二类错误,则是 没有意识到某件真正重要的事情。

这种错误会导致重要信息被隐藏起来,或者系统没有提供关键功能,使开发者不得不不断重新创造这些内容。

这种错误不仅降低开发效率,还会导致大量 未知的未知(unknown unknowns)

更广泛地思考

关注真正重要的事情 这一思想,并不仅仅适用于软件设计。

它同样适用于技术写作。

让一篇文档易于阅读的最佳方法,是在开头明确几个核心概念,然后围绕这些核心概念组织整个文档。

当讨论系统细节时,也应不断把这些细节与整体概念联系起来。

把注意力集中在真正重要的事情上,也是一种很好的生活哲学。

找出生活中真正重要的几件事情,并尽可能把自己的时间和精力投入到这些事情上。

不要把大量时间浪费在那些你自己都认为既不重要、也没有价值的事情上。

“Good taste” ,描述的正是区分什么重要、什么不重要的能力。

拥有良好的品味,是成为优秀软件设计者的重要组成部分。

总结

本书始终围绕着一个主题展开:复杂性(complexity)

应对复杂性,是软件设计中最重要的挑战。复杂性使系统难以构建、难以维护,同时也常常导致系统运行缓慢。

在整本书中,我尝试分析导致复杂性的根本原因,例如 依赖关系(dependencies)晦涩难懂(obscurity)

我还介绍了一些能够帮助你识别不必要复杂性的 危险信号(red flags) ,例如信息泄漏(information leakage)、不必要的错误条件(unneeded error conditions),以及过于泛化的名称(names that are too generic)。

此外,我提出了一些构建更简单软件系统的通用原则,例如追求 **深而通用(deep and generic)**的类、让错误在设计层面“不复存在”(defining errors out of existence),以及将接口文档(interface documentation)与实现文档(implementation documentation)分离。

最后,我讨论了构建简单设计所需要具备的 投资心态(investment mindset)

这些建议的缺点在于:它们都会让项目早期投入更多工作。

此外,如果你还没有养成从设计角度思考问题的习惯,那么在学习这些设计方法时,开发速度还会进一步放慢。

如果你唯一关心的是尽快完成眼前的代码,那么思考设计只会让你觉得是在做一些阻碍真正目标的苦差事。

另一方面,如果你认为优秀的设计本身就是一个值得追求的重要目标,那么本书介绍的这些思想,会让编程变得更加有趣。

软件设计本身就是一个引人入胜的谜题:如何用尽可能简单的结构解决一个具体问题?

不断探索不同的方法,并最终找到一个既简单又强大的解决方案,是一种非常令人满足的体验。

一个干净、简单、清晰的设计,本身就是一种美。

此外,你在优秀设计上的投入,很快就会获得回报。

项目开始阶段认真定义好的模块,会在之后不断复用,从而节省大量时间。

六个月前写下的清晰文档,当你再次回到代码、准备增加新功能时,同样会替你节省大量时间。

而你花费在提升设计能力上的时间,也会不断产生回报。

随着经验和能力不断增长,你会发现自己能够越来越快地完成优秀的设计。

一旦真正掌握了设计方法,优秀设计所花费的时间,其实并不会比“快速凑合(quick-and-dirty)”式设计多多少。

成为一名优秀设计者最大的回报,在于你能够把更多时间花在设计阶段——而这是软件开发中最有趣的部分。相反,设计能力不足的人,大部分时间都会浪费在复杂而脆弱的代码中,不断追逐各种 Bug。

如果你不断提升自己的设计能力,那么不仅能够更快地开发出质量更高的软件,整个软件开发过程本身也会变得更加愉快。