【书摘】Unix 编程艺术
不懂 Unix 的人注定最终还要重复发明一个蹩脚的 Unix。
哲学
Unix 有它自己的文化,有独特的编程艺术,有一套影响深远的设计哲学。理解这些传统,会使你写出更好的软件。
工程和设计的每个分支都有自己的技术文化。在大多数工程领域中,就一个专业人员的素养组成来说,有些不成文的行业素养具有与标准手册及教科书同等重要的地位(并且随着专业人员经验的日积月累,这些经验常常会比书本更重要)。自身工程师们在工作中会积累大量的隐性知识,他们用类似禅宗『教外别传』的方式,通过言传身教传授给后辈。
性能-时间的指数曲线对软件开发过程所引发的结果,就是每过十八个月,就有一半的知识会过时。Unix 并不承诺让你免遭此劫,只是让你的知识投资更趋稳定。因为不变的东西有很多:语言、系统调用、工具用法。
Unix 应用程序通常会提供很多的行为选项和令人眼花缭乱的定制功能。这种倾向也反映出 Unix 的遗风:原本是为技术人员设计的操作系统;同时也表明设计的信念:最终用户永远比操作系统设计人员更清楚他们究竟需要什么。
Unix 具有非常彻底的灵活性。Unix 提供众多的程序粘合手段,这意味着 Unix 基本工具箱的各种组件连纵开合后,将收到单个工具设计者无法想象的功效。
Unix 传统将重点放在尽力使各个接口相对小巧、简洁和正交——这也是另一个提高灵活性的方面。
从设计角度来说,趣味性也绝非无足轻重。对于程序员和开发人员来说,如果完成某项任务所需要付出的努力对他们是个挑战却又恰好还在力所能及的范围内,他们就会觉得很有乐趣。因此,趣味性是一个峰值效率的标志。充满痛苦的开发环境只会浪费劳动力和创造力,这样的环境会在无形之中耗费大量时间、资金还有机会。
Unix 哲学起源于 Ken Thompson 早期关于如何设计一个服务接口简洁、小巧精干的操作系统的思考,同时还从其它许多地方博采众长。
Unix 哲学说来不算是一种正规设计方法。它并不打算从计算机科学的理论高度搞产生理论上完美的软件。
Unix 哲学(同其他工程领域的民间传统一样)是自下而上的,而不是自上而下的。Unix 哲学注重实效,立足于丰富的经验。你不会在正规方法学和标准中找到它,它更接近于隐形的半本能的知识,即 Unix 文化所传播的专业经验。它鼓励那种分清轻重缓急的感觉,以及怀疑一切的态度,并鼓励你以幽默达观的态度对待这些。
Unix 管道的发明人、Unix 传统的奠基人之一 Doug Mcllroy 曾经说过:
- 让每个程序就做好一件事。如果有新任务,就重新开始,不要往原程序中加入新功能而搞得复杂
- 假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序还是未知的。输出中不要有无关的信息干扰。避免使用严格的分栏格式和二进制格式输入。不要坚持使用交互式输入
- 尽可能早地将设计和编译的软件投入试用,哪怕是操作系统也不例外,理想情况下,应该是在几星期内。对拙劣的代码别犹豫,扔掉重写。
- 优先使用工具而不是拙劣的帮助来减轻编程任务的负担。工欲善其事,必先利其器。
后来他这样总结到(来自《Unix 的四分之一世纪》):Unix 哲学是这样的 - 一个程序只做一件事,并做好。程序要能协作。程序要能处理文本流,因为这是最通用的接口。
Rob Pike,最伟大的 C 语言大师之一,在《Notes on C Programming》中从另一个稍微不同的角度描述了 Unix 的哲学
- 原则一:你无法断定程序会在什么地方耗费运行时间。瓶颈经常出现在想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在
- 原则二:估量。在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度
- 原则三:花哨的算法在 n 很小时通常很慢,而 n 通常很小。花哨算法的常数复杂度很大。除非你确定 n 总是很大,否则不要用花哨算法(即使 n 很大,也优先考虑原则 2)
- 原则四:花哨的算法比简单算法更容易出 bug、更难实现。尽量使用简单的算法配合简单的数据结构
- 原则五:数据压倒一切。如果已经选择了正确的数据结构并且把一切都组织得井井有条,正确的算法也就不言自明。编程的核心是数据结构,而不是算法
- 原则六:没有原则六
Ken Thompson - Unix 最初版本的设计者和实现者,禅宗般地对 Pike 的原则作了强调:拿不准就穷举。
Unix 哲学中更多的内容不是这些先哲们口头表述出来的,而是由他们所作的一切和 Unix 本身所作出的榜样体现出来的。从整体上来说,可以概括为以下几点:
- 模块原则:使用简洁的接口拼合简单的部件
- 清晰原则:清晰胜于机巧
- 组合原则:设计时考虑拼接组合
- 分离原则:策略通机制分裂,接口同引擎分类
- 简洁原则:设计要简洁,复杂度能低则低
- 吝啬原则:除非确无它法,不要编写庞大的程序
- 透明性原则:设计要可见,以便审查和调试
- 健壮原则:健壮源于透明与简洁
- 表示原则:把知识叠入数据以求逻辑质朴而健壮
- 通俗原则:接口设计避免标新立异
- 缄默原则:如果一个程序没什么好说的,就沉默
- 补救原则:出现异常时,马上退出并给出足够错误信息
- 经济原则:宁花机器一分,不花程序员一秒
- 生成原则:避免手工 hack,尽量编写程序去生成程序
- 优化原则:雕琢前先要有原型,跑之前先学会走
- 多样原则:绝不相信所谓『不二法门』的断言
- 扩展原则:设计着眼未来,未来总比预想来得快
浓缩为一条铁律:KISS(Keep It Simple, Stupid!)
应用 Unix 哲学的的部分内容
- 只要可行,一切都应该做成与来源和目标无关的过滤器
- 数据流应尽可能文本化(这样可以使用标准工具来查看和过滤)
- 数据库部署和应用协议应尽可能文本化(让人可以阅读和编辑)
- 复杂的前端(用户界面)和后端应该泾渭分明
- 如果可能,用 C 编写前,先用解释性语言搭建原型
- 当且仅当只用一门语言编程会提高程序复杂度时,混用语言编程才比单一语言编程来得好
- 宽收严发(对接收的东西要包容,对输出的东西要严格)
- 过滤时,不需要丢弃的信息决不丢
- 小就是美。在确保完成任务的基础上,程序功能尽可能少
态度也要紧!
看到该做的就去做 - 短期来看似乎是多做了,但从长期来看,这才是最佳捷径。如果不能确定什么是对的,那么就只做最少量的工作,确保任务完成就行,至少直到明白什么是对的。
要良好的运用 Unix 哲学,你就应该不断追求卓越。你必须相信,软件设计是一门技艺,值得你付出所有的智慧、创造力和激情。否则,你的视线就不会超越那些简单、老套的设计和实现;你就会在应该思考的时候急急忙忙跑去编程。你就会在该无情删繁就简的时候把问题复杂化——然后你还会反过来奇怪你的代码怎么会那么臃肿、那么难以调试。
要良好地运用 Unix 哲学,你应该珍惜你的时间决不浪费。一旦某人已经解决了某个问题,就直接拿来利用,不要让骄傲或偏见拽住你又去重做一遍。永远不要蛮干;要多用巧劲,省下力气到需要的时候再用,好钢用在刀刃上。善用工具,尽可能将一切都自动化。
软件设计和实现应该是一门充满快乐的艺术,一种高水平的游戏。要良好地运用 Unix 哲学,你需要具备这种态度。你需要用心。你需要去游戏。你需要乐于探索。
历史
小型实验原型系统的后继产品往往备受令人讨厌的『第二版效应』折磨。由于迫切希望把所有首次开发时遗漏的功能都添加进去,往往导致设计十分庞大、过于复杂。其实,还有一个因不常遇到而鲜为人知的『第三版效应』:有时候,在第二系统不堪自身重负而崩溃之后,有可能返璞归真,走上正道。
最初的 Unix 就是一个第三系统。
到 1993 年年末,Linux 已经具备有 Internet 能力和 X 系统。整套 GNU 工具包从一开始就内置其中,以提供高质量的开发工具。除了 GNU 工具,Linux 好像一个魅力聚宝盆,囊括了二十年来分散在十几种专有 Unix 平台上的开源软件之精华。
在旧学派的 Unix 开发者中,一部分脑筋活络的人开始注意到,做了多年的平价 Unix 之梦从一个意想不到的方向悄然成真。它就这样从 Internet 的石头缝中跳了出来,浑然天成,以令人惊奇的方式重新规划拼装了 Unix 的传统元素。
Unix 传统是一种隐性的文化,不只是一书袋的技术窍门。这种传统传达着一个有关美和优秀设计的价值体系:里面有它的江湖和侠客。Unix 传统、黑客文化以及开源运动间的关系微妙而复杂。三种隐性文化背后往往是同一群人,然而期间的关系并未因此而简化。但是,从 1990 年以来,Unix 的故事很大程度上成了开源世界的黑客们改变规则、从保守的专有 Unix 厂商手中夺取主动权的故事。因此,今天 Unix 身后的历史,有一半就是黑客的历史。
在 Unix 历史中,最大的规律就是:距开源越接近就越繁荣。任何将 Unix 专有化的企业,只能陷入停滞和衰败。对今后的教训就是:过度依赖任何一种技术或者商业模式都是错误的——相反,保持软件及其设计传统的灵活性才是长存之道。
模块性:保持清晰,保持简洁
模块化原则在这里展开来说就是:要编写复杂软件又不至于一败涂地的唯一方法,就是用定义清晰的接口把若干简单模块组合起来,如此一来,多数问题只会出现在局部,那么还有希望对局部进行改进或优化,而不至于牵动全身。
封装和最佳模块大小
模块化代码的首要特质就是封装。封装良好的模块不会过多向外部披露自身的细节,不会直接调用其他模块的实现码,也不会胡乱共享全局数据。模块之间通过应用程序编写接口(API)——一组严密、定义零号的程序调用和数据结构来通信。这就是模块化原则的内容。
API 在模块间扮演双重角色。在实现层面,作为模块之间的滞塞点(choke point),阻止各自的内部细节被相邻模块知晓;在设计层面,正是 API(而不是模块间的实现代码)真正定义了整个体系。
有一种很好的方式来验证 API 是否设计良好:如果试着用纯人类语言描述设计(不许摘录任何源代码),能否把事情说清楚?
模块分解得越彻底,每一块就越小,API 的定义也就越重要。全局复杂度和受 bug 影响的程度也会相应降低。软件系统应设计成由层次分明的嵌套模块组成,而且每个层面上的模块粒度应降至最低。
紧凑型和正交性
紧凑性就是一个设计是否能装进人脑中的特性。测试软件紧凑性的一个很实用的好方法是:有经验的用户通常需要操作手册吗?如果不需要,那么这个设计就是紧凑的。
在通用编程语言中,C 和 Python 是半紧凑的(Go 也是),Perl/Java/shell 则不是.
在纯粹的正交设计中,任何操作均无副作用。每一个动作(无论是 API 调用、宏调用还是语言运算)只改变一件事,不会影响其它。无论你控制的是什么系统,改变每个属性的方法有且只有一个。
正交性的一个原则就是『不要重复自身(DRY, Don’t Repeat Yourself)』,在本书中,我们更愿意把这个原则称为『真理的单点性(SPOT, Single Point of Truth)』。
重复会导致前后矛盾、产生隐微问题的代码,原因是当你修改重复点时,往往只改变了一部分而非全部。通常,这也意味着你对代码的组织没有想清楚。
常量、表和元数据应该只声明和初始化一次,并导入其它地方。无论何时,重复代码都是危险信号。复杂度是要花代价的,不要为此重复付出。
SPOT 原则就是提倡寻找一种数据结构,使得模型中的状态跟真实世界的状态能够一一对应。
要达到紧凑、正交的设计,就从零开始。禅教导我们:依附导致痛苦;软件设计的经验教导我们:依附于被人忽略的假定将导致非正交、不紧凑的设计,项目不是失败就是成为维护的梦魇。
软件是多层的
一般来说,设计函数或对象的层次结构可以选择两个方向。选择何种方向、何时选择,对代码的分层有着深远的影响。
实际代码往往是自顶向下和自底向上的综合产物。同一个项目中经常同时兼有自顶向下的代码和自底向上的代码。这就导致了『胶合层』的出现。
Unix 程序员几十年的教训之一就是:胶合层是个挺讨厌的东西,必须尽可能薄,这一点极为重要。胶合层用来将东西粘在一起,但不应该用来隐藏各层的裂痕和不平整。
薄胶合层原则可以看作是分离原则的升华。策略(应用逻辑)应该与机制(域原语集)清晰地分离。如果有许多代码既不属于策略又不属于机制,就很有可能除了增加系统的整体复杂度之外,没有任何其他用处。
程序库
Unix 编程风格强调模块性和定义零号的 API,它所产生的影响之一就是:强烈倾向于把程序分解成由胶合层连接的库集合,特别是共享库。
库分层的一个重要形式是插件,即拥有一套已知入口、可在启动以后动态从入口处来执行特定任务的库。这种模式必须将调用程序作为文档详备的服务库组织起来,使得插件可以回调。
Unix 和面向对象语言
OO 设计理念的价值最初在图形系统、图形用户界面和某些仿真程序中被认可。使大家惊讶并逐渐失望的是,很难发现 OO 设计在这些领域以外还有多少显著优点。其中原因值得我们去探究一番。
在 Unix 的模块化传统和围绕 OO 语言发展起来的使用模式之间,存在着某些紧张对立的关系。Unix 程序员一直比其他程序员对 OO 更持怀疑态度,原因之一就源于多样性原则。
所有的 OO 语言都显示出某种使程序员陷入过度分层陷阱的倾向。对象框架和对象浏览器并不能代替良好的设计和文档,却常常被混为一谈。过多的层次破坏了透明性:我们很难看清这些层次,无法在头脑中理清代码到底是怎样运行的。简洁、清晰和透明原则统统被破坏了,结果代码中充满了 bug,始终存在维护问题。
可能正是因为许多编程课程都把厚重的软件分层作为实现表达原则的方法来教授,这种趋势还在恶化。根据这种观点,拥有很多类就等于在数据中嵌入了很多知识。问题在于,胶合层中的『智能数据』却经常不代表任何程序处理的自然实体——仅仅是胶合物而已。
Unix 风格程序设计所面临的主要挑战就是如何将分离法的优点(将问题从原始的场景中简化、归纳)同代码和设计的薄胶合、浅平透层次结构的优点相结合。
模块式编码
模块性体现在良好的代码中,但首先来自良好的设计。在编写代码时,问问自己以下问题:
- 有多少全局变量?全局变量对模块化是毒药,很容易使各模块轻率、混乱地互相泄露信息
- 单个模块的大小是否在 Hatton 的『最佳范围』内,也就是 200~400 行。知道自己的『最佳范围』是多少吗?知道与你合作的其他程序员的最佳范围是多少吗?如果不知道,最好保守点
- 模块内的单个函数是不是太大了?与其说这是一个行数计算问题,还不如说是一个内部复杂性问题。如果不能用一句话来简单描述一个函数与其调用程序之间的约定,这个函数可能太大了
- 代码是不是有内部 API——即可作为单元向其他人描述的函数调用集和数据结构集,并且每个单元都封装了某一层次的函数,不受其他代码的影响?好的 API 应该是意义清除,不用看具体如何实现就能够理解的。
- API 的入口点是不是超过七个?有没有哪个类有七个以上的方法?数据结构的成员是不是超过七个?
- 整个项目中每个模块的入口点数量如何分布?是不是不均匀?有很多入口点的模块真的需要这么多入口点吗?模块复杂性往往和入口点数量的平方成正比——这也是简单 API 优于复杂 API 的另一个原因
文本化:好协议产生好实践
互用性、透明性、可扩展性和存储/事务处理的经济性——这些都是设计文件格式和应用协议时需要考虑的重要方面。互用性和透明性要求我们在此类设计中要重点考虑数据表达的清晰问题,而不是首先考虑实现的方便性和可能达到的最高性能。
文本化的重要性
管道和套接字即可以传输文本也可以传输二进制数据。但文本流是非常有用的通用格式,因为人无需专门工具就可以很容易地读写和编辑文本流,这些格式是透明的。
数据文件元格式
数据文件元格式是一套句法和词法约定,这套约定或者已经正式标准化,或者已经通过实践得到了充分的确定,已有标准服务库来处理列集和散集操作。
Unix 已经形成或采纳了适合多种应用程序的不同元格式。尽可能使用这些元格式是个好习惯。第一个好处是使用服务库可以避免编写大量的用户解析代码和生成代码。但最重要的好处还是开发者甚至很多用户都能立即认出这些格式,有亲切感,这就减少了学习新程序的磨合成本(friction cost)。
『传统 Unix 工具』指 grep
, sed
, awk
和 cut
- DSV 风格
- DSV 表示 Delimiter-Separated Values(分隔符分隔值)。在 Unix 中,对字段值可能包含空格的 DSV 格式,冒号是默认的分隔符。这种风格的数据文件一般应通过反斜杠
\
转义符支持在数据域中包含冒号。 - 事实上,Microsoft 版的 CSV 是一个如何设计文本文件格式的典型反面例子
- DSV 表示 Delimiter-Separated Values(分隔符分隔值)。在 Unix 中,对字段值可能包含空格的 DSV 格式,冒号是默认的分隔符。这种风格的数据文件一般应通过反斜杠
- RFC 822 格式
- RFC 822 格式源自互联网电子邮件信息采用的文本格式。在这种元格式中,记录属性每行存放一个,有类似邮件头字段名的标记命名,用冒号后接空白作为结束。字段名不得包含空格;通常用横线代替空格。
- Cookie-Jar 格式
- Cookie-jar 格式是
fortune
程序为随机引用数据库而使用的一种格式。这种格式很适合记录只是一堆非结构化文本的情况。这种格式简单使用跟随%%
的新行符(或者有时只有一个%
)。 - 简单的 cookie-jar 格式适用于词以上结构没有自然顺序,而且结构不易区别的文本段,或适用于搜索关键字而不是文本上下文的文本段。
- Cookie-jar 格式是
- Record-Jar 格式
- cookie-jar 记录分隔符和 RFC 822 记录元格式结合得非常好,产生一种我们称之为 record-jar 的格式。Record-jar 格式适合于那些类似 DSV 文件、但又有可变字段数目而且可能伴随无结构文本的字段属性关系集合。
- XML
- XML 非常适合复杂的数据格式。一个优势在于经常无需知道数据语义,仅通过语法检查就能发现形式不良、损坏或者错误生成的数据。最严重的问题是无法很好和传统的 Unix 工具协作。同时 XML 本身也相当庞大,要在所有的标记中找到数据很困难
- Windows INI 格式
- 这种格式可读性好,设计得不错,但和 XML 一样,不能与
grep
或常规 Unix 脚本工具很好地配合使用
- 这种格式可读性好,设计得不错,但和 XML 一样,不能与
Unix 文本文件格式的约定
- **如果可能,以新行符结束的每一行只存一个记录。**这样用文本流工具提取记录就非常容易。为了和其他操作系统交换数据,最好让文件格式的解析器不受行结束符是 LF 还是 CR-LF 的影响。在这种格式中,习惯上忽略结尾的空白,以防范常见的编辑错误
- **如果可能,每行不超过 80 个字符。**这样使格式可以在普通尺寸的终端视窗上浏览。如果很多记录一定要超过 80 个字符,考虑使用分节格式(stanza format)
- **使用
#
引入注释。**能在数据文件中嵌入注解和说明会非常好。最好是把它们作为文件结构的一部分,便可被知道这种格式的工具保存下来。对于解析时不保存的说明,惯例上采用#
作为起始字符 - **支持反斜杠约定。**支持嵌入不可打印控制字符的最自然方法,就是解析 C 语言风格的反斜杠转义。
- **在每行一条记录的格式中。**冒号约定似乎起源于 Unix 的口令文件。如果某个字段必须包含分隔符,使用反斜杠前缀进行转义
- **不要过分区别 tab 和 whitespace。**否则,当用户编辑器的 tab 设置不同时,会产生很多令人头疼的麻烦。这条原则是治愈头痛的良方
- 优先选用十六进制而不是八进制。
- **对于复杂的记录,使用『节(stanza)』格式:一个记录若有多行,就使用
%%\n
或%\n
作为记录分隔符。**在人们肉眼检查文件时,这种分隔符是非常有用而且直观的边界标志 - **在节格式中,要么每行一个记录字段,要么让记录格式和 RFC 822 电子邮件头类似,用冒号终止的字段名关键字作为引导字段。**当字段经常空缺或者超过 80 个字符,或者当记录很稀疏时(如经常有空字段),适用第二种方案。
- **在节格式中,支持连续行。**解释文件时,或者抛弃空格符之后的反斜杠,或者将空格符之后的新行符解释为单个空格;这样,一个很长的逻辑行就能够折叠成多个很短(容易编辑!)的物理行。在这些格式中,习惯上忽略结尾的空格,可防范常见的编辑错误
- **要么包含一个版本号,要么将格式设计成相互独立的自描述字节块。**哪怕只存在一丁点格式发生改变或扩展的可能性,也要包含一个版本号,这样代码才能够有条件地在所有版本上正确运行。换句话说,将格式设计成自描述字节块,无须立即破坏旧代码就可以增加新的块类型
- **注意浮点数取整问题。**由于所用转换库质量的不同,浮点数从二进制转换成文本格式再转换回二进制格式时可能会有精度损失。如果列集/散集的结构中包含浮点数,应该从两个方向都测试一下转换。如果看上去任何一个方向的转换都可能存在取整误差,做好将浮点字段作为未处理器的二进制格式或字符串编码形式转储的准备
- 不要仅对文件的一部分进行压缩或二进制编码。
透明性:来点光
如果没有阴暗的角度和隐藏的深度,软件系统就是透明的。透明性是一种被动品质。如果实际上能预测到程序行为的全部或大部分情况,并能建立简单的心理模型,这个程序就是透明的,因为可以看透机器究竟在干什么。
如果软件系统所包含的功能是为了帮助人们对软件建立正确的『做什么,怎样做』的心理模型而设计,这个软件系统就是可显的。可显性降低进入门槛;透明性则减少代码中的存在成本。
编写透明、可显的系统而节省的精力,将来完全可能就是自己的财富。
要追求代码的透明,最有效的方法很简单,就是不要在具体操作的代码上叠放太多的抽象层。
禅的一个主要教导是,通常我们都透过源于欲望的偏见和成见的迷雾观察世界。要开悟,我们必须循序禅的教导,不仅要『去欲望,少依恋』,还要『如实见』——不要让偏见和成见蒙住了眼。
和禅宗一样,优秀 Unix 代码的简洁依赖于严格自律和高水平技艺,这两者乍看未必会看得出来。透明性是项辛苦的工作,但值得我们努力追求,而且并不为附庸风雅。和禅宗不一样的是,软件需要调试——而且通常在整个使用期都需要不断的维护、向前移植和改写。因此,透明性不仅是一种美学意义上的成功,更是一种胜利,反映在软件整个生命周期上,意味着更低的成本。
透明性和可显性同模块性一样,主要是设计的特性而不是代码的特性。仅仅做对一些底层风格要素,如清晰且统一的代码缩进,或具有良好的变量命名约定,是不够的。这些特性更多与代码中不易硬性规定的特性有关。以下这些问题需要好好思考:
- 程序调用层次中最大的静态深度是多少?提示:如果大于四,就要当心
- 代码是否具有强大、明显的不变性质?不变性质帮助人们推演代码和发现有问题的情况
- 每个 API 中的各个函数调用是否正交?或者是否存在太多的特征标志(magic flags)和模式位,使得一个调用要完成多个任务?完全避免模式标志会导致混乱的 API,里面包括太多一模一样的函数,但是频繁使用模式标志更容易产生错误(很多易忘并且易混的模式标记)
- 是否存在一些顺手可用的关键数据结构或全局唯一的记录器(scoreboard),捕获了系统的高层级状态?这个状态是否容易被形象化和检验,还是分布在数目众多的各个全局变量或对象中,而难以找到?
- 程序的数据结构或分类和它们所代表的外部实体之间,是否存在清晰的一对一映射?
- 是否容易找到给定函数的代码部分?不仅单个函数、模块,还有整个代码,需要花多少精力才能读懂
- 代码增加了特殊情况还是避免了特殊情况?每一个特殊情况可能对任何其它特殊情况产生英系那个;所有隐含的冲突都是 bug 滋生的温床。然而更重要的是,特殊情况是得代码更难理解
- 代码中有多少个 magic number(意义含糊的常量)?通过审查是否很容易查出实现代码中的限制(比如关键缓冲区的大小)?
代码能简单最好。但是如果代码很好地解决了上述问题,则代码也可以很复杂,而且不会对维护人员造成认知负担。
如果作者以外的其他人能够顺利地理解和修改软件,则这个软件就是可维护的。可维护性不仅要求代码能够运行;还要求代码能够遵循清晰原则,并且和人以及计算机成功沟通。
多道程序设计:分离进程为独立的功能
Unix 最具特点的程序模块化技法就是将大型程序分解成多个协作进程。多道程序设计是设计中的蛮荒之地,几乎没有号的实践方针。许多程序员尽管精于判断如何将代码分解成子过程(subroutine),然而最终还是编写出单个庞然大物般的单进程程序,而这些程序往往失败在自身的内部复杂度之上。
无论在协作进程还是在同一进程的协作子过程层面上,Unix 设计风格都运用『做单件事并做好』的方法,强调用定义良好的进程间通信或共享文件来连通小型进程。因此,Unix 操作系统提倡把程序分解成更简单的子进程,并专注考虑这些子进程间的接口。
尽管将程序划分成协作进程带来了全局复杂度降低的好处,但代价是我们必须更多地关注在进程间传递信息和命令的协议设计(在所有种类的软件系统中,接口都是 bug 聚集之地)
配置:迈出正确的第一步
什么应是可配置的?Unix 的回答是『一切』。这种方式产生的程序往往功能强大,专家用户用起来会非常顺手;但它所产生的接口往往选项过多,并且配置文件像杂草一样疯长,从而彻底打击了新手和一般用户。
无论何时想增加配置选项,请考虑以下这些较普遍的问题:
- 能省掉这个功能吗?为什么在加厚手册之外还要加重用户负担?
- 能否用某种无伤大雅的方式改变程序的常规行为从而无需这个选项?
- 这个选项是否花哨没用?是否应该少考虑用户界面的可配置性而多考虑正确性?
- 这个选项附加的行为是否应该用一个独立的程序来代替?
传统上,一个 Unix 程序可以在启动环境的五个地方寻找控制信息:
/etc
下的运行控制文件(或者系统中其他固有位置)- 由系统设置的环境变量
- 用户主目录中的运行控制文件
- 由用户设置的环境变量
- 启动程序的命令行所传递的开关和参数
用户和系统环境变量的共同点是,在必须复制大量应用程序运行控制文件所包含的信息时特别麻烦,而且尤其令人讨厌的是,只要优先选项改变就必须到处去改变信息。
接口:Unix 环境下的用户接口设计模式
我们将使用五种度量标准对接口风格进行分类:简洁、表现力、易用、透明和脚本化能力。
接口设计模式
- 过滤器模式:接受标准输入的数据,转换成某种格式后,再将结果发送到标准输出端。例子
tr
,grep
,cat
- Cantrip 模式:没有输入,没有输出,只被调用一次,产生退出状态数值。一个 cantrip 程序的行为只能由启动条件来控制。例子
clear
,rm
,touch
- 源模式:类似过滤器的程序,不需要输入;它的输出只能在启动条件中控制。例子
ls
,who
,ps
- 接收器模式:类似过滤器的程序,只接纳标准输入而不发送任何东西到标准输出。同样,它对输入端数据的做用行为只能在启动条件中控制。较少用到,一个例子是
lpr
- 编译器模式:既无标准输出也无标准输入;然而它们会将错误信息发送到标准错误端。例子
gcc
ed
模式:前面的模式交互能力极低。这种模式在启动之后需要由用户持续的会话来驱动,ed
来源于 Unix 的行编辑器ed
,其他例子ftp
,sh
- Roguelike 模式:运行在系统控制台,X 终端模拟器或视频显示终端上的游戏,使用全屏幕、支持可视界面风格,但使用字符阵列显示,而非图形和鼠标界面。例子
vi
,emacs
(一般来说 hjkl 固定做光标键)
优化
程序员工具箱中最强大的额优化技术就是不做优化(因为摩尔定律的存在)
有三种常规的策略来减少时延:
- 对可以共享启动开销的事务进行批处理
- 允许事务重叠
- 缓存
工具:开发的战术
- 编辑器:vi 和 Emacs
- 专用代码生成器:yacc 和 lex
- 自动化编译:make
- 版本控制:git
- 性能分析:profiler