Unix哲学起源于Ken Thompson早期关于如何设计一个服务接口简洁小巧精干的操作系统的思考随着Unix文化在学习如何尽可能发掘Thompson设计思想的过程中不断成长同时一路上还从其它许多地方博采众长
Unix哲学说来不算是一种正规设计方法它并不打算从计算机科学的理论高度来产生理论上完美的软件那些毫无动力松松垮垮而且薪水微薄的程序员们能在短短期限内如同神灵附体般造出稳定而新颖的软件——这只不过是经理人永远的梦呓罢了
Unix哲学(同其它工程领域的民间传统一样)是自下而上的而不是自上而下的Unix哲学注重实效立足于丰富的经验你不会在正规方法学和标
准中找到它它更接近于隐性的半本能的知识即Unix文化所传播的专业经验它鼓励那种分清轻重缓急的感觉以及怀疑一切的态度并鼓励你以幽默达观的
态度对待这些
Unix管道的发明人Unix传统的奠基人之一Doug McIlroy在[McIlroy]中曾经说过
(i)让每个程序就做好一件事如果有新任务就重新开始不要往原程序中加入新功能而搞得复杂
(ii)假定每个程序的输出都会成为另一个程序的输入哪怕那个程序还是未知的输出中不要有无关的信息干扰避免使用严格的分栏格式和二进制格式输入不要坚持使用交互式输入
(ⅲ)尽可能早地将设计和编译的软件投入试用 哪怕是操作系统也不例外理想情况下 应该是在几星期内对拙劣的代码别犹豫扔掉重写
(iv)优先使用工具而不是拙劣的帮助来减轻编程任务的负担工欲善其事必先利其器
后来他这样总结道(引自《Unix的四分之一世纪》(A Quarter Century of Unix [Salus]))
Unix哲学是这样的一个程序只做一件事并做好程序要能协作程序要能处理文本流因为这是最通用的接口
Rob Pike 最伟大的C语言大师之一 在《Notes on C Programming》中从另一个稍微不同的角度表述了Unix的哲学[Pike]
原则你无法断定程序会在什么地方耗费运行时间瓶颈经常出现在想不到的地方所以别急于胡乱找个地方改代码除非你已经证实那儿就是瓶颈所在
原则估量在你没对代码进行估量特别是没找到最耗时的那部分之前别去优化速度
原则花哨的算法在n很小时通常很慢而n通常很小花哨算法的常数复杂度很大除非你确定n总是很大否则不要用花哨算法(即使n很大也优先考虑原则)
原则花哨的算法比简单算法更容易出bug更难实现尽量使用简单的算法配合简单的数据结构
原则数据压倒一切如果已经选择了正确的数据结构并且把一切都组织得井井有条正确的算法也就不言自明编程的核心是数据结构而不是算法
原则没有原则
Ken Thompson——Unix最初版本的设计者和实现者禅宗偈语般地对Pike的原则作了强调
拿不准就穷举
Unix哲学中更多的内容不是这些先哲们口头表述出来的而是由他们所作的一切和Unix本身所作出的榜样体现出来的从整体上来说可以概括为以下几点
模块原则使用简洁的接口拼合简单的部件
清晰原则清晰胜于机巧
组合原则设计时考虑拼接组合
分离原则策略同机制分离接口同引擎分离
简洁原则设计要简洁复杂度能低则低
吝啬原则除非确无它法不要编写庞大的程序
透明性原则设计要可见以便审查和调试
健壮原则健壮源于透明与简洁
表示原则把知识叠入数据以求逻辑质朴而健壮
通俗原则接口设计避免标新立异
缄默原则如果一个程序没什么好说的就沉默
补救原则出现异常时马上退出并给出足够错误信息
经济原则宁花机器一分不花程序员一秒
生成原则避免手工hack尽量编写程序去生成程序
优化原则雕琢前先要有原型跑之前先学会走
多样原则决不相信所谓不二法门的断言
扩展原则设计着眼未来未来总比预想来得快
如果刚开始接触Unix这些原则值得好好体味一番谈软件工程的文章常常会推荐大部分的这些原则但是大多数其它操作系统缺乏恰当的工具和传统将
这些准则付诸实践所以多数的程序员还不能自始至终地贯彻这些原则蹩脚的工具糟糕的设计过度的劳作和臃肿的代码对他们已经是家常便饭了他们奇
怪Unix的玩家有什么好烦的呢
模块原则使用简洁的接口拼合简单的部件
正如Brian Kernighan曾经说过的计算机编程的本质就是控制复杂度[KernighanPlauger]排错占用了大部分的开发时间弄出一个拿得出手的可用系统通常与其说出自才华横溢的设计成果还不如说是跌跌撞撞的结果
汇编语言编译语言流程图过程化编程结构化编程所谓的人工智能第四代编程语言面向对象以及软件开发的方法论不计其数的解决之道被抛
售者吹得神乎其神但实际上这些都用处不大原因恰恰在于它们成功地将程序的复杂度提升到了人脑几乎不能处理的地步就像Fred
Brooks的一句名言[Brooks]没有万能药
要编制复杂软件而又不至于一败涂地的唯一方法就是降低其整体复杂度——用清晰的接口把若干简单的模块组合成一个复杂软件如此一来多数问题只会局限于某个局部那么就还有希望对局部进行改进而不至牵动全身
清晰原则: 清晰胜于机巧
维护如此重要而成本如此高昂在写程序时要想到你不是写给执行代码的计算机看的而是给人——将来阅读维护源码的人包括你自己——看的
在Unix传统中这个建议不仅意味着代码注释良好的Unix实践同样信奉在选择
算法和实现时就应该考虑到将来的可扩展性而为了取得程序一丁点的性能提升就大幅度增加技术的复杂性和晦涩性这个买卖做不得——这不仅仅是因为复杂的代码容易滋生bug也因为它会使日后的阅读和维护工作更加艰难
相反优雅而清晰的代码不仅不容易崩溃——而且更易于让后来的修改者立刻理解这点非常重要尤其是说不
定若干年后回过头来修改这些代码的人可能恰恰就是你自己永远不要去吃力地解读一段晦涩的代码三次第一次也许侥幸成功但如果发现必须重新解读一遍——离第一次太久了具体细节无从回想——那么你该注释代码了这样第三次就相对不会那么痛苦了
—Henry Spencer
组合原则设计时考虑拼接组合
如果程序彼此之间不能有效通信那么软件就难免会陷入复杂度的泥淖
在输入输出方面Unix传统极力提倡采用简单文本化面向流设备无关的格式在经典的Unix下多数程序都尽可能采用简单过滤器的形式即将一个输入的简单文本流处理为一个简单的文本流输出
抛开世俗眼光Unix程序员偏爱这种做法并不是因为他们仇视图形用户界面而是因为如果程序不采用简单的文本输入输出流它们就极难衔接
Unix中文本流之于工具就如同在面向对象环境中的消息之于对象文本流界面的简洁性加强了工具的封装性而许多精致的进程间通讯方法比如远程过程调用都存在牵扯过多各程序间内部状态的倾向
要想让程序具有组合性就要使程序彼此独立在文本流这一端的程序应该尽可能不要考虑文本流另一端的程序将一端的程序替换为另一个截然不同的程序而完全不惊扰另一端应该很容易做到
GUI可以是个好东西有时竭尽所能也不可避免复杂的二进制数据格式但是在做一个GUI前最好还是应该想想可不可以把复杂的交互程序跟干粗活的算法程序分离开每个部分单独成为一块然后用一个简单的命令流或者是应用协议将其组合在一起
在构思精巧的数据传输格式前有必要实地考察一下是否能利用简单的文本数据格式以一点点格式解析的代价换得可以使用通用工具来构造或解读数据流的好处是值得的
当程序无法自然地使用序列化协议形式的接口时正确的Unix设计至少是把尽可能多的编程元素组织为一套定义良好的API这样至少你可以通过链接调用应用程序或者可以根据不同任务的需求粘合使用不同的接口
(我们将在第章详细讨论这些问题)
分离原则: 策略同机制分离接口同引擎分离
在Unix之失的讨论中我们谈到过X系统的设计者在设计中的基本抉择是实行机制而不是策略这种做法——使X成为一个通用图形引擎而将用户
界面风格留给工具包或者系统的其它层次来决定这一点得以证明是正确的因为策略和机制是按照不同的时间尺度变化的策略的变化要远远快于机制GUI工
具包的观感时尚来去匆匆而光栅操作和组合却是永恒的
所以把策略同机制揉成一团有两个负面影响一来会使策略变得死板难以适应用户需求的改变二来也意味着任何策略的改变都极有可能动摇机制
相反将两者剥离就有可能在探索新策略的时候不足以打破机制另外我们也可以更容易为机制写出较好的测试(因为策略太短命不值得花太多精力在这上面)
这条设计准则在GUI环境之外也被广泛应用总
而言之这条准则告诉我们应该设法将接口和引擎剥离开来实现这种剥离的一个方法是比如将应用按照一个库来编写这个库包含许多由内嵌脚本语言驱动的C服务程序而至于整个应用的控制流程则用脚本来撰
写而不是用C语言这种模式的经典例子就是Emacs编辑器它使用内嵌的脚本语言Lisp解释器来控制用C编写的编辑原语操作我们会在第章讨论这
种设计风格
另一个方法是将应用程序分成可以协作的前端和后端进程通过套接字上层的专用应用协议进行通讯我们会在第章和第章讨论这种设计前端实现策略后端实现
机制比起仅用单个进程的整体实现方式来说这种双端设计方式大大降低了整体复杂度bug有望减少从而降低程序的寿命周期成本
简洁原则设计要简洁复杂度能低则低
来自多方面的压力常常会让程序变得复杂(由此代价更高bug更多)其中一种压力就是来自技术上的虚荣心理程序员们都很聪明常常以能玩转复杂
东西和耍弄抽象概念的能力为傲这一点也无可厚非但正因如此他们常常会与同行们比试看看谁能够鼓捣出最错综复杂的美妙事物正如我们经常所见他们
的设计能力大大超出他们的实现和排错能力结果便是代价高昂的废品
错综复杂的美妙事物听来自相矛盾Unix程序员相互比的是谁能够做到简洁而漂亮并以此为荣这一点虽然只是隐含在这些规则之中但还是很值得公开提出来强调一下
—Doug McIlroy
更为常见的是(至少在商业软件领域里)过度的复杂性往往来自于项目的要求而这些要求常常基于当月的推销热点而不是基于顾客的需求和软件实际能
够提供的功能许多优秀的设计被市场推销所需要的大堆大堆特性清单扼杀——实际上这些特×××几乎从未用过然后恶性循环开始了比别人花哨的方
法就是把自己变得更花哨很快庞大臃肿变成了业界标准每个人都在使用臃肿不堪bug极多的软件连软件开发人员也不敢敝帚自珍
无论以上哪种方式最后每个人都是失败者
要避免这些陷阱唯一的方法就是鼓励另一种软件文化以简洁为美人人对庞大复杂的东西群起而攻之——这是一个非常看重简单解决方案的工程传统总是设法将程序系统分解为几个能够协作的小部分并本能地抵制任何用过多噱头来粉饰程序的企图
这就有点Unix文化的意味了
吝啬原则: 除非确无它法不要编写庞大的程序
大有两重含义体积大复杂程度高程序大了维护起来就困难由于人们对花费了大量精力才做出来的东西难以割舍结果导致在庞大的程序中把投资浪费在注定要失败或者并非最佳的方案上
(我们会在第章就软件的最佳大小进行更多的详细讨论)
透明性原则设计要可见以便审查和调试
因为调试通常会占用四分之三甚至更多的开发时间所以一开始就多做点工作以减少日后调试的工作量会很划算一个特别有效的减少调试工作量的方法就是设计时充分考虑透明性和显见性
软件系统的透明性是指你一眼就能够看出软件是在做什么以及怎样做的显见性指程序带有监视和显示内部状态的功能这
样程序不仅能够运行良好而且还可以看得出它以何种方式运行设计时如果充分考虑到这些要求会给整个项目全过程都带来好处至少调试选项的设置应该尽量不要在事后而应该在设计之初便考虑进去这是考虑到程序不但应该能够展示其正确性也应该能够把原开发者解决问题的思维模型告诉后来者
程序如果要展示其正确性应该使用足够简单的输入输出格式这样才能保证很容易地检验有效输入和正确输出之间的关系是否正确
出于充分考虑透明性和显见性的目的还应该提倡接口简洁以方便其它程序对其进行操作——尤其是测试监视工具和调试脚本
健壮原则: 健壮源于透明与简洁
软件的健壮性指软件不仅能在正常情况下运行良好而且在超出设计者设想的意外条件下也能够运行良好
大多数软件禁不起磕碰毛病很多就是因为过于复杂很难通盘考虑如果不能够正确理解一个程序的逻辑就不能确信其是否正确也就不能在出错的时候修复它
这也就带来了让程序健壮的方法就是让程序的内部逻辑更易于理解要做到这一点主要有两种方法透明化和简洁化
就健壮性而言设计时要考虑到能承受极端大量的输入这一点也很重要这时牢记组合原则会很有益处经不起其它一些程序产生的输入(例如原始的
Unix
C编译器据说需要一些小小的升级才能处理好Yacc的输出)当然这其中涉及的一些形式对人类来说往往看起来没什么实际用处比如接受空的列表/字符
串等等即使在人们很少或者根本就不提供空字符串的地方也得如此这可以避免在用机器生成输入时需要对这种情况进行特殊处理
—Henry Spencer
在有异常输入的情况下保证软件健壮性的一个相当重要的策略就是避免在代码中出现特例bug通常隐藏在处理特例的代码以及处理不同特殊情况的交互操作部分的代码中
上面我们曾说过软件的透明性就是指一眼就能够看出来是怎么回事如果怎么回事不算复杂即人们不需要绞尽脑汁就能够推断出所有可能的情况那么这个程序就是简洁的程序越简洁越透明也就越健壮
模块性(代码简朴接口简洁)是组织程序以达到更简洁目的的一个方法另外也有其它的方法可以得到简洁接下来就是另一个
表示原则: 把知识叠入数据以求逻辑质朴而健壮
即使最简单的程序逻辑让人类来验证也很困难但是就算是很复杂的数据对人类来说还是相对容易地就能够推导和建模的不信可以试试比较一下是五
十个节点的指针树还是五十行代码的流程图更清楚明了或者比较一下究竟用一个数组初始化器来表示转换表还是用switch语句更清楚明了呢?可以看
出不同的方式在透明性和清晰性方面具有非常显著的差别参见Rob Pike的原则
数据要比编程逻辑更容易驾驭所以接下来如果要在复杂数据和复杂代码中选择一个宁愿选择前者更进一步在设计中你应该主动将代码的复杂度转移到数据之中去
此种考量并非Unix社区的原创但是许多Unix代码都显示受其影响特别是C语言对指针使用控制的功能促进了在内核以上各个编码层面上对动态修改引用结构在
结构中用非常简单的指针操作就能够完成的任务在其它语言中往往不得不用更复
杂的过程才能完成(我们将在第章再讨论这些技术)
通俗原则接口设计避免标新立异
(也就是众所周知的最少惊奇原则)
最易用的程序就是用户需要学习新东西最少的程序——或者换句话说最易用的程序就是最切合用户已有知识的程序
因此接口设计应该避免毫无来由的标新立异和自作聪明如果你编制一个计算器程序+应该永远表示加法而设计接口的时候尽量按照用户最可能熟悉的同样功能接口和相似应用程序来进行建模
关注目标受众他们也许是最终用户也许是其他程序员也许是系统管理员对于这些不同的人群最少惊奇的意义也不同
关注传统惯例Unix世界形成了一套系统的惯例比如配置和运行控制文件的格式命令行开关等等这些惯例的存在有个极好的理由缓和学习曲线应该学会并使用这些惯例
(我们将在第章和第章讨论这些传统惯例)
最小立异原则的另一面是避免表象相似而实际却略有不同这会极端危险因为表象相似往往导致人们产生错误的假定所以最好让不同事物有明显区别而不要看起来几乎一模一样
—Henry Spencer
缄默原则如果一个程序没什么好说的就保持沉默
Unix中最古老最持久的设计原则之一就是若程序没有什么特别之处可讲就保持沉默行为良好的程序应该默默工作决不唠唠叨叨碍手碍脚沉默是金
沉默是金这个原则的起始是源于Unix诞生时还没有视频显示器在年的缓慢的打印终端每一行多余的输出都会严重消耗用户的宝贵时间现在这种情况已不复存在一切从简的这个优良传统流传至今
我认为简洁是Unix程序的核心风格一旦程序的输出成为另一个程序的输入就很容易把需要的数据挑出来站在人的角度上来说――重要信息不应该混杂在冗长的程序内部行为信息中如果显示的信息都是重要的那就不用找了
—Ken Arnold
设计良好的程序将用户的注意力视为有限的宝贵资源只有在必要时才要求使用
(我们将在第章末尾进一步讨论缄默原则及其理由)
补救原则: 出现异常时马上退出并给出足量错误信息
软件在发生错误的时候也应该与在正常操作的情况下一样有透明的逻辑最理想的情况当然是软件能够适应和应付非正常操作而如果补救措施明明没有成功却悄无声息地埋下崩溃的隐患直到很久以后才显现出来这就是最坏的一种情况
因此软件要尽可能从容地应付各种错误输入和自身的运行错误但是如果做不到这一点就让程序尽可能以一种容易诊断错误的方式终止
同时也请注意Postel的规定[]宽容地收谨慎地发Postel谈的是网络服务程序但是其含义可以广为适用就算输入的数据很不规
范一个设计良好的程序也会尽量领会其中的意义以尽量与别的程序协作然后要么响亮地倒塌要么为工作链下一环的程序输出一个严谨干净正确的数据
然而也请注意这条警告
最初HTML文档推荐宽容地接受数据结果因为每一种浏览器都只接受规范中一个不同的超集使我们一直倍感无奈要宽容的应该是规范而不是它们的解释工具
—Doug McIlroy
McIlroy 要求我们在设计时要考虑宽容性而不是用过分纵容的实现来补救标准的不足否则正如他所指出的一样一不留神你会死得很难看
经济原则: 宁花机器一分不花程序员一秒
在Unix早期的小型机时代这一条观点还是相当激进的(那时机器要比现在慢得多也贵得多)如今随着技术的发展开发公司和大多数用户(那些需要对核爆炸进行建模或处理三维电影动画的除外)都能够得到廉价的机器所以这一准则的合理性就显然不用多说啦!
但不知何故实践似乎还没完全跟上现实的步伐如果我们在整个软件开发中很严格的遵循这条原则的话大多数的应用场合都应该使用高一级的语言如
PerlTclPythonJavaLisp甚至shell——这些语言可以将程序员从自行管理内存的负担中解放出来(参见
[Ravenbrook])
这种做法在Unix世界中已经开始施行尽管Unix之外的大多数软件商仍坚持采用旧Unix学派的C(或C++)编码方法本书会在后面详细讨论这个策略及其利弊权衡
另一个可以显著节约程序员时间的方法是教会机器如何做更多低层次的编程工作这就引出了……
生成原则: 避免手工hack尽量编写程序去生成程序
众所周知人类很不善于干辛苦的细节工作因此程序中的任何手工hacking都是滋生错误和延误的温床程序规格越简单越抽象设计者就越容易做对由程序生成代码几乎(在各个层次)总是比手写代码廉价并且更值得信赖
我们都知道确实如此(毕竟这就是为什么会有编译器解释器的原因)但我们却常常不去考虑其潜在的含义对于代码生成器来说需要手写的重复而麻木
的高级语言代码与机器码一样是可以批量生产的当代码生成器能够提升抽象度时——即当生成器的说明性语句要比生成码简单时使用代码生成器会很合算而
生成代码后就根本无需再费力地去手工处理了
在Unix传统中人们大量使用代码生成器使易于出错的细节工作自动化Parser/Lexer生成器就是其中的经典例子而makefile生成器和GUI界面式的构建器(interface builder)则是新一代的例子
(我们会在第章讨论这些技术)
优化原则: 雕琢前先得有原型跑之前先学会走
原型设计最基本的原则最初来自于Kernighan 和 Plauger 所说的%的功能现在能实现比%的功能永远实现不了强做好原型设计可以帮助你避免为蝇头小利而投入过多的时间
由于略微不同的一些原因Donald Knuth(程序设计领域中屈指可数的经典著作之一《计算机程序设计艺术》的作者)广为传播普及了这样的观点过早优化是万恶之源[]他是对的
还不知道瓶颈所在就匆忙进行优化这可能是唯一一个比乱加功能更损害设计的错误从畸形的代码到杂乱无章的数据布局牺牲透明性和简洁性而片面追求
速度内存或者磁盘使用的后果随处可见滋生无数bug耗费以百万计的人时——这点芝麻大的好处远不能抵消后续排错所付出的代价
经常令人不安的是
过早的局部优化实际上会妨碍全局优化(从而降低整体性能)在整体设计中可以带来更多效益的修改常常会受到一个过早局部优化的干扰结果出来的产品既性能低劣又代码过于复杂在Unix世界里有一个非常明确的悠久传统(例证之一是Rob Pike以上的评论 另一个是Ken
Thompson关于穷举法的格言)先制作原型再精雕细琢优化之前先确保能用或者先能走再学跑极限编程宗师Kent
Beck从另一种不同的文化将这一点有效地扩展为先求运行再求正确最后求快
所有这些话的实质其实是一个意思先给你的设计做个未优化的运行缓慢很耗内存但是正确的实现然后进行系统地调整寻找那些可以通过牺牲最小的局部简洁性而获得较大性能提升的地方
制作原型对于系统设计和优化同样重要——比起阅读一个冗长的规格说明判断一个原型究竟是不是符合设想要容易得多我记得Bellcore有一位开
发经理他在人们还没有谈论快速原型化和敏捷开发前好几年就反对所谓的需求文化他从不提交冗长的规格说明而是把一些shell脚本和
awk代码结合在一起使其基本能够完成所需要的任务然后告诉客户派几个职员来使用这些原型问他们是否喜欢如果喜欢他就会说在多少多少个月之
后花多少多少的钱就可以获得一个商业版本他的估计往往很精确但由于当时的文化他还是输给了那些相信需求分析应该主导一切的同行
—Mike Lesk
借助原型化找出哪些功能不必实现有助于对性能进行优化那些不用写的代码显然无需优化目前最强大的优化工具恐怕就是delete键了
我最有成效的一天就是扔掉了行代码
—Ken Thompson
(我们将在第章对相关内容进行深一步讨论)
多样原则:决不相信所谓不二法门的断言
即使最出色的软件也常常会受限于设计者的想象力没有人能聪明到把所有东西都最优化也不可能预想到软件所有可能的用途设计一个僵化封闭不愿与外界沟通的软件简直就是一种病态的傲慢
因此 对于软件设计和实现来说Unix传统有一点很好即从不相信任何所谓的不二法门Unix奉行的是广泛采用多种语言开放的可扩展系统和用户定制机制
扩展原则: 设计着眼未来未来总比预想快
如果说相信别人所宣称的不二法门是不明智的话那么坚信自己的设计是不二法门简直就是愚蠢了决不要认为自己找到了最终答案因此要为数据格式和代
码留下扩展的空间否则就会发现自己常常被原先的不明智选择捆住了手脚因为你无法既要改变它们又要维持对原来的兼容性
设计协议或是文件格式时应使其具有充分的自描述性以便可以扩展一直总是要么包含进一个版本号要么采用独立自描述的语句按照可以随时插
入新的换掉旧的而不会搞乱格式读取代码的方法组织格式Unix经验告诉我们稍微增加一点让数据部署具有自描述性的开销就可以在无需破坏整体的情况
下进行扩展你的付出也就得到了成千倍的回报
设计代码时要有很好的组织让将来的开发者增加新功能时无需拆毁或重建整个架构当然这个原则并不是说你能随意增加根本用不上的功能而是建议在
编写代码时要考虑到将来的
需要使以后增加功能比较容易程序接合部要灵活在代码中加入如果你需要……的注释有义务给之后使用和维护自己编写的代码的人做点好事
也许将来就是你自己来维护代码而在最近项目的压力之下你很可能把这些代码都遗忘了一半所以设计为将来着眼节省的有可能就是自己的精力