每次说起程序员将来想走什么职业道路,估计有一半的人会说:要做架构师(剩下的一半,按最近的时尚,一般是瞄上了产品经理)。做架构,就经常免不了讨论程序结构。而在讨论时,有一条整个程序设计世界的金科玉律:「为将来而设计」。每个项目,多小一点的程序,也得煞有介事地定义一个又一个抽象结构或一个又一个保留参数,振振有词地宣称这样可以让将来的扩展变得「简单」。如果有人对此提出质疑,那简直就是捅了马蜂窝一般——平时看上去温柔可人的程序员们,个个可以变得凶神恶煞脸红脖子粗地跟所有人争辩:「预留的扩展如何没用!……扩展!为将来做设计,可能没用么!」
你不是在设计,你是在过度设计。
逻辑学有一条著名的奥卡姆剃刀原则:如无必要,勿增实体。这句话原本出自哲学讨论,但实际上同样适用于工程。如果具体到程序设计,那么非常适合用来给那些自以为是架构师的愣头青作为一个行为准则:你引入一个新的类,数据结构,或者函数参数,应当是一个有确定意义的「实体」。事实上,它包含了两个问题:一、它本身给程序提供了什么样功能?二、我们如何证明这个功能是为了完成程序功能不可或缺的?前一个问题很容易蒙混过关,后一个问题就未必。
举一个 Windows 自己的例子:CoInitializeEx function (COM)。它的第一个参数在 MSDN 的官方文档上标明为预留(Reserved)。而 COM 如今诞生已经超过十年,这个参数始终都是预留,毫无改变。如今 Windows 桌面开发已经逐步进入以 .NET 框架为基础的时代,我不认为在接下来的时间里,这个参数在将来某一天还有发掘出新用途的可能。简而言之,这是个在我有生之年也许都不会被用上的扩展。
只是个案么?不妨用 Google 搜索关键字:reserved for future use,结果估计要让我们大吃一惊:这几个关键字,几乎能出现在你能想到的各个公司的公开文档里。
上面只是一个参数的例子,但足以展示一种危险的设计思路,即:我不知道这个东西会不会用上,于是我就占个位子,将来也许就用上了。这种做法存在两个问题:第一,如果占下的位子确实没有用上,那么这个保留的位置就永远和程序如影随形。一个参数或许不算严重的问题,但结构体里的保留参数则会永远占用它的空间。另一个问题是:如果占下的位子并没有一个清晰的用途,我们往往还是免不了在未来某个时候重新设计。比较典型的例子是 DnsQuery function (Windows) 之于 DnsQueryEx function (Windows):事实证明,DnsQuery() 保留的参数,也没能避免我们在新的 DnsQueryEx() 中重新设计参数:需求变了,或者业务逻辑改变了,都可能造成这种结果。
反过来说,如果我们一开始就明确地知道预留的用途,我们何必用 Reserved 这样模糊不清的名字命名我们的参数?
很多程序员经常秉持一种看法,认为好的代码应当浑身上下无处不可扩展,进而把程序可扩展部分的数目多寡当作衡量程序设计水平高低的标志。但当具体问到某个扩展点究竟在何种场景下使用,具体应该如何使用,则经常语焉不详。这种情况下,最常出现的结果就是留下一些作者自己也无法解释究竟做何用途的代码,美其名曰「为将来而设计」,而实际的效果,除了让所有人不明就里之外,什么也做不到。这种你糊涂我糊涂大家都糊涂的做法,无论如何不能被称为「设计」。
那么什么才是正确的做法?
我们写程序,首先是因为我们需要解决具体的问题。我不否认程序确实有将来扩展的可能,因此合格的设计者应当能够,也必须能够定义正确的扩展点。但这个论断还有一个伴生的结论:合格的设计者也应当能够正确地告诉使用者,哪些核心功能应该保持不变,或只能由原始设计者进行扩展。这些核心的功能往往包括了一些非常重要的程序行为,包括但不限于:多线程执行时序、同步或异步、异常安全、日志查错支持、升级兼容性等等。如果随便交给后来者(无论是是使用者,还是接手的维护者),都可能因为思路不同而导致不可预知的后果。扩展的前提,必须能够确保后来者不会误解最初设计者的思路。离开这个前提还能得到正确的扩展,近乎于撞大运。真正常见的后果,或者是无用的扩展,或者是无效的扩展。
空口无凭?我们都看到例子了。
因此,好的设计者,需要会做加法,更需要会做减法。与其为未知的将来随便留一个自己都不知对错的方案,不如忘掉它,老老实实做好眼下的事。
扪心自问,你会做减法了么?
— 完 —
本文作者:陈甫鸼
【知乎日报】
你都看到这啦,快来点我嘛 Σ(▼□▼メ)