1993
(本文选自 On Lisp 的导言。)
编程风格中有一条长期存在的原则:程序的功能性元素不应过大。如果程序的某个组件大到难以轻易理解,它就会变成一团复杂性,其隐藏错误的轻松程度,就像大城市隐藏逃犯一样。这样的软件会难以阅读、难以测试,也难以调试。
按照这一原则,大程序必须拆分成若干部分,而且程序越大,就越需要拆分。怎样拆分一个程序?传统的方法称为自顶向下设计:你会说“程序的目的就是做这七件事,所以我把它分成七个主要子程序。第一个子程序要做这四件事,因此它又会有自己四个子程序”,诸如此类。这个过程会一直继续,直到整个程序具有合适的粒度——每一部分都大到足以完成实质性的工作,但又小到可以作为一个独立单元来理解。
有经验的 Lisp 程序员会以不同的方式划分程序。除了自顶向下设计,他们还遵循一条可称为自底向上设计的原则——让语言去适应问题。在 Lisp 中,你不只是朝着语言去编写程序,你还会朝着程序去构建语言。当你在写程序时,可能会想“我希望 Lisp 有这样那样的运算符。”于是你就去把它写出来。随后你会意识到,使用这个新运算符会简化程序另一部分的设计,诸如此类。语言和程序会共同演化。就像交战双方之间的国界,语言与程序之间的边界被一次次划定、重划,直到最终沿着山川河流——也就是你所面对问题的自然边界——停下来。到最后,你的程序看起来就像是为这门语言量身设计的一样。而当语言和程序彼此契合得很好时,你得到的代码就会清晰、简洁而高效。
值得强调的是,自底向上设计并不意味着只是用不同顺序重写同一个程序。当你采用自底向上的方式工作时,最终通常会得到一个不同的程序。你得到的不是一个单一的、庞大的程序,而是一门带有更多抽象运算符的更大语言,以及一个用这门语言写成的更小程序。不是过梁,而是拱门。
在典型代码中,一旦你把那些纯粹属于记账的部分抽象出去,剩下的内容就会短得多;你把语言搭得越高,从顶部向下抵达它的距离就越短。这带来几个好处:
- 通过让语言承担更多工作,自底向上设计会得到更小、更灵活的程序。较短的程序不需要被拆成那么多组件,而组件更少意味着程序更容易阅读或修改。组件更少也意味着组件之间的连接更少,因此出错的机会也更少。正如工业设计师努力减少机器中的运动部件一样,有经验的 Lisp 程序员使用自底向上设计来减少程序的规模和复杂性。
- 自底向上设计促进代码复用。当你编写两个或更多程序时,你为第一个程序写的许多工具在后续程序中也会派上用场。一旦你积累了大量工具库,编写新程序所需的精力就可能只占你必须从原始 Lisp 开始时的一小部分。
- 自底向上设计使程序更易读。 这种类型的抽象实例要求读者理解一个通用运算符;而功能抽象的实例则要求读者理解一个专用子程序。[1]
- 因为它会促使你始终留意代码中的模式,自底向上的工作方式有助于澄清你对程序设计的想法。如果程序中两个相距很远的组件在形式上相似,你就会注意到这种相似性,并可能以更简单的方式重新设计程序。
在 Lisp 之外的其他语言中,自底向上设计也能在一定程度上实现。每当你看到库函数时,自底向上设计其实就在发生。然而,Lisp 在这方面赋予你的能力要广泛得多,而扩展语言在 Lisp 风格中所扮演的角色也相应更大——以至于 Lisp 不仅仅是一门不同的语言,而是一整种不同的编程方式。
确实,这种开发风格更适合能由小团队编写的程序。不过与此同时,它也扩展了小团队所能完成事情的边界。在《人月神话》中,Frederick Brooks 提出,程序员团队的生产力不会随着规模线性增长。随着团队规模增大,单个程序员的生产力会下降。Lisp 编程的经验则把这条规律表述得更令人振奋一些:随着团队规模减小,单个程序员的生产力会上升。相对而言,小团队会获胜,仅仅因为它更小。当小团队还能利用 Lisp 所能实现的技术时,它就能彻底取胜。
新: 免费下载《On Lisp》。
[1] “但是,如果不了解你所有的新工具,谁也读不懂这个程序。” 要弄清这种说法为什么通常是错误的,请参见第 4.8 节。