软件设计中最重要的原则之一,就是 区分哪些事情重要,哪些事情不重要 。
系统应该围绕那些真正重要的事情来组织。对于那些不那么重要的事情,应尽可能减少它们对系统其他部分的影响。重要的内容应该被突出,使其更加明显;而不重要的内容,则应该尽可能隐藏起来。
本书前面许多章节的思想,其核心其实都是在做同一件事情: 把重要的东西与不重要的东西分离 。
例如,在设计抽象(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。
如果你不断提升自己的设计能力,那么不仅能够更快地开发出质量更高的软件,整个软件开发过程本身也会变得更加愉快。