自底向上的编程
Original1993年1月
(这篇文章来自On Lisp的介绍。)
编程风格的一个长期原则是,程序的功能元素不应过大。如果程序的某个组件超出了易于理解的阶段,它就会变成一个复杂的整体,像大城市隐藏逃犯一样容易隐藏错误。这样的软件将难以阅读、难以测试和难以调试。
根据这一原则,一个大型程序必须被分成多个部分,程序越大,分割得越多。你如何分割一个程序?传统的方法称为*自上而下设计:*你说“程序的目的是做这七件事,所以我将其分为七个主要子例程。第一个子例程必须做这四件事,因此它又将有四个自己的子例程,”依此类推。这个过程持续进行,直到整个程序具有适当的粒度——每个部分足够大以完成某些实质性工作,但又足够小以被理解为一个单一的单元。
有经验的Lisp程序员以不同的方式划分他们的程序。除了自上而下设计,他们遵循一个可以称为自下而上设计的原则——改变语言以适应问题。在Lisp中,你不仅仅是将程序写向语言,你还在将语言构建向你的程序。当你在写程序时,你可能会想“我希望Lisp有这样的操作符。”于是你去写它。之后你意识到,使用新的操作符会简化程序的另一部分的设计,依此类推。语言和程序共同演变。就像两个交战国家之间的边界一样,语言和程序之间的边界被不断绘制和重绘,直到最终它沿着山脉和河流停下来,成为你问题的自然边界。最终,你的程序看起来就像语言是为它设计的一样。当语言和程序彼此契合时,你最终会得到清晰、小巧且高效的代码。
值得强调的是,自下而上设计并不意味着只是以不同的顺序编写相同的程序。当你以自下而上的方式工作时,你通常会得到一个不同的程序。你将得到一个更大的语言,具有更多的抽象操作符,以及用它编写的更小的程序。你将得到一个拱门,而不是一个门楣。
在典型代码中,一旦你抽象出那些仅仅是记账的部分,剩下的就会短得多;你构建语言的层次越高,从上到下到达它的距离就越小。这带来了几个好处:
通过让语言承担更多的工作,自下而上设计产生的程序更小且更灵活。一个较短的程序不必被分成那么多组件,组件更少意味着程序更容易阅读或修改。组件更少也意味着组件之间的连接更少,因此出错的机会也更少。正如工业设计师努力减少机器中的运动部件数量一样,有经验的Lisp程序员使用自下而上设计来减少程序的大小和复杂性。
自下而上设计促进了代码重用。当你编写两个或更多程序时,你为第一个程序编写的许多实用工具在后续程序中也会有用。一旦你获得了大量的实用工具,编写新程序所需的努力只需你从原始Lisp开始所需努力的一小部分。
自下而上设计使程序更易于阅读。
这种类型的抽象要求读者理解一个通用操作符;一个功能抽象的实例要求读者理解一个特定用途的子例程。[1]
因为它使你始终关注代码中的模式,自下而上工作有助于澄清你对程序设计的想法。如果程序的两个远程组件在形式上相似,你会注意到这种相似性,并可能以更简单的方式重新设计程序。
在Lisp以外的语言中,自下而上设计在一定程度上是可能的。每当你看到库函数时,自下而上设计就正在发生。然而,Lisp在这方面给你提供了更广泛的能力,增强语言在Lisp风格中扮演了相对更大的角色——以至于Lisp不仅仅是一种不同的语言,而是一种完全不同的编程方式。
确实,这种开发风格更适合由小组编写的程序。然而,同时,它也扩展了小组能够完成的工作范围。在神话般的人月中,弗雷德里克·布鲁克斯提出,程序员小组的生产力并不会随着其规模线性增长。随着小组规模的增加,个别程序员的生产力会下降。Lisp编程的经验表明,这一法则可以更乐观地表述:随着小组规模的减少,个别程序员的生产力会提高。相对而言,小组的优势在于它更小。当一个小组还利用Lisp所能实现的技术时,它可以直接获胜。
新: 免费下载On Lisp。
[1] “但没有人能在不理解你所有新实用工具的情况下阅读程序。”要了解为什么这样的说法通常是错误的,请参见第4.8节。