Skip to content

第一章 哲学

Henry Spencer

不懂 Unix 的人注定最终还要重复发明一个蹩脚的 Unix。

1.1 文化? 什么文化

如果你不是程序员,或者对 Unix 涉水未深,这可能让你感觉很奇怪。但是 Unix 确实有它自己的文化;有独特的编程艺术;有一套影响深远的设计哲学。

1.2 Unix 的生命力

Unix 诞生于 1969 年,此后便一直应用于生产领域。按照计算机工业的标准,那已经是好几个地质纪元前的事了——比 PC 机、工作站、微处理器甚至视频显示终端都要早,与第一块半导体存储器是同一时代的古物。

从 Unix 诞生之日起,各种信誓旦旦的预言就伴随着它,说 Unix 必将衰败,或者被其他操作系统挤出市场。可是在今天,化身为 Linux、BSD、Solaris、MacOS X 以及好几个其它变种的 Unix,却显得前所未有的强大。

1.3 反对学习 Unix 文化的理由

外行常常把 Unix 当作教学用的玩具或者是黑客的沙盒而不屑一顾。有一本著名的抨击 Unix 的书——《Unix 反对者手册》,几乎从 Unix 诞生时就一直奉行反对路线,将 Unix 的追随者描写成一群信奉邪教的怪人和失败者。

1.4 Unix 之失

对于一个始于 1969 年的设计来说,在 Unix 设计中居然很难找到硬伤,这着实令人称奇。

  • Unix 文件在字节层次以上再无结构可言。
  • 文件删除了就没法回复。
  • Unix 的安全模型公认地太过原始。
  • 作业控制有欠精致。
  • 命名方式非常混乱。
  • 或许 Unix 拥有文件系统本身就是一个错误。

1.5 Unix 之得

1.5.1 开源软件

尽管“开源”这个术语和开源定义直到1998年才出现,但是自由共享源码的同僚严格复审的开发方式打从 Unix 诞生起就是其文化最具特色的部分。

1.5.2 跨平台可移植性和开放标准

Unix 仍是唯一一个在不同种类的计算机、众多厂商、各种专用硬件上提供了一个一致的、文档齐全的应用程序接口的操作系统。

1.5.3 Internet 和互联网

美国国防部将第一版 TCP/IP 协议栈的开发合同交给一个 Unix 研发组就是考虑到 Unix 大部分时开放源码。除了 TCP/IP 以外,Unix 也也成为互联网服务提供商行业不可或缺的核心技术之一。

1.5.4 开源社区

今天,Unix 社区是各种软件开发的强大支持组。高质量的开源开发工具在 Unix 世界极为丰富。

1.5.5 从头到脚的灵活性

Unix 传统将重点放在尽力使各个程序接口相对小巧、简洁和正交——这也是提高灵活性的方面。

整个 Unix 系统,容易的事还是那么容易,困难的事情呢,至少是有可能做到的。

1.5.6 Unix Hack 之趣

从设计角度来说,趣味性也绝非无足轻重。对于程序员和开发人员来说,如果完成某项任务所需要付出的努力对他们是个挑战却又还在力所能及的范围内,他们就会觉得很有乐趣。

1.5.7 Unix 的经验别处也可适用

Unix 是一个学习设计原则和开发方法的良好平台。

1.6 Unix 哲学基础

Unix 哲学起源于 Ken Thompson 早期关于如何设计一个服务接口简洁、小巧精干的操作系统的思考。

Unix 哲学说来不算是一种正规设计方法。它并不打算从计算机科学的理论高度来产生理论上完美的软件。

Unix 哲学是自上而下的,而不是自下而上的。Unix 哲学注重实效,立足于丰富的经验。

Unix 管道的发明人、Unix 传统的奠基人之一 Doug Mcilroy 曾经说过:

  1. 让每个程序做就好一件事。如果有新任务,就重新开始,不要往原程序中假加入新功能而搞得复杂。
  2. 假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序还是位置的。输出中不要有无关的信息干扰。
  3. 尽可能早的将设计和编译的软件投入使用,哪怕是操作系统也不例外。
  4. 对拙劣的代码别犹豫,扔掉重写
  5. 优先使用工具而不是拙劣的帮助来减轻编程任务的负担。

后来他总结道: ::: mdn Unix 哲学是这样的 一个程序只做一件事,并做好。程序要能协作。程序要能处理文本流,因为这是最通用的接口的。 :::

Rob Pike,最伟大的 C 语言大师之一,从另一个稍微不同的角度表述了 Unix 的哲学:

  1. 你无法断定程序会在什么地方耗费运行时间。瓶颈常常出现在意想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。
  2. 估量。在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度。
  3. 花哨的算法在 n 很小时通常很慢,而 n 通常很小。
  4. 花哨的算法比简单算法更容易出 bug、更难实现。尽量使用简单的算法配合简单的数学结构
  5. 数据压倒一切。如果已经选择了正确的数据结构并且把一切都组织的井井有条,正确的算法也就不言自明。编程的核心是数据结构,而不是算法

Ken Thompson 对 Pike 的原则 4 作了强调:拿不准就穷举

Unix 哲学中更多的内容不是这些先哲们口头表述出来的,而是由他们所作的一切和 Unix 本身所作出的榜样体现出来的。从整体上来说,可以概括为一下几点:

  1. 模块原则:使用简单的接口拼接简单的部件。
  2. 清晰原则:清晰胜于技巧。
  3. 组合原则:设计时考虑拼接组合。
  4. 分离原则:策略同机制分离,接口同引擎分离。
  5. 简洁原则:设计要简洁,复杂度能低则低。
  6. 吝啬原则:除非确无它法,不要编写庞大的程序。
  7. 透明性原则:设计要可见,以便审查和调试。
  8. 健壮原则:健壮源于透明与简洁。
  9. 表示原则:把知识叠入数据以求逻辑质朴而健壮。
  10. 通俗原则:接口设计避免标新立异。
  11. 缄默原则:如果一个程序没什么好说的,就沉默。
  12. 补救原则:出现异常时,马上退出并给出足够错误信息。
  13. 经济原则:宁花机器一分,不花程序员一秒。
  14. 生成原则:避免手工 hack,尽量编写程序去生成程序。
  15. 优化原则:雕琢前要有原型,跑之前先学会走。
  16. 多样原则:决不相信所谓“不二法门”的断言。
  17. 拓展原则:设计着眼未来,未来总比预想来的快。

1.6.1 模块原则:使用简洁的接口拼合简单的部件

正如 Brian Kernighan 曾经说过的:“计算机编程的本质就是控制复杂度”。拍错占用了大部分的开发时间,弄出一个拿得出的可用系统,通常与其说出自才华横溢的设计成果,还不如说是跌跌撞撞的结果。

要编写复杂软件而又不至于一败涂地的唯一方法就是降低其整体复杂度——用清晰的接口把若干简单的模块组合成一个复杂软件。如此一来,多数问题只会局限于某个局部,那么就还有希望对局部进行改进而不至于牵动全身。

1.6.2 清晰原则:清晰胜于技巧

维护如此重要而成本如此高昂:在写程序时,要想到你不是写给执行代码的计算机看的,而是给人——将来阅读维护源码的人,包括你自己——看的。

在 Unix 传统中,这个建议不仅意味着代码注释。良好的 Unix 实践同样信奉在选择算法和实现时就应该考虑到将来的可扩展性。而为了取得程序一丁点的性能提升就大幅度增加技术的复杂性和晦涩性,这个买卖做不得。

相反,优雅而清晰的代码不仅不容易崩溃——而且更易于让后来的修改这立即理解。这点非常重要,尤其是说不定若干年后回过头来修改这些代码的人可能恰恰就是你自己。

1.6.3 组合原则:设计时考虑拼接组合

如果程序彼此之间不能有效通信,那么软件就难免会陷入复杂度的泥淖。

在输入输出方面,Unix 传统极力提倡采用简单、文本化、面向流、设备无关的格式。在经典的 Unix 下,多数程序都尽可能简单过滤器的形式,即将一个输入的简单文本流处理为一个简单的文本流输出。

Unix 中,文本流之于工具,就如同在面向对象环境中的消息之于对象。文本流界面的简洁性加强了工具的封装性。而许多精致的进程间通讯方法,比如远程过程调用,都存在牵扯过多各程序间内部状态的倾向。

要想让程序具有组合性,就要使程序彼此独立。在文本流这一段的程序应该尽可能不要考虑文本流另一端的程序。将一端的程序替换为另一个截然不同的程序,而完全不惊扰另一端应该很容易做到。

当程序无法自然地使用序列化、协议形式的接口时,正确的 Unix 设计至少是,把尽可能多的编程元素组织为一套定义良好的 API。这样,至少你可以通过链接调用应用程序,或者可以根据不同任务的需求粘合使用不同的接口。

1.6.4 分离原则:策略同机制分离,接口同引擎分离

在 Unix 之失的讨论中,我们谈到过 X 系统的设计者在设计中的基本抉择是“实行机制,而不是策略”这种做法——使得 X 成为一个通用图形引擎,而将用户界面风格留给工具包或者系统的其他层次来决定。这一点得以证明是正确的,因为策略和机制是按照不同的时间尺度变化的,策略的变化要远远快于机制。

总而言之,这条准则告诉我们应该设法将接口和引擎剥离开来。

实现这种剥离的一个方法是,比如,将应用按照一个库来编写,这个库包含许多由内嵌脚本语言驱动的编译型程序,而至于整个应用的控制流程则用脚本来撰写而不是用编译型语言。

另一个方法是将应用程序分成可以协作的前端和后端进程,通过套接字上层的专用应用协议进行通讯。前端实现策略,后端实现机制。比起仅用单个进程的整体实现方式来说,这个双端设计大大降低了程序的整体复杂度,有望减少 bug,从而降低程序的寿命周期成本。

1.6.5 简洁原则:设计要简洁,复杂度能低则低

来自多方面的压力常常会让程序变得复杂,其中一种压力就是来自技术上的虚荣心理。程序员们都很聪明,常常以能玩转复杂东西和耍弄抽象概念的能力为傲,这一点也无可厚非。

而更为常见的是,过度的复杂性往往来自于项目的要求,而这些要求常常基于当时的热销热点,而不是基于顾客的需求和软件实际能够提供的功能。许多优秀的设计被市场推销所需要的大堆大堆“特性清单”扼杀——实际上,这些特性几乎从未用过。

要避免这些陷阱,唯一的方法就是鼓励另一种软件文化,以简洁为美,人人对庞大复杂的东西群起而攻之——这是一个非常看重简单解决方案的工程传统,总是设法将程序系统分解为几个能够协作的小部分,并本能的抵制任何用过多噱头来粉饰程序的企图。

这就有点 Unix 文化的意味了。

1.6.6 吝啬原则:除非确无它法,不要编写庞大的程序

“大”有两重含义:体积大,复杂程度搞。程序大了,维护起来就越困难。由于人们对花费了大量精力才做出来的东西难以割舍,结果导致在庞大的程序中把投资浪费在注定要失败或并非最佳的方案上。

1.6.7 透明性原则:设计要可见,以便审查和调试

因为调试通常会占用四分之三甚至更多的开发时间,所以一开始就多做点工作以减少日后调试的工作量会很划算。一个特别有效的净少调试工作量的方法就是设计时充分考虑透明性和显见性。

软件系统的透明性是指你一眼就能够看出软件是在做什么以及怎样做的。显见性指程序带有监视和显示内部状态的功能,这样程序不仅能够运行良好,而旦还可以看得出它以何种方式运行。

设计时如果充分考虑到这些要求会给整个项目全过程都带来好处。至少,调试选项的设置应该尽量不要在事后,而应该在设计之初便考虑进去。这是考虑到程序不但应该能够展示其正确性,也应该能够把原开发者解决问题的思维模型告诉后来者。

程序如果要展示其正确性,应该使用足够简单的输入输出格式,这样才能保证很容易地检验有效输入和正确输出之间的关系是否正确。

出于充分考虑透明性和显见性的目的,还应该提倡接口简洁,以方便其它程序对其进行操作——尤其是测试监视工具和调试脚本。

1.6.8 健壮原则:健壮源于透明与简洁

软件的健壮性指软件不仅能在正常情况下运行良好,而且在超出设计者设想的意外条件下也能够运行良好。

大多数软件禁不起磕碰,毛病很多,就是因为过于复杂,很难通盘考虑。如果不能够正确理解一个程序的逻辑,就不能确信其是否正确,也就不能在出错的时候修复它。

1.6.9 表示原则:把知识叠入数据以求逻辑质朴而健壮

即使最简单的程序逻辑让人类来验证也很困难,但就算是很复杂的数据,对人类说来,还是相对容易地就能够推导和建模的。数据要比编程逻辑更容易驾驭。所以接下来,如果要在复杂数据和复杂代码种选择一个,宁愿选择前者。更近一步:在设计中,你应该主动将代码的复杂度转移到数据之中去。

1.6.10 通俗原则:接口设计避免标新立异

也就是众所周知的“最少惊奇原则”。最易用的程序就是用户需要学习新东西最少的程序——或者换句话说就是最切合用户已有知识的程序。

1.6.11 缄默原则:如果一个程序没什么好说的,就保持沉默

1.6.12 补救原则:出现异常时,马上退出并给出足量错误信息

软件在发生错误的时候也应该与在正常操作的情况下一样,有透明的逻辑。最理想的情况当然是软件能够适应和应付非正常操作;而如果补救措施明明没有成功,却悄无声息地埋下崩溃的隐患,直到很久以后才显现出来,这就是最坏的一种情况。

因此,软件要尽可能从容地应付各种错误输入和自身的运行错误。但是,如果做不到这一点,就让程序尽可能以一种容易诊断错误的方式终止。

1.6.13 经济原则:宁花机器一份,不花程序员一秒

在 Unix 早期的小型机时代,这一条观点还是相当激进的(那时机器要比现在慢得多也贵得多)。如今,随着技术的发展,开发公司和大多数用户(那些需要对核爆炸进行建模或处理三维电影动画的除外)都能够得到廉价的机器,所以这一准则的合理性就显然不用多说啦!

但不知何故,实践似乎还没完全跟上现实的步伐。如果我们在整个软件开发中很严格的遵循这条原则的话,大多数的应用场合都应该使用高一级的语言,如Perl、Tcl、Python、Java、Lisp,甚至shell一一这些语言可以将程序员从自行管理内存的负担中解放出来。

1.6.14 生成原则:避免手工 Hack,尽量编写程序去生成程序

1.6.15 优化原则:雕琢前得现有原型,跑之前先学会走

原型设计最基本的原则最初来自于 Kernighan 和 Plauger 所说的“90%的功能现在能实现,比100%的功能永远实现不了强”。做好原型设计设计可以帮助你避免为蝇头小利而投入过多的时间。

1.6.16 多样原则:绝不相信所谓“不二法门”的断言

1.6.17 扩展原则:设计着眼未来,未来总比预想来的快

1.7 Unix 哲学之一言以蔽之

所有的 Unix 哲学浓缩为一条铁律,那就是各地编程大师们奉为圭臬的“KISS”原则:

::: mdn KISS Keep It Simple, Stupid! :::

1.8 应用 Unix 哲学

在 Unix 世界中,这些原则都直接来自于实践,并形成了具体的规定,一下列举的只是部分内容:

  1. 只要可行,一切都应该做成与来源和目标无关的过滤器。
  2. 数据流应尽可能文本化。
  3. 数据库部署和应用协议应尽可能文本化。
  4. 复杂的前端和后端应该泾渭分明。
  5. 如果可能,用 C 编写前,先用解释性语言搭建原型
  6. 当且仅当只用一本编程语言会提高程序复杂度时,混用编程语言才比单一语言编程来的好。
  7. 宽收严发。
  8. 过滤时,不需要丢弃的信息绝不丢。
  9. 小就是美。在确保完成任务的基础上,程序功能尽可能少。

1.9 态度也要紧

看到该做的就去做——短期来看似乎是多做了,但从长期来看,这才是最佳捷径。

如果不确定什么是对的,那么就只做最少量的工作,确保任务完成就行,知道明白什么是对的。

要良好的运用 Unix 哲学,你就应该不断追求卓越。你必须相信,软件设计是一门技艺,值得你付出所有的指挥、创造力和激情。否则,你的视线就不会超越那些简单、老套的设计和实现。