自底向上编程
1993
(本文节选自On Lisp的介绍。)
程序设计风格的一个长期原则是,程序的功能元素不应太大。如果程序的某个组件增长到难以理解的阶段,它就会变成一团复杂的混乱,像大城市隐藏逃犯一样容易隐藏错误。这样的软件将难以阅读、难以测试和难以调试。
根据这一原则,大型程序必须分成若干部分,程序越大,就越需要进行划分。如何划分程序?传统的方法称为_自顶向下设计_:你说“程序的目标是做这七件事,所以我把它分成七个主要子程序。第一个子程序必须做这四件事,所以它反过来会有四个自己的子程序”,等等。这个过程一直持续到整个程序达到正确的粒度级别——每个部分都足够大,可以做一些实质性的事情,但又足够小,可以作为一个单独的单元来理解。
经验丰富的 Lisp 程序员以不同的方式划分他们的程序。除了自顶向下设计之外,他们还遵循一个可以称为_自底向上设计_的原则——改变语言以适应问题。在 Lisp 中,你不仅仅是将程序向下写入语言,你还将语言向上构建到你的程序中。当你编写程序时,你可能会想“我希望 Lisp 有这样一个运算符”。所以你去编写它。之后,你意识到使用新的运算符会简化程序另一部分的设计,等等。语言和程序一起发展。就像两个交战国之间的边界一样,语言和程序之间的边界被绘制和重新绘制,直到最终它沿着山脉和河流,你问题的自然边界而静止。最终,你的程序看起来好像该语言是为其设计的。当语言和程序彼此很好地契合时,你最终会得到清晰、小巧且高效的代码。
值得强调的是,自底向上设计并不意味着仅仅以不同的顺序编写相同的程序。当你自底向上工作时,你通常会得到一个不同的程序。你将获得一种更大的语言,其中包含更多抽象的运算符,以及一个用它编写的更小的程序,而不是一个单一的、庞大的程序。你将得到一个拱门,而不是一个过梁。
在典型的代码中,一旦你抽象出那些仅仅是簿记的部分,剩下的部分就会短得多;你构建语言的高度越高,你从上到下到达它的距离就越短。这带来了几个优点:
- 通过让语言完成更多的工作,自底向上设计产生的程序更小、更灵活。一个较短的程序不必分成那么多组件,而更少的组件意味着程序更容易阅读或修改。更少的组件也意味着组件之间的连接更少,因此出现错误的机会也更少。正如工业设计师努力减少机器中移动部件的数量一样,经验丰富的 Lisp 程序员使用自底向上设计来减少程序的大小和复杂性。
- 自底向上设计促进了代码重用。当你编写两个或多个程序时,你为第一个程序编写的许多实用程序也将在后续程序中很有用。一旦你获得了一个庞大的实用程序基础,编写一个新程序可能只需要你从原始 Lisp 开始所需工作量的一小部分。
- 自底向上设计使程序更易于阅读。这种类型的抽象要求读者理解一个通用的运算符;函数抽象的一个实例要求读者理解一个特殊的子程序。[1]
- 因为它使你始终注意代码中的模式,所以自底向上工作有助于澄清你对程序设计的想法。如果程序中两个遥远的组件在形式上相似,你就会注意到这种相似性,并可能以更简单的方式重新设计程序。
在 Lisp 以外的语言中,自底向上设计在一定程度上是可能的。无论何时你看到库函数,自底向上设计都在发生。但是,Lisp 在这方面为你提供了更广泛的能力,并且增强语言在 Lisp 风格中起着更大的作用——以至于 Lisp 不仅仅是一种不同的语言,而是一种完全不同的编程方式。
诚然,这种开发风格更适合由小型团队编写的程序。但是,与此同时,它扩展了小型团队可以完成的任务的范围。在_人月神话_中,Frederick Brooks 提出,一组程序员的生产力不会随着其规模线性增长。随着团队规模的增加,单个程序员的生产力会下降。Lisp 编程的经验提出了一种更令人愉快的表达这一规律的方式:随着团队规模的减小,单个程序员的生产力会上升。相对而言,一个小型团队获胜仅仅是因为它更小。当一个小型团队也利用 Lisp 使之成为可能的技术时,它可以彻底获胜。
新内容:免费下载 On Lisp。
[1]“但是没有人可以在不理解你所有新实用程序的情况下阅读该程序。”要了解为什么这些说法通常是错误的,请参见第 4.8 节。