`
javasogo
  • 浏览: 1765933 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

读《Unix编程艺术》笔记

阅读更多

http://blog.sina.com.cn/s/blog_4c451e0e0100d5be.html

读《Unix编程艺术》笔记(一)

1)行为的最终逻辑被尽可能推后到使用端;
2)最终用户永远比操作系统设计人员更清楚他们究竟需要什么;
3)用错误的方式解决正确的问题 总比用正确的方法解决错误的问题好;
注:正确提出问题等于正确解决问题的一半。
4)只提供机制不提供方针的哲学能使Unix长久保鲜;
注:机制:有机体的构造、功能及其相互关系。
方针:指导事业向前发展的纲领。

纲领:正式表述出来、严格信奉和坚持的原则、条例、意见和教训的条文或概要。
也就是说将 逻辑控制 与 功能实现 分开。
5)POSIX是Portable Operating System Interface of Unix的缩写。
6)Unix实现代码幕后的同僚复审这个强力传统。
注:“大教堂”(集权、封闭、受控、保密)和
“集市”(分权、公开、精细的同僚复审)两种开发模式。
7)要作为一个有效的网络专家,对Unix及其文化的理解绝对是必不可少的。
8)Unix将重点放在尽力使各个程序接口相对小巧简洁正交--这也是另一个提高灵活性的方面。
注:
在计算技术中,该术语用于表示某种不相依赖性或是解耦性。
如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。
在设计良好的系统中,数据库代码与用户界面是正交的:
你可以改动界面,而不影响数据库;更换数据库,而不用改动界面。
9)它鼓励那种分清轻重缓急的感觉,以及怀疑一切的态度,并鼓励你以幽默达观的态度驿待这些。
10)Unix管道的发明人Doug Mcllroy曾经说过:
1>让每个程序就做好一件事。
如果有新任务,就重新开始,不要往原程序中加入新功能而搞得复杂。
2>假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序还是未知的。
1.输出中不要有无关的信息干扰。
2.避免使用严格的分栏格式和二进制格式输入
3.不要坚持使用交互式输入
3>尽可能早地将设计和编译的软件投入试用,哪怕是操作系统也不例外。
理想情况下,应该是在几星期内拙劣的代码别犹豫扔掉重写
 4>优先使用工具而不是拙劣的帮助来减轻编程任务的负担。
后来他这样总结道:
1.一个程序只做一件事,并做好。
2.程序要能协作。
3.程序要能处理文本流,因为这是最通用的接口。
11)C 语言大师Rob Pike
曾经说过
1>你无法断定程序会在什么地方耗费运行时间。
瓶颈经常出现在想不到的地方,所以别急于胡乱找个地方改代码
除非你已经证实那儿就是瓶颈所在。
2>估量。
在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度
3>花哨的算法在 n很小时通常很慢,而 n通常很小。
花哨算法的常数复杂度很大。
除非你确定 n总是很大,否则不要用花哨算法。
即使 n很大,也优先考虑原则2。
4>花哨的算法比简单算法更容易出bug、更难实现。
尽量使用简单的算法配合简单的数据结构
Ken Thompson 对此作了强调:拿不准就穷举
5>数据压倒一切。
如果已经选择了正确的数据结构并且把一切都组织得井井有条,
正确的算法也就不言自明。
编程的核心是数据结构,而不是算法
“给我看流程图而不让我看数据表,我仍会茫然不解;
如果给我看数据表,通常就不需要流程图了;
数据表是能够说明问题的了。”--
Rob Pike 如是说。
12)Unix哲学整体概括如下:
1>模块原则:使用简洁的接口拼合简单的部件。
Brian Kernighan 曾经说过的:“计算机编程的本质就是控制复杂度”。
要编制复杂软件而又不至于一败涂地的唯一方法就是降低其整体复杂度,
用清晰的接口把若干简单的模块组合成一个复杂软件
如此一来,多数问题只会局限于某个局部,
那么就还有希望对局部进行改进而不至牵动全身
2>清晰原则:清晰胜于机巧。
在写程序时,要想到你不是写给执行代码的计算机看的,而是给人,
将来阅读维护源码的人,尤其是说不定若干年后回过头来修改这些代码的人可能恰恰就是你自己。
就算你将永远不再修改这些代码,也应该尽量做到不让替你维护代码的人因这些代码而骂你。
3>组合原则:设计时考虑拼接组合。
在输入输出方面,Unix传统极力提倡采用简单文本化面向流设备无关的格式。
每个部分单独成为一块,然后用一个简单的命令流或者是应用协议将其组合在一起。
4>分离原则:策略同机制分享,接口同引擎分享。
把策略同机制揉成一团有两个负面影响:
一来会使策略变得死板,难以适应用户需求的改变
二来也意味着任何策略的改变都极有可能动摇机制
实现方法:
一是,逻辑控制->用脚本语言来撰写。功能实现->用编译语言来撰写。
二是,将应用程序分成可以协作的前端进程(实现策略) 和 后端进程(实现机制),
通过套接字上层的专用应用协议进行通讯。
以上两个方法,层次化分相同,交互方式不同而已。
5>简洁原则:设计要简洁,复杂度能低则低。
恶性循环陷阱:比别人花哨的方法就是把自己变得更花哨。
要避免如此陷阱,唯一的方法就是鼓励以简洁为美,人人对庞大复杂的东西群起而攻之,
这是一个非常看重简单解决方案的工程传统,总是设法将程序系统分解为几个能够协作的小部分
并本能地抵制任何用过多噱头来粉饰程序的企图。
6>吝啬原则:除非确无它法,不要编写庞大的程序。
‘大’有两重含义:
一曰:体积大
长篇幅的代码本身就给维护者很大的心理压力,且定位、阅读、理解均有不便。
二曰:复杂程度高
说出的话,写出的语,有时还不能让人尽解,更何况非母语的专业程序语言了!
非常规的逻辑处理技巧,无异于是给你的代码加了壳,让维护者无从下手。
7>透明原则:设计要可见,以便审查和调试。
一个特别有效的减少调试工作量的方法就是设计时充分考虑:透明性显见性
透明性:是指你一眼就能够看出软件是在做什么以及怎样做的
显见性:是指程序带有监视显示内部状态的功能
这样程序不仅能够运行良好,而且还可以看得出它以何种方式运行。
调试选项的设置应该尽量不要在事后,而应该在设计之初便考虑进去。
这是考虑到程序不但应该能够展示其正确性
应该能够原开发者解决问题的思维模型告诉后来者。
还应该提倡接口简洁,以方便其它程序对其进行操作。
8>健壮原则:健壮源于透明与简洁。
软件的健壮性指软件不仅能在正常情况下运行良好,
而且在超出设计者设想的意外条件下也能够运行良好
因为过于复杂,很难通盘考虑,不能够正确理解一个程序的逻辑,也就不能确信其是否正确,
所以禁不起磕碰,毛病多,不能在出错时修复它
程序的内部逻辑更易于理解带来了让程序健壮的两种方法:透明化简洁化
透明化:就是指一眼就能够看出来是怎么回事。
简洁化:即人们不需要绞尽脑汁就能够推断出所有可能的情况。

避免在代码中出现特例,
BUG通常隐藏在处理特例的代码以及处理不同特殊情况的交互操作部分的代码中。

模块性(代码简朴,接口简洁)是组织程序以达到更简洁目地的方法之一。
9>表示原则:把知识叠入数据以求逻辑质朴而健壮。
数据要比编程逻辑更容易驾驭。
如果要在复杂数据和复杂代码中选择一个,宁愿选择复杂数据。
在设计中,你应该主动将代码的复杂度转移到数据之中去
10>通俗原则:接口设计避免标新立异。(也称之为“最少惊奇原则”)
最易用的程序就是用户需要学习新东西最少的程序或者是最切合用户已有知识的程序。
接口设计应该避免 毫无来由 的标新立异和自作聪明
尽量按照用户最可能熟悉的同样功能接口相似应用程序来进行建模。
关注目标受众,对于不同的人群,最少惊奇的意义也不同。
关注传统惯例,缓和学习曲线,应该学会并使用这些惯例。
避免表象相似而实际却略有不同,因为表象相似往往导致人们产生错误的假定。
所以最好让不同事物有明显区别,而不要看起来几乎一模一样。
11>缄默原则:如果一个程序没什么好说的,就保持沉默。
重要信息不应该混杂在冗长的程序内部行为信息中。
设计良好的程序将用户的注意力视为有限的宝贵资源,只有在必要时才要求使用。
12>补救原则:出现异常时,马上退出并给出足够错误信息。
软件要尽可能从容地应付各种错误输入和自身的运行错误(注:建议输出日志描述错误),
如做不到,也应尽可能的以一种容易诊断错误的方式终止(注:描述原因与运行环境各值)。
悄无声息的埋下崩溃的隐患,直到很久以后才显现出来,这就是最坏的一种情况。
Jonathan Postel谈网络服务程序的规定:“宽容地收谨慎地发”,但是其含义可以广为适用。
13>经济原则:宁花机器一分,不花程序员一秒。
14>生成原则:避免手工处理,尽量编写程序去生成程序。
众所周知,人类很不善于干辛苦的细节工作。
程序中的任何手工处理都是滋生错误和延误的温床。
15>优化原则:雕琢前先要有原型,跑之前先学会走。
《计算机程序设计艺术》的作者 Donald Knuth 广为传播普及这样的观点:
过早优化是万恶之源”。
还不知道瓶颈所在就匆忙进行优化,这可能是唯一一个比乱加功能更损害设计的错误。
过早的局部优化实际上会妨碍全局优化。
“极限编程”宗师 Kent Beck 曾经说过:先求运行,再求正确,最后求快。
先制作原型,再精雕细琢。优化之前先确保能用。
先给你的设计做个未优化的、运行缓慢、很耗内存但是正确的实现,
然后进行系统地调整,寻找那些可以通过牺牲最小的局部简洁性而获得较大性能提升的地方。
16>多样原则:决不相信所谓“不二法门”的断言。
Unix奉行的是广泛采用多种语言、开放的可扩展系统和用户定制机制。
17>扩展原则:设计着眼未来,未来总比预想来得快。
设计协议或是文件格式时,应使其具有充分的自描述性以便可以扩展。
Unix经验告诉我们:
稍微增加一点让数据部署具有自描述性的开销
就可以在无需破坏整体的情况下进行扩展,你的付出也就得到了成千倍的回报。
设计代码时,要有很好的组织,让将来的开发者增加新功能时无需拆毁或重建整个架构
程序接合部要灵活,在代码中加入“如果你需要......”的注释。
设计为将来着眼,节省的有可能就是自己的精力。
13)Unix世界中来自于实践的具体规定:
1>只要可行,一切都应该做成与来源和目标无关的过滤器。
2>数据流应尽可能文本化(这样可以使用标准工具来查看和过滤)。
3>数据库部署和应用协议应尽可能文本化(让人可以阅读和编辑)。
4>复杂的前端(用户界面)和后端应该泾渭分明。
5>如果可能,用 C 编写前,先用解释性语言搭建原型。
6>当且仅当只用一门语言编程会提高程序复杂度时,混用语言编程才比单一语言编程来得好。
7>宽收严发(对接收的东西要包容,对输出的东西要严格)。
8>过滤时,不需要丢弃的信息决不丢。
9>小就是美,在确保完成任务的基础上,程序功能尽可能少。
14)如果不能确定什么是对的,那么就只做最少量的工作确保任务完成就行
至少直到明白什么是对的。
读《Unix编程艺术》笔记(二)

1)前事不忘,后事之师。
2)微软从家庭和小型商用市场赚了数十亿美元的钱,而争战不休的Unix各方却从未决意涉足这些市场。
3)第一个成功的图形操作系统,巩固了微软的统治地位,
为微软在九十年代荡平并最终垄断桌面应用市场创造了条件。
注:通俗原则,也就是最少惊奇原则中提到:(见 笔记一 Unix哲学体系概括第十条)
“最易用的程序就是用户需要学习新东西最少的程序或者是最切合用户已有知识的程序。”
Unix哲学体系中提出这个概念,但没有针对平民大众去落实,
只是在程序接口设计上如此要求开发人员(一小部分人),
而微软把这个哲学概念应用给了大部分人,此所谓“得道者多助,失道者寡助”。
让更多的人为你的发明创造受益,那么你一定也会受到相应的荣誉且或金钱。
4)如果有足够多眼睛的关注,所有的bug都无处藏身。
5)过度依赖任何一种技术或者商业模式都是错误区的,
相反,保持软件及其设计传统的灵活性才是长存之道。
6)别和低价而灵活的方案较劲
读《Unix编程艺术》笔记(三)

1)Unix 至少设立了三层内部边界来防范恶意用户或有缺陷的程序:
1>第一层是内存管理:
Unix用硬件自身的内存管理单元(MMU)来保证各自的进程不会侵入到其它进程的内存地址空间。
2>第二层是为多用户设置的真正权限组,普通用户(非root用户)的进程未经允许,
就不能更改或者读取其他用户的文件。
3>第三层是把涉及关键安全性的功能限制在尽可能小的可信代码块上。
2)客户端操作系统更关注用户的视觉体验,而不是7*24小时的连续正常运行。
3)要打的字越多,愿意用的人就越少。
4)要查找任何东西都既费时又费钱,这往往会阻碍探索性编程降低人们对大型工具包的学习兴趣。
5)程序中使用的图像、声音、文字等资源存储在资源分支中,可以独立于应用程序代码进行修改。
6)干净、强大、面向对象的设计,具有易懂的行为特性和良好的可扩展性。
7)商业和非技术的最终用户,意味着对界面复杂度的容忍度较低。
8)消亡的操作系统的通病:
1>硬件平台的不可植性;
2>不具备良好的网络支持能力;
在一个网络无处不在的世界,即使为单个用户设计的系统也需要多用户能力(多种权限组),
如不具备这一点,任何可能欺骗用户运行恶意代码的网络事务都将颠覆整个系统。
读《Unix编程艺术》笔记(四)

1)软件设计有两种方式:
第一种是设计得极为简洁,没有看得到的缺陷;
第二种是设计得极为复杂,有缺陷也看不出来;
第一种方式的难度要大得多。
2)要编写复杂软件又不至于一败涂地的唯一方法,就是用定义清晰的接口把若干简单模块组合起来,
如此一来,多数问题只会出现在局部,对局部进行改进或优化,而不至于牵动全身。
3)模块化原则的内容:
模块化代码的首要特质就是封装,封装良好的模块:
1>会过多向外部披露自身的细节
2>会直接调用其它模块的实现码
3>会胡乱共享全局数据
模块之间通过应用程序编程接口(API),一组严密且定义良好的程序调用数据结构来通信。
4)应用程序编程接口(API)在模块间扮演双重角色:
在实现层面,阻止各自的内部细节被相邻模块知晓;
在设计层面,真正定义了整个体系;
5)有一种很好的方式来验证API是否设计良好:
1>养成在编码前为应用程序编程接口(API)编写一段非正式书面描述的习惯,
用纯人类语言描述设计把事情说清楚不许摘录任何源代码
这段描述能够帮助你组织思路,本身就是十分有用的模块说明。
最终你可能还想把这些说明做成路标文档(roadmap document),方便以后的人阅读代码。
注:人类语言更能准确清楚的描述设计的目标需求,减少二义性,而且随手即来,便于修改。
用人类语言(母语)描述出来的API,更便于直观的发现不足之处、更便于全面思考。
2>定义 API,为其编写简要注释(编写注释的过程就阐明了代码必须达到的目的)。
3>编写代码。
6)模块分解得越彻底,每一块就越小,API的定义也就越重要。
在模块很小时,bug发生率也出乎意料地增多,这在大量以不同语言实现的各种系统中均是如此。
模块小时,几乎所有复杂度都在于接口;
想要理解任何一部分代码前必须理解全部代码,因此阅读代码非常困难。
注:以功能(解决具体的一个事情)为大模块(相对其它功能模块独立),
并提供调用API(完成这个事情所需的说明就是参数),
内嵌处理具体步骤的小模块(这些小模块之间会存在很多的联系)。
模块以可重复利用为原则定义编写。
7)软件系统应设计成由层次分明的嵌套模块组成,而且每个层面上的模块粒度应降至最低。
8)Brook定律预言道:对一个已经延期的项目,增加程序员只会使该项目更加延期。
项目成本和错误率按程序员人数的平方增长。
9)Hatton的经验数据表明:
假设其它所有因素(如程序员能力)都相同,
200到400之间逻辑行的代码是“最佳点”,可能的缺陷密度达到最小。
根据分析人员对逻辑行的理解以及其它偏好(比如注释是否剔除)的不同,
代码行的统计方法会有较大差别。
Hatton建议逻辑行与物理行之间为两倍的折算率,即最佳物理行数建议应在400至800行之间
这个大小与所用的语言无关,即尽可能用最强大的语言和工具编程
10)在设计API、命令集、协议以及其它让计算机工作的方法时,认真考虑以下两个特性:
1>紧凑性:就是一个设计是否能装进人脑中的特性。
例如:有经验的用户不需要操作手册,那么这个设计就是紧凑的。
       (至少这个设计是涵盖了正常用途的子集)
用着顺手,想法在工作中体现,工作更有成效。
如果一个设计构建在易于理解且利于组合的抽象概念上,
则这个系统能在具有非常强大、灵活的功能的同时保持紧凑。
紧凑不等于“薄弱”,也不等同“容易学习”。
对于某些紧凑设计而言,在掌握其精妙的内在基础概念模型之前,
要理解这个设计相当困难。
一旦理解了这个概念模型,整个视角就会改变,紧凑的奥妙也就十分简单了。
认识心理学:人类短期记忆能够容纳的不连续信息数就是加二或减二
评测API紧凑性的很好经验法则:编程者需要记忆的条目数大于七吗?
C++是反紧凑性的,设计者不指望有哪个程序员能够完全理解C++。
不紧凑的设计也未必注定会灭亡或很糟糕
有些问题域简直是太复杂了,一个紧凑的设计不可能有如此跨度。
有时,为了其它优势,如纯性能和适应范围等,也有必要牺牲紧凑性。
BSD套接字API就是如此。
合理对待紧凑性设计中尽量考虑决不随意抛弃
2>正交性:是有助于使复杂设计也能紧凑的最重要特性之一。
无论你控制的是什么系统,改变每个属性的方法有且只有一个
《程序员修炼之道》一书中针对正交性指出:
正交性缩短了测试和开发的时间,
因为那种既不产生副作用也不依赖其它代码副作用的代码校验起来要容易得多,
需要测试的情况组合要少得多。
如果正交性代码出现问题,把它替换掉而不影响系统其余部分也很容易做到。
最后,正交性代码更容易文档化和复用。
《程序员修炼之道》针对一类特别重要的正交性,明确提出了一条原则:
“不要重复自身(Don't Repeat Yourself)”,意思是说:
“任何一个知识点在系统内都应当有一个 唯一明确权威 的表述。”

常量、表和元数据只应该声明和初始化一次,并导入其它地方。
无论何时重复代码都是危险信号
复杂度是要花代价的,不要为此重复付出。
重复会导致前后矛盾、产生隐微问题的代码,
原因是当你修改重复点时往往只改变了一部分而并非全部
通常,这也意味着你对代码的组织没有想清楚。

碰到重复数据这种情况时,下面问题值得你思考:
1>如果代码中含有重复数据是因为在两个不同的地方必须使用两个不同的表现形式,
那么能否写个函数、工具或代码生成程序,让其中一个由另一个生成,
或两者都来自同一个来源?
2>如果文档重复了代码中的知识点,能否从部分代码中生成部分文档,或者反之,
或者两者都来自同一个更高级的表现形式?
3>如果头文件和接口声明重复了实现代码中的知识点,
是否可以找到一种方法,从代码中生成文件和接口声明?
数据结构也存在类似的原则:“无垃圾,无混淆(No junk, no confusion)”。
“无垃圾”是说数据结构(模型)应该最小化,比如:
不要让数据结构太通用,居然还能表示不可能存在的情况。
“无混淆”是指在真实世界中绝对明确清晰的状态在模型中也应该同样明确清晰
真理的单点性原则:就是提倡寻找一种数据结构,
使得模型中的状态跟真实世界系统的状态能够一一对应
真理的单点性原则得出以下推论:
1>是不是因为缓存了某个计算或查找的中间结果而复制了数据?
仔细考虑一下,这是不是一种过早优化;
陈旧的缓存(以及保持缓存同步所必需的代码层)是滋生bug的温床,
而且如果(实际经常是)缓存管理的开销比预想的要高,甚至可能降低整体性能。
2>如果有大量的样板代码,是不是可以用单一的更高层表现形式生成这些代码,
然后通过提供不同的细致的调制选项生成不同个例呢?
重构代码就是改变代码的结构组织,而不改变其外在行为
要提高设计的紧凑性,有一个精妙但强大的方法:
就是围绕“解决一个定义明确的问题”的强核心算法组织设计,避免臆断和捏造
11)禅曰:依附导致痛苦
软件设计的经验教导我们:
依附于被人忽略的假定将导致非正交、不紧凑的设计,项目不是失败就是成为维护的梦魇。
注:依附:附着;依赖;从属。
就是围绕“解决一个定义明确的问题”的强核心算法组织设计,避免臆断和捏造
12)设计函数或对象的层次结构可以选择两个方向:
1>自底向上:从具体到抽象,从问题域中你确定要进行的具体操作开始,向上进行。
2>自顶向下:从抽象到具体,从最高层面描述整个项目的规格说明或应用逻辑开始,向下进行,
直到各个具体操作。
逐步求精,在拥有具体的工作码前,先在抽象层面上规定程序要做些什么,
然后用实现代码逐步填充。
当以下三个条都成立时,自顶向下不失为好方法:
1>能够精确预知程序的任务;
2>在实现过程中,程序规格不会发生重大变化;
3>在底层,有充分自由来选择程序完成任务的方式;
如果纯粹地自顶向下编程,常常产生在某些代码上的过度投资效应,
这些代码因为接口没有通过实际检验必须废弃或重做
向上的设计者通常先考虑封装具体的任务,以后再按某种相关次序把这些东西粘合在一起
向下的设计者通常先考虑程序的主事件循环,以后才插入具体的事件
程序员尽量双管齐下:
1>一方面以自顶向下的应用逻辑来表达抽象规范
注:应用逻辑:完成某一功能、操作或事物的流程描述。
抽象规范:应用逻辑中按某种相关次序粘合在一起的模块的调用或沟通方式。
2>一方面以自底向上的函数或库来收集底层的域原语
注: 域:范围。
原语:由若干多机器指令构成的完成某种特定功能的一段程序,具有不可分割性。
即原语的执行必须是连续的,在执行过程中不允许被中断。
这样,当高层设计变化时,这些域原语仍然可以重用。
注:具体的接口参数、返回值的定义,要从自底向上的得到。
只有考虑过如何实现具体的任务(如:对具体硬件的操作),
才可能知道需要什么参数返回什么的结果
不同的调用(函数调用式、设置回调函数式等)与沟通方式(网络通讯、共享内存等)的需求,
(有时来自二次开发者用户)都可能会影响封装模块的参数与返回值的形式,
所以只有等自底向上的考虑完成后,才能定义如何转化成需求所要的调用与沟通方式。

简单说:自顶向下的描述主流程或用户操作流程,以逻辑功能来化分出模块并定义调用方式。
自底向上的思考如何实现具体任务,并得到接口参数与返回值。独立且可重复利用。
自顶向下(设计时)可以产生概要设计说明书,描述策略(应用逻辑)。
自顶向下(开发时)可能存在因用户意志或需求而丢弃作废的策略(应用逻辑)
自底向上(设计时)可以产生详细设计说明书,定义接口,便于团队并发工作。
自底向上(开发时)可以产生不因用户意志而改变的机制(域原语集)且可重复利用。
故此,设计时,应先自顶向下而后自底向上;
开发时,应先自底向上而后自顶向下;

因此实际代码往往是自顶向下和自底向上的的综合产物
13)Unix程序员几十年的教训之一就是:
胶合层是个挺讨厌的东西,必须尽可能薄,这一点极为重要。
胶合层用来将东西粘在一起不应该用来隐藏各层的 裂痕不平整
14)薄胶合层原则可以看作是分享原则的升华。
策略(应用逻辑)应该与机制(域原语集)清晰地分离
如果有许多代码既不属于策略又不属于机制,就很有可能除了增加系统的整体复杂度之外,
没有任何其它用处。
15)法国作家、冒险家、艺术家和航空工程师安东尼.德.圣埃克苏佩里说:
完美之道,不在无可增加,而在无可删减
16)如果谨慎而聪明地处理设计,那么常常可以将程序划分开来,
一个是用户界面处理的主要部分(策略),另一个是服务例程的集合(机制),中间不带任何胶合层
当程序要进行图形图像、网络协议包、硬件接口控制块等多种数据结构的具体操作处理时,
这种方法特别合适。
在Unix下,通常是清晰地划分出这种层次,并把服务程序集中在一个库中并单独文档化。
这样的程序中,前端专门解决用户界面高层协议的问题
库分层的一个重要形式是插件,
拥有一套已知入口可在启动以后动态从入口处载入来执行特定任务的库
这种模式必须将调用程序作为文档详备的服务库组织起来,以使得插件可以回调。
17)API应该随试验程序(exerciser program)一起提供,反之亦然!
18)面向对象(OO)设计理念的价值最初在图形系统、图形用户界面和某些仿真程序中被认可。
主要原因之一可能是因为这些领域里很难弄错类型的本体问题。
例如:在GUI和图形系统中,类 和 可操作的可见对象之间有相当自然的映射关系。
如果你发现增加的类和所显示的对象没有明显对应关系,
那么很容易就会注意到胶合层太厚了。
很难发现面向对象(OO)设计理念在这些领域以外还有多少显著优点。
Unix程序员一直比其他程序员对OO更持怀疑态度,原因之一就源于多样性原则。
Unix的模块化传统就是薄胶合层原则,也就是说,硬件和程序顶层对象之间的抽象层越少越好
OO语言鼓励“具有厚重的胶合和复杂层次”的体系。
所有的OO语言都显示出某种使程序员陷入过度分层陷阱的倾向。
对象框架和对象浏览器不能代替良好的设计和文档,但却常常被混为一谈。
过多的层次破坏了透明性:我们很难看清这些层次,无法在头脑中理清代码到底是怎样运行的。
OO抽象的另一个副作用就是程序往往丧失了优化的机会。
Unix程序员知道什么时候不该用OO,就算用OO,他们也尽可能保持对象设计的整洁清晰。
正如《网络风格的元素》(The Elements of Networking Style)的作者Padlipshy所说:
如果你知道自己在做什么三层就足够了;但如果你不知道自己在做什么,十七层也没用。”
19)模块性体现在良好的代码中,但首先来自良好的设计。
以下这些问题,可能会有助于提高代码的模块性:
1>有多少全局变量?全局变量对模块化是毒药,很容易使各模块轻率、混乱地互相泄漏信息。
2>单个模块的大小是否在hatton的“最佳范围”内(即最佳物理行数建议应在400至800行之间)?
如果回答是“不,很多都超过”的话,就可能产生长期的维护问题。
知道与你合作的其他程序员的最佳范围是多少吗?
如果不知道,最好保守点儿,坚持Hatton最佳范围的下限。
3>模块内的单个函数是不是太大了?
与其说这是一个行数计算问题,还不如说是一个内部复杂性问题。
如果不能用一句话来简单描述一个函数与其调用程序之间的约定,这个函数可能太大了。
ken Thompson 说:就我个人而言,
如果局部变量太多,我倾向于拆分子程序。
另一个办法是看代码行是否存在太多缩进。
4>代码是不是有内部API -- 即可作为单元向其他人描述的函数调用集数据结构集
并且每一个单元都封装了某一层次的函数不受其它代码的影响
好的API应是意义清楚,不用看具体如何实现就能够理解的。
一个经典的测试方法:
通过电话向另一个程序员描述。
如果说不清楚,API很可能就是太复杂,设计太糟糕了。
5>API的入口点是不是超过七个?
有没有哪个类有七个以上的方法?
数据结构的成员是不是超过七个?
6>整个项目中每个模块的入口点数量如何分布?
是不是不均匀?
有很多入口点的模块真的需要这么多入口点吗?
模块复杂性往往和入口点数量的平方成正比,这也是简单API优于复杂API的另一个原因。
读《Unix编程艺术》笔记(五)

1)序列化(保存)操作有时也称为列集(marshaling),其反向操作(载入)称为散集(unmarshaling)。
2)互用性、透明性、可扩展性和存储/事务处理的经济性,
这些都是设计文件格式和应用协议时需要考虑的重要方面。
互用性和透明性要求我们在此类设计中要重点考虑数据表达的清晰问题
而不是首先考虑实现的方便性和可能达到的最高性能。
既然二进制协议很难扩展和干净地抽取子集可扩展性当然也青睐文本化协议
事务处理的经济性有时则会提出相反的要求,但我们应看到,
首先考虑这个标准就是一种过早优化,不这么做往往是明智选择。
3)正是文本流的限制帮助了强化封装:
因为文本流不鼓励内容丰富、编码结构密集的复杂表达,也不提倡程序互相干涉内部状态。
4)使用二进制协议的正当理由如下两点:
1)如果要处理大批量的数据集,因而确实关注能否在介质上获得最大位(bit)密度
例如:大图像和多媒体数据的格式有时可以算这种情况的例子。
2)如果非常关心将数据转化为芯片核心结构所必须的时间或指令开销
例如:对延时有严格要求的网络协议有时则可以算是这种情况的例子;
5)Unix文本文件格式的约定:
1>如果可能,以换行符(\n)结束的每一行只存一个记录。
为了和其它操作系统交换数据,最好让文件格式的解析器不受行结束符是LF还是CR-LF的影响。
在这种格式中,习惯上忽略结尾的空白,以防范常见的编辑错误。
2>如果可能,每行不超过80个字符。
3>使用“#”引入注释。
4>支持反斜杠约定(C语言的)。
5>在每行一条记录的格式中,使用冒号或任何连续的空白作为字段分隔符。
加反斜杠的冒号或空格,就不在是分隔符了,而是实际值的一部分了。
6>不要过分区别TAB和whitespace。
7>优先选用十六进制而不是八进制
8>对于复杂的记录,使用“节(stanza)”格式:
一个记录若有多行,就使用%%\n或%\n作为记录分隔符。
(建议用:%%\n构成的一行更为清晰,也不大可能在编辑时无意产生。)
9>在节格式中,要么每行一个记录字段,要么让记录格式和RFC 822电子邮件头类似,
用冒号终止的字段名关键字作为引导字段。
例如:字段名:字段值
10>在节格式中,支持连续行。
11>要么包含一个版本号,要么将格式设计成相互独立的自描述字节块。
将格式设计成自描述字节块无需立即破坏旧代码就可以增加新的块类型
12>注意浮点数取整问题。
将浮点字段作为未处理的二进制格式或字符串编码形式存储
13>不要仅对文件的一部分进行压缩或二进制编码。
6)纯文本、纯二进制或压缩文本都可能是最佳方案,
具体取决于对存储经济性可显性让浏览工具编写起来尽可能简单等问题的权衡考虑。
7)“第二版系统效应(the second system effect)”:
一个系统设计师在设计第一版系统时,往往出于较弱的自信心以及量力而行的考虑
尽量剪裁要实现的功能数量
而当第一版系统成功发布,开始第二版的设计时,随着自信心的增强
大量以前被压制的提议都会重现,设计师在塞入新的功能时也会不再那么保守
这很可能导致产生一个 臃肿 缺乏概念完整性 的第二版系统

读《Unix编程艺术》笔记(六)

1)如果没有阴暗的角落和隐藏的深度,软件系统就是透明的。透明性是一种被动品质
如果实际上能预测程序行为的全部或大部分情况,并能建立简单的心理模型,
这个程序就是透明的,因为可以看透机器究竟在干什么。
2)如果软件系统所包含的功能是为了帮助人们对软件建立正确的“做什么、怎样做”的心理模型而
   设计,这个软件系统就是可显的。可显性是一种主动品质
   举例来说,对用户而言,良好的文档有助于提高可显性;
        对程序员而言,良好的变量和函数名有助于提高可显性。
3)用户喜欢UI中的这些特性,是因为这意味着学习曲线比较平缓。
   当人们说UI“直观”时,很大程序度上是指UI的透明性和可显性。
4)优雅是力量与简洁的结合。
优雅的代码事半功倍。
优雅的代码不仅正确,而且显然正确。
优雅的代码不仅将算法传达给计算机,同时也把见解和信心传递给阅读代码的人。
优雅的代码既透明又可显。
通过追求代码的优雅,我们能够编写更好的代码。
学习编写透明的代码是学习如何编写优雅代码的第一关,很难的一关,
而关注代码的可显性则帮助我们学习如何编写透明的代码。
5)可显性降低进入门槛透明性则减少代码中的存在成本
6)请记住:编写透明、可显的系统而节省的精力,将来完全可能就是自己的财富。
7)要为透明性和可显性而设计,就必须问如下几个问题:
1)这个设计能行吗?
注:能行的几点要求:
1>能否实现目地?
2>是否存在致命缺陷?
3>是否优大于弊?
4>是否便于维护和可以扩展?
2)别人能读懂这个设计吗?
3)这个设计优雅吗?
8)要追求代码的透明,最有效的方法很简单,就是不要在具体操作的代码上叠放太多的抽象层
建议对引起设计问题的特殊、意外的情况进行抽象、简化和概括,并尽量从中分离出来。
9)透明性和可显性同模块性一样,主要是设计的特性而不是代码(风格)的特性。
以下这些问题需要好好思考:
1>程序调用层次中最大的静态深度是多少?
也就是说,不考虑递归,为了建立心理模型来理解代码的操作,人们将要调用多少层?
提示:如果大于四,就要当心了!!!
2>代码是否具有强大、明显的不变性质?
不变性质帮助人们推演代码和发现有问题的情况。
注:不变性质是指一个软件设计中各个操作都保持不变的特性。
如:在大多数数据库中,两个记录的关键字不能相同,这就是不变性。
3>每个API中的各个函数调用是否正交?
或者是否存在太多的特征标志(Magic Flags)和模式位,使得一个调用要完成多个任务?
完全避免模式标志会导致混乱的API,里面包括太多几乎一模一样的函数,
但是频繁使用模式标志更容易产生错误(很多易忘并且易混的模式标记)。
4>是否存在一些顺手可用的关键数据结构或全局唯一的记录器(Scoreboard),
捕获了系统的高层级状态?
这个状态是否容易被形象化和检验,还是分布在数目众多的各个全局变量或对象中,
而难以找到?
5>程序的数据结构或分类和它们所代表的外部实体之间,是否存在清晰的一对一映射?
6>是否容易找到给定函数的代码部分?
不仅单个函数、模块,还有整个代码,需要花多少精力才能读懂?
7>代码增加了特殊情况还是避免了特殊情况?
每一个特殊情况可能对任何其它特殊情况产生影响;
所有隐含的冲突都是Bug滋生的温床。
然而更重要的是,特殊情况使得代码更难理解。
8>代码中有多少个Magic Number(意义含糊的常量)?
通过审查是否很容易查出实现代码中的限制(比如关键缓冲区的大小)?
如果代码很好地解决了上述问题,则代码也可以复杂,且不会对维护人员造成认知负担。
10)经验丰富的Unix用户实际上把调试和探测开关的存在视为良好程序的标志,
不存在则认为程序可能有问题。
11)Unix程序员学到了一种品性,就是宁愿抛弃、重建代码也不愿修补那些蹩脚的代码。
12)一个非常重要的实践就是应用清晰原则:选择简单算法。
另一个重要的实践是要包含开发者手册(Hacker's Guide)。
在发布源码的同时包含指导文档,简略地描述代码的关键数据结构和算法,
这种做法永远得到高度认可。
读《Unix编程艺术》笔记(七)

1)Unix最具特点的程序模块化技法就是将大型程序分解成多个协作进程。
无论在协作进程还是在同一进程的协作子过程层面上,
Unix设计风格都运用“做单件事并做好”的方法,
强调用定义良好的进程间通信或共享文件来连通小型进程一。
因此,Unix操作系统提倡把程序分解成更简单的子进程,并专注考虑这些子进程间的接口
这至少可通过以下三种方法来实现:
1>降低进程生成的开销;
2>提供方法(shellout[shell 执行模块]、I/O重定向、管道、消息传递和套接字)简化进程间通信;
3>提倡使用能由管道和套接字传递的简单、透明的文本数据格式。
尽管将程序划分成协作进程带来了合局复杂度降低的好处,
但代价是我们必须更多地关注在进程间传递信息和命令的协议设计,
在所有各类的软件系统中,接口都是BUG聚焦之地
2)在应用协议语法中运用良好的风格并不难,
真正的挑战不是协议语法而是协议逻辑,设计一个协议,既有充分的表达能力又有防范死锁的能力
几乎同样重要的是,协议必须看得出很有表现力并可防范死锁。
也就是说人们必须能够在头脑中尝试对通信程序的行为建模并验证其正确性

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics