Unix 编程艺术

2016-09-05 ALEX LIN 更多博文 » 博客 » GitHub »

Unix 编程艺术

原文链接 http://chaosky.me/2016/09/05/Unix-Art/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


《Unix 编程艺术》:这本书在模块化、文本化、配置、接口、复杂度、优化、可移植性等方面,都提供了Unix/Linux世界所积累的宝贵经验。好的程序可以经受时间、平台与用户的考验,好的编程思想可以经受实践的检验。

读书笔记摘录自 《Unix 编程艺术》

Unix 设计原则

所有的 Unix 哲学浓缩为一条铁律,『KISS』原则:Keep It Simple, Stupid!

<!-- more -->

Unix 管道的发明人、Unix 传统的奠基人之一 Doug Mcllroy

  1. 让每个程序就做好一件事。如果有新任务,就重新开始,不要往原程序中加入新功能而搞得复杂。
  2. 假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序还是未知的。输出中不要有无关的信息干扰。避免使用严格的分栏格式和二进制格式输入。不要坚持使用交互式输入。
  3. 尽可能早地将设计和编译的软件投入试用,哪怕是操作系统也不例外,理想情况下,应该是在几星期内。对拙劣的代码别犹豫,扔掉重写。
  4. 优先使用工具而不是拙劣的帮助来减轻编程任务的负担。工欲善其事必先利其器。

一个程序只做一件事,并做好。程序要能协作。程序要能处理文本流,因为这是最通用的接口。

最伟大的C语言大师之一 Rob Pike

  1. 你无法断定程序会在什么地方耗费运行时间。瓶颈经常出现在想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。
  2. 估量。在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度。
  3. 花哨的算法在 n 很小时通常很慢,而 n 通常很小。花哨算法的常数复杂度很大。除非你确定 n 总是很大,否则不要用花哨算法(即使 n 很大,也优先考虑原则2)。
  4. 花哨的算法比简单算法更容易出 bug,更难实现。尽量使用简单的算法配合简单的数据结构。

    拿不准就穷举。

  5. 数据压倒一切。如果已经选择了正确的数据结构并且把一切都组织得井井有条,正确的算法也就不言自明。编程的核心是数据结构,而不是算法。

    给我看流程图而不让我看数据表,我仍会迷茫不解;如果给我看数据表,通常就不需要流程图了;数据表是够说明问题了。

Unix 哲学

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

计算机编程的本质就是控制复杂度。

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

清晰原则:清晰胜于机巧

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

在选择算法和实现时就应该考虑到将来的可扩展性。而为了取得程序一丁点的性能提升就大幅度增加技术的复杂度和晦涩性,这个买卖做不得——这不仅仅是因为复杂的代码容易滋生 bug,也因为它会使日后的阅读和维护工作更加艰难。

相反,优雅而清晰的代码不仅不容易崩溃——而且更易于让后来的修改者立刻理解。

永远不要去吃力地解读语段晦涩的代码三次。

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

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

在输入输出方面,Unix 传统极力提倡采用简单、文本化、面向流、设备无关的格式。文本刘界面的简洁性加强了工具的封装性。

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

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

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

实行机制,而不是策略。因为策略和机制是按照不同的时间尺度变化的,策略的变化要远远快于机制。GUI 工具包的观感时尚来去匆匆,而光栅操作和组合却是永恒的。

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

简洁而漂亮。总是设法将程序系统分解为几个能够协作的小部分,并本能地抵制任何用过多噱头来粉饰程序的企图。

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

『大』有两重含义:体积大,复杂程度高。程序大了,维护起来就困难。

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

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

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

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

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

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

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

让程序健壮的方法,就是让程序的内部逻辑更易于理解。要做到这一点主要两种方法:透明化和简洁化。

在有异常输入的情况下,保证软件健壮性的一个相当重要的策略就是避免在代码中出现特例。bug 通常隐藏在处理特例的代码以及处理不同特殊情况的交互操作部分的代码中。

模块性(代码简朴,接口简洁)是组织程序以达到更简洁目的的一个方法。

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

即使最简单的程序逻辑让人类来验证也很困难,但是就算是很复杂的数据,对人类来说,还是相对容易地就能够推导和建模的。

数据要比编程逻辑更容易驾驭。所以接下来,如果要在复杂数据和复杂代码中选择一个,宁愿选择前者。更进一步:在设计中,你应该主动将代码的复杂度转移到数据之中去。

特别是 C 语言对指针使用控制的功能,促进了在内核以上各个编码层面上对动态修改引用结构。在结构中用非常简单的指针操作就能够完成的任务,在其他语言中,往往不得不用更复杂的过程才能完成。

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

也就是众所周知的『最少惊奇原则』。

最易用的程序就是用户需要学习新东西最少的程序——或者,换句话说,最易用的程序就是最切合用户已有知识的程序。

接口设计应该避免毫无来由的标新立异和自作聪明;关注目标受众,对于不同的人群,最少惊奇的意义也不同;关注传统惯例,Unix 世界形成了一套系统的惯例,这些惯例的存在有个极好的理由:缓和学习曲线。

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

Unix 中最古老最持久的设计原则之一就是:若程序没有什么特别之处可讲,就保持沉默。行为良好的程序应该默默工作,决不唠唠叨叨,碍手碍脚。沉默是金。

设计良好的程序将用户的注意力视为有限的宝贵资源,只有在必要时才要求使用。

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

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

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

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

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

程序中的任何手工 hacking 都是滋生错误和延误的温床。程序规格越简单抽象,设计者就越容易做对。有程序生成代码几乎(在各个层次)总是比手写代码廉价并且更值得信赖。

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

原型设计最基本的原则:『90%的功能现在能实现,比100%的功能永远实现不了强』。做好原型设计可以帮助你避免为蝇头小利而投入过多的时间。

过早的优化是万恶之源。

还不知道瓶颈所在就匆忙进行优化,这可能是唯一一个比乱加功能更损害设计的错误。从畸形的代码到杂乱无章的数据布局,牺牲透明性和简洁性而片面追求速度、内存或者磁盘使用的后果随处可见。滋生无数 bug,耗费以百万计的人时——这点芝麻大的好处,远不能抵消后续排错所付出的代价。

先制作原型,在精雕细琢。优化之前先确保能用。

先求运行,再求正确,最后求快。

先给你的设计做个未优化的、运行缓慢、很耗内存但是正确的实现,然后进行系统地调整,寻找那些可以通过牺牲最小的局部简洁性而获得较大性能提升的地方。

制作原型对于系统设计和优化同样重要——比起阅读一个冗长的规格说明,判断一个原型究竟是不是符合设想要容易得多。借助原型化找出哪些功能不必实现,有助于对性能进行优化;那些不用写的代码显然无需优化。

多样原则:绝不相信所谓『不二法门』的断言

即使最出色的软件也常常会受限于设计者的想象力。没有人能聪明到把所有东西都最优化,也不可能预想到软件所有可能的用途。设计一个僵化、封闭、不愿与外界沟通的软件,简直就是一种病态的傲慢。

Unix 奉行的是广泛采用多种语言、开放的可扩展系统和用户定制机制。

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

为数据格式和代码留下扩展的空间,否则,就会发现自己常常被原先的不明智选择捆住了手脚,因为你无法既要改变他们又要维持对原来的兼容性。

设计协议或者文件格式时,应使其具有充分的自描述性以便可以扩展。

设计代码时,要有很好的组织,让将来的开发者增加新功能时无需拆毁或者重建整个架构。

运用 Unix 哲学

要良好的运用 Unix 哲学,你就应该不断追求卓越。你必须相信,软件设计时一门技艺,值得你付出所有的智慧、创造力和激情。否则,你的视线就不会超越哪些简单、老套的设计和实现:你就会在应该思考的时候急急忙忙跑去编程。你就会在该无情删繁就简的时候反而把问题复杂化——然后你还会反过来奇怪你的代码怎么会那么臃肿、那么难以调试,

要良好地运用 Unix 哲学,你应该珍惜你的时间绝不浪费。一旦某人已经解决了某个问题,就直接拿来利用,不要让骄傲或偏见拽住你又去重做一遍。永远不要蛮干:要多用巧劲,省下力气到需要的时候在用,好钢用在刀刃上。善用工具,尽可能将一切都自动化。