Part I Context
Unix哲学基础:
一:一个程序只做一件事,并做好。程序要能协作。程序要能处理文本流,因为这是最通用的接口。
二:
1、让每个程序就做好一件事,如果有新任务就重新开始,不要往新程序中加入功能而搞的复杂。
2、假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序是未知的。输出中不要有无关的信息干扰,避免使用严格的分栏格式和二进制格式输入。不要坚持使用交互式输入。
3、尽可能早将设计和编译的软件投入试用,哪怕是操作系统也不例外,理想情况下应该是几星期内,对抽劣的代码别犹豫,扔掉重写。
4、优先使用工具,而非拙劣的帮助来减轻编程任务的负担,工欲善其事,必先利其器。
三:
原则1:你无法断行程序会在什么地方耗费运行时间。瓶颈经常出现在想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。
原则2:估量。在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度。
原则3:花哨的算法,在n很小的适合通常很慢,而n通常很小。花哨算法的常数复杂度很大,除非你确定n一直很大,否则不要用花哨算法(即使n很大,也要优先考虑原则2)。
原则4:花哨的算法比简单的算法更容易出bug ,更难实现。尽量使用简单的算法配合简单的数据结构。
原则5:数据压倒一切。如果已经选择了正确的数据结构并且把一切组织得井井有条,正确的算法也就不言自明,编程的核心是数据结构,而不是算法。
原则6:没有原则6.
四:拿不准就穷举
Unix哲学的17条原则:
1、模块原则:简洁的接口拼合简单的部件。
2、清晰原则:清晰胜于机巧。
3、组合原则:设计时考虑拼接组合。
4、分离原则:策略同机制分离,接口同引擎分离。
5、简洁原则:设计要简洁,复杂度能低则低。
6、吝啬原则:除非却无他法,不要编写庞大的程序。
7、透明性原则:设计要可见,以便审查和调试。
8、健壮原则:健壮源于透明与简洁。
9、表示原则:把知识叠入数据,以求逻辑质朴而健壮。
10、通俗原则:接口设计避免标新立异。
11、缄默原则:如果一个程序没什么好说的,就沉默。
12、补救原则:出现异常时,马上退出,并给出足够错误信息。
13、经济原则:宁花机器一分,不花程序员一秒。
14、生成原则:避免手工hack,尽量编写程序去生成程序。
15、优化原则:雕琢前先要有原型,跑之前先学会走。
16、多样原则:绝不相信所谓“不二法门”的断言。
17、扩展原则:设计着眼未来,未来总是比预想来得快。
Unix哲学之一言以蔽之:KISS
Keep it simple,stupid!
应用unix哲学:
1、只要可行,一切都应该做成与来源和目标无关的过滤器。
2、数据流应尽可能的文本化(这样可以用标准工具来查看和过滤)。
3、数据库部署和应用协议应尽可能文本化(让人阅读和编辑)。
4、复杂的前端(用户界面)和后端应该泾渭分明。
5、如果可能,用c编写前,先用解释性语言搭建原型。
6、当且仅当只用一门编程语言会提高程序复杂度时,混用语言编程才比单一语言编程来得好。
7、宽收严发(对接收的东西要包容,对输出的东西要严格)
8、过滤时,不需要丢弃的消息绝不丢。
9、小就是美。在确保完成任务的基础上,程序功能尽可能的少。
操作系统的风格元素:
1、什么是操作系统的统一性理念
2、多任务能力
3、协作进程(IPC)
4、内部边界
5、文件属性和记录结构
6、二进制文件格式
7、首选用户界面风格
8、目标受众
9、开发的门坎
Part II Design
模块性:保持清晰,保持简洁
**封装和最佳模块大小**:模块化代码的首要特质就是封装,API在模块间扮演双重角色,实现层面作为模块之间的滞塞点,阻止各自的内部细节被相邻模块知晓;在设计层面,正是API真正定义了整个体系。养成在编码前为API编写一段非正式书面描述的习惯,是一个非常好的方法。**模块的最佳大小,逻辑行200-400行,物理行在400-800之间。**
**紧凑性**:就是一个设计能否装入人脑中的特性。紧凑性不等于“薄弱”,如果一个设计构建在易于理解且利于组合的抽象概念上,则 这个系统能在具有非常强大、灵活的功能的同时保持紧凑。紧凑也不等同于“容易学习”:对于某些紧凑 设计而言,在掌握其精妙的内在基础概念模型之前,要理解这个设计相当困难;但一旦理解了这个概念模型,整个视角就会改变,紧凑的奥妙也就十分简单了。紧凑也不意味着“小巧”。即使一个设计良好的系统,对有经验的用户来说没什么特异之处、“一眼”就能看懂,但仍然可能包含很多部分。
**正交性**:重构技术中的很多坏味道,特别是重复代码,是违反正交性的明显例子,“重构的原则性目标就是提高正交性”。
**SPOT原则(single point of truth)**:真理的单点性,任何一个知识点在系统内部都应当有一个唯一、明确、权威的表述。数据结构也存在SPOT原则:“无垃圾,无混淆”(No junk,no confusion),"无垃圾"说明数据结构(模型)应该最小化,“无混淆”指在真实世界中绝对清晰的状态在模型中同样明晰。
**紧凑性和强单一中心**:要提高设计的紧凑性,有一个精妙但强大的方法,就是围绕“解决一个定义明确的问题”的强核心算法组织设计,避免臆断和捏造,将任务的核心形式化,建立明确的模型。
**分离的价值**
**软件是多层的**
**自顶向下**:(a)能够精确预知程序的任务(b)在实现过程中,程序规格不会发生重大变化(c)在底层,有充分自由来选择程序完成任务的方式。
**自底向上**
**胶合层**:顶层的应用逻辑和底层的域原语集必须用胶合逻辑层在进行阻抗匹配。薄胶合层原则可以看做是分离原则的升华。
文本化:好协议产生好实践
文本流是非常有用的通用格式,无需专门工具就可以很容易地读写和编辑文本流,这些格式是透明的。如果担心性能问题,就在协议层面之上或之下压缩文本协议流,最终产生的设计会比二进制协议更干净,性能可能更好。使用二进制协议的唯一正当理由是:如果要处理大批量的数据,因而确实关注能否在介质上获得最大位密度,或者是非常关心将数据转化为芯片核心结构所必须的时间或指令开销。
**数据文件元格式**:
1、DSV风格,DElimiter-Seperated Values
使用分隔符来分隔值,例如/etc/passwd 适合场景:数据为列表,名称(首个字段)为关键字,而且记录通常很短(小于80个字符)
2、RFC 822格式
互联网电子邮件信息采用的文本格式,使用属性名+冒号+值的形式,记录属性每行存放一个,如HTTP 1.1协议。适合场景:任何带属性的或者与电子邮件类似的信息,非常适合具有不同字段集合而字段中数据层次又扁平的记录。
3、Cookie-jar格式。简单使用跟随%%的新行符(或者有时只有一个%)作为记录分隔符,很适用于记录非结构化文本的情况。适合场景:词以上结构没有自然顺序,而且结构不易区别的文本段,或适用于搜索关键字而不是文本上下文的文本段。
4、Record-jar格式,cookie-jar和RFC-822的结合,形如
name:dennis
age:21
%%
这样的格式。适合场景:那些类似DSV文件,但又有可变字段数据而且可能伴随无结构文本的字段属性关系集合。
5、XML格式,适合复杂递归和嵌套数据结构的格式,并且经常可以在无需知道数据语义的情况下仅通过语法检查就能发现形式不良损坏或错误生成的数据。缺点在于无法跟传统unix工具协作。
6、Windows INI格式,形如
[DEFAULT]
account=esr
[python]
directory=/home/ers/cvs/python
developer=1
[sng]
directory=/home/esr/WWW/sng
numeric_id=1001
developer=1
[fetchmail]
numeric_id=4928492
这样的格式.适合场景:数据围绕指定的记录或部分能够自然分成“名称-属性对”两层组织结构。
**Unix文本文件格式的约定:**
1、如果可能,以新行符结束的每一行只存一个记录
2、如果可能,每行不超过80个字符
3、使用”#“引入注释
4、支持反斜杠约定
5、在每行一条记录的格式中,使用冒号或连续的空白作为字段分隔符。
6、不要过分区分tab和whitespace
7、优先使用十六进制而不是八进制。
8、对于复杂的记录,使用“节(stanza)”格式,要么让记录格式和RFC 822电子邮件头类似,用冒号终止的字段名关键字作为引导字段。
9、在节格式中,支持连续行,多个逻辑行折叠成一个物理行
10、要么包含一个版本号,要么将格式设计成相互独立的的自描述字节块。
11、注意浮点数取整。
12、不要仅对文件的一部分进行压缩或者二进制编码。
**应用协议元格式**
1、经典的互联网应用元协议 RFC 3117《论应用协议的设计》,如SMTP、POP3、IMAP等协议
2、作为通用应用协议的HTTP,在HTTP上构建专用协议,如互联网打印协议(IPP)
3、BEEP:块可扩展交换协议,既支持C/S模式,又支持P2P模式
4、XMP-RPC、SOAP和Jabber,基于XML的协议。
透明性:来点光
如果软件系统所包含的功能是为了帮助人们对软件建立正确的“做什么、怎样做”的心理模型而设计,这个软件系统就是可显的。
要追求代码的透明性,最有效的方法是很简单,就是不要在具体操作的代码上叠放太多的抽象层。
OO在其取得成功的领域(GUI、仿真和图形)之所以能成功,主要原因之一可能是因为在这些领域里很难弄错类型的本体问题。例如,在GUI和图形系统中,类和可操作的可见对象之间有相当自然的映射关系。
Unix风格程序设计所面临的主要挑战就是如何将分离法的优点(将问题从原始的场景中简化、归纳)同代码和设计的薄胶合、浅平透层次结构的优点相组合。
**为透明性和可显性而编码:**
1、程序调用层次中(不考虑递归)最大的静态深度如果大于四,就要当心。
2、代码是否具有强大、明显的不变性质(约束)?不变性质帮助人们推演代码和发现有问题的情况。
3、每个API中各个函数调用是否正交?或者是否存在太多的magic flags或者模式位?
4、是否存在一些顺手可用的关键数据结构或全局唯一的记录器,捕获系统的高层次状态?这个状态是否容易被形象化和检验,还是分布在数目众多的各个全局变量或对象中而难以找到?
5、程序的数据结构或分类和它们所代表的外部实体之间,是否存在清晰的一对一映射。
6、是否容易找到给定函数的代码部分?不仅单个函数、模块,还有整个代码,需要花多少精力才能读懂?
7、代码增加了特殊情况还是避免了特殊情况?每一个特殊情况可能对任何其他特殊情况产生影响:所有隐含的冲突都是bug滋生的温床。然而更重要的是,特殊情况使得代码更难理解。
8、代码中有多少个magic number?通过审查是否很容易查出代码中的限制(比如关键缓冲区的大小)?
**隐藏细节和无法访问细节有着重要区别。不要过度保护。**
无论何时碰到涉及编辑某类复杂二进制对象的设计问题时,UNIX传统都提倡首先考虑,是否能够编写一个能够在可编辑的文本格式和二进制格式之间来回进行无损转换的工具?这类工具可称为文本化器(textualizer).
宁愿抛弃、重建代码也不愿修补那些蹩脚的代码。
“代码是活代码、睡代码还是死代码?”活代码周围存在一个非常活跃的开发社团。睡代码之所以“睡着”,经常是因为对作者而言,维护代码的痛苦超过了代码本身的效用。死代码则是睡得太久,重新实现一段等价代码更容易。
多道程序设计:分离进程为独立的功能
无论在协作进程还是在同一进程的协作子过程层面上,Unix设计风格都运用“做单件事并做好的方法“,强调用定义良好的进程间通信或共享文件来连通小型进程,提倡将程序分解为更简单的子进程,并专注考虑这些子进程间的接口,这至少需要通过以下三种方法来实现:
1、降低进程生成的开销(思考下Erlang的process)
2、提供方法(shellout、IO重定向、管道、消息传递和socket)简化进程间通信
3、提倡使用能由管道和socket传递的简单、透明的文本数据格式
**Unix IPC(进程间通信)方法的分类:**
1、将任务转给专门程序,如system(3),popen调用等,称为shellout
2、Pipe、重定向和过滤器,如bc和dc
3、包装器,隐藏shell管线的复杂细节。
4、安全包装器和Bernstein链
5、主/从进程
6、对等进程间通信:
(1)临时文件
(2)信号
(3)系统守护程序和常规信号
(4)socket
(5)共享内存,mmap
远程过程调用(RPC)的缺憾:
1、RPC接口很难做到可显,如果不编写和被监控程序同样复杂的专用工具,也难以监控程序的行为。RPC接口和库一样具有版本不兼容的问题,由于是分布式的,因此更难被追查。
2、类型标记越丰富的接口往往越复杂,因而越脆弱。随着时间的推移,由于在接口之间传递的类型总量逐渐变大,单个类型越来越复杂,这些接口往往产生类型本体蠕变问题。这是因为接口比字符串更容易失配;如果两端程序的本体不能正确匹配,要让它们通信肯定很难,纠错更是难上加难。
3、支持RPC的常见理由是它比文本流方法允许“更丰富”的接口,但是接口的功能之一就是充当阻隔点,防止模块的实现细节彼此泄漏,因此,支持RPC的主要理由同时恰恰证明了RPC增加而不是降低了程序的全局复杂度。
Unix传统强烈赞成透明、可显的接口,这是unix文化不断坚持文本协议IPC的动力。
ESR在这里还谈到XML-RPC和SOAP等协议,认为是RPC和unix对文本流支持的一种融合,遗憾的是SOAP本身也成为一种重量级、不那么透明的协议了,尽管它也是文本协议。
线程是有害的:
线程是那些进程生成昂贵、IPC功能薄弱的操作系统所特有的概念。
尽管线程通常具有独立的局部变量栈,它们却共享同一个全局内存,在这个共享地址空间管理竞争和临界区的任务相当困难,而且成为增加整体复杂度和滋生bug的温床。除了普通的竞争问题之外,还产生了一类新问题:时序依赖。
当工具的作用不是控制而是增加复杂度的时候,最好扔掉从零开始。
微型语言:寻找歌唱的乐符
对软件错误模式进行的大量研究得出的一个最一致的结论是,程序员每百行程序出错率和所使用的编程语言在很大程度上是无关的。更高级的语言可以用更少的行数完成更多的任务,也意味着更少的bug。
防御设计不良微型语言的唯一方法是知道如何设计一个好的微型语言。
**语言分类法:**
对微型语言的功能测试:不读手册可以编写吗?
现代微型语言,要么就完全通用而不紧凑,要么就非常不通用而紧凑;不通用也不紧凑的语言则完全没有竞争力。
**设计微型语言:**
1、选择正确的复杂度,要的是数据文件格式,还是微型语言?
2、扩展和嵌入语言
3、编写自定义语法,yacc和lex
4、慎用宏,宏的主要问题是滥用带来的奇怪、不透明的代码,以及对错误诊断的扰乱。
5、语言还是应用协议。
生成 提升规格说明的层次
**数据比程序逻辑更容易驾驭。**
**数据驱动编程**
数据驱动编程中(对比面向对象):数据不仅仅是某个对象的状态,实际上还定义了程序的控制流;OO首先考虑封装,数据变成更看重编写尽可能少的固定代码。最重要的是把程序逻辑从硬编定的控制结构转移到数据中。
**专用代码生成器**
显示分隔符可以改善隐式分隔符的不容易观察性。
建设性的懒惰是大师级程序员的基本美德之一。
配置:迈出正确的第一步
首先,对于能够可靠地进行自动检测的东西,就不要提供配置开关。
其次,用户不应该看到优化开关。
最后,能用脚本包装器或简单管道实现的任务,就不要用配置开关实现。能简单利用其它程序完成的任务,就不要增加本程序的复杂度。
**无论何时想增加配置选项,请考虑以下这些比较普通的问题:**
能省掉这个功能么?为什么在加厚手册之外还要加重用户负担?
能否用某种无伤大雅的方式改变程序的常规行为从而无需这个选项?
这个选项是否花哨没用?是否应该少考虑用户界面的可配置性而多考虑正确性
这个选项附加的行为是否应该用一个独立的程序来代替
**针对运行控制语法的一些通用的风格规定**
1、支持说明性注释,并以“#”开始
2、不要区别隐匿的空白符
3、把多个空行和注释行视为单个空行
4、词法上把文件视作简单的用空白分隔的标记序列,或多行序列。
5、支持以字符串语法对待内嵌空白符的标记。使用单引号或者双引号作为对称的分隔符。
6、支持反斜杠语法以在字符串中嵌入不可打印字符和特殊字符。
环境变量
**何时使用环境变量**
变量值根据共享点文件或父进程需要向多个子进程传递信息的上下午环境的不同而变化
变量值随点文件不同而频繁改变,但每次启动都不变化。
进程唯一的覆盖必须以不要求改变命令行调用的方式来表述
命令行选项
从-a到-z的命令行选项
**-a**
所有项(all)(不带参数); 添加(append)
**-b**
缓冲区(buffer)大小或块(block)的大小(带参数); 批处理(batch)
**-c**
命令(带参数); 检查(check)(不带参数)
**-d**
调试(debug)(带或不带参数); 删除(delete);目录(directory)
**-D**
定义(define)(带参数)
**-e**
执行(execute)(带参数); 编辑(edit);排除(exclude);表达(expression)
**-f**
文件(file)(带参数); 强制(force)(典型情况下不带参数)
**-h**
表头(head)(通常不带参数); 帮助(help)
**-i**
初始化(initialize)(通常不带参数); 交互(interactive)(通常不带参数)
**-I**
包含(include)(带参数);
**-k**
保留(keep)(不带参数); 杀死(kill)
**-l**
列表(list)(不带参数);长(long);加载(load)(带参数);登录(login);长度(length);锁定(lock)
**-m**
消息(message)(带参数);邮件(mail);模式(mode);修改时间(modification-time)
**-n**
数字(number)(带参数); 否(not)(不带参数)
**-o**
输出(output)(带参数);
**-p**
端口(port)(带参数); 协议(protocol)(带参数)
**-q**
安静(quite)(通常不带参数);
**-r/-R**
递归(recurse)(不带参数); 反向(reverse)(不带参数)
**-s**
缄默(silent)(不带参数);主题(subject)(带参数);大小(size)
**-t**
标记(tag)(带参数);
**-u**
用户(user)(带参数);
**-v**
冗长(verbose)(带或不带参数);版本(version)(不带参数)
**-V**
版本(version)(不带参数)
**-w**
宽度(width)(带参数); 警告(warning)(不带参数)
**-x**
启动调试(带或不带参数);提取(extract)(带参数)
**-y**
是(yes)(不带参数)
**-z**
启动压缩(不带参数)
接口:Unix环境下的用户接口设计模式
两个主题:与其他程序通讯方式的前瞻性设计;最小立意原则
最小立意原则:“少来标新立意”,所有接口设计中的通用原则,并非局限于软件设计。不应理解为在设计中号召机械的保守主义。如有可能,尽量允许用户将接口功能委派给熟悉的程序来完成。不能委派时,那就效仿。目的就是为了减少用户在使用接口时必须学习的复杂过程。
五种度量标准对接口风格进行分类:简洁、表现力、易用、透明、脚本化。
简洁:一个事物处理需要的动作时间及复杂度有较低的上限(可以用击键量、鼠标手势量和需要多少秒的注意力来衡量)
表现力:接口可以触发相当广泛的行为。
易用:易用性与接口要求用户记忆的东西成反比
透明度:用户在使用接口的时候,几乎没有什么问题、数据或程序的相关状态需要记忆
脚步能力:接口能够容易的为其他程序所使用
**Unix接口的设计模式**
1、过滤器模式
牢记Postel原则:宽进严出
在过滤时,不需要的信息也决不丢弃
在过滤时,绝不增加无用数据
2、Cantrip模式
如:clear 没有输入没有输出,只被调用一次,产生退出状态数值
3、源模式
不需要输入,它的输出只能在启动条件中控制。如:ls(1),ps(1),who(1)
4、接收器模式
只接纳标准输入而不发送任何东西到标准输出
5、编译器模式
既无标准输入也无标准输出,会将错误信息发送到标准错误端
6、ed模式
具有简单的交互设计模式:sh,ftp
7、Roguelike模式
Roguelike程序是设计来运行系统控制台、X终端模拟器或视频显示终端上的游戏,使用全屏幕、支持可视化界面风格,但使用字符阵列显示,而非图形和鼠标界面。
8、“引擎和接口分离”模式
配置者/执行者组合
假脱机/守护进程组合
驱动/引擎组合
客户端/服务器组合
CLI服务器组合
9、基于语音的接口模式
优化
1、什么也不做
2、先估量,后优化
3、非定域性之害
4、吞吐量和延迟:三种上个的策略减少时延:对可以共享启动开销的事物进行批处理;允许事物重叠;缓存
复杂度:尽可能简单,但别简单过了头
最简原则暗示:选择需要管理的上下文环境,并且按照边界所允许的最小化方式构建程序。
吝啬原则:只有证实了其他方法行不通时才写庞大程序。
虽然看了两遍,但是很多例子还是没看懂TAT。。。所以先划下重点。。。sigh,这么学习也不是办法啊。。。应该启动CRON,定时重看,嗯。
记于2014年06月17日 EOF
点击评论或查看评论。