Unix哲学是这样的:一个程序只做一件事,并做好。程序要能协作。程序要能处理文本流,因为这是最通用的接口。
l
Rob Pike:
原则1:你无法断定程序会在什么地方耗费运行时间。瓶颈经常出现在想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。
原则2:估量。在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度。
原则3:花哨的算法在n很小时通常很慢,而n通常很小。花哨算法的常数复杂度很大。除非你确定n总是很大,否则不要用花哨算法(即使n很大也优先考虑原则2)。
原则4:花哨的算法比简单算法更容易出bug、更难实现。尽量使用简单的算法配合简单的数据结构。
原则5:数据压倒一切。如果已经选择了正确的数据结构并且把一切都组织得井井有条,正确的算法也就不言自明。编程的核心是数据结构,而不是算法。
原则6:没有原则6。
l Ken Thompson对原则4做了强调:拿不准就穷举。
l Unix哲学中更多的内容不是先哲们口头表达出来的,而是由他们所作的一切和Unix本身所作出的榜样体现出来的。从整体来说,可以概括为以下几点:
1.
模块原则:使用简洁的接口拼合简单的部件。
正如Brian
Kernighan曾经说过的:“计算编程的本质就是控制复杂度”。排错占用了大部分的开发时间,弄出一个拿得出手的可用系统,通常与其说出自才华横溢的设计成果,还不如说是跌跌撞撞的结果。
汇编语言、编译语言、流程图、过程化编程、结构化编程、所谓的人工智能、第四代编程语言、面向对象、以及软件开发的方法论,不计其数的解决之道被抛售者吹得神乎其神。但实际上这些都用处不大,原因恰恰在于它们“成功”地将程序的复杂度提升到了人脑几乎不能处理的地步。就像Fred Brooks的一句名言:没有万能药。
要编制复杂软件而又不至于一败涂地的唯一方法就是降低其整体复杂度――用清晰的接口把若干简单的模块组合成一个复杂软件。如此一来,多数问题只会局限于某个局部,那么就还有希望对局部进行改进而不至牵动全身。
2.
清晰原则:清晰胜于机巧。
维护如此重要而成本如此高昂;在写程序时,要想到你不是写给执行代码的计算机看的,而是给人――将来阅读维护源码的人,包括你自己――看的。
在Unix传统中,这个建议不仅意味着代码注释。良好的Unix实践同样信奉在选择算法和实现时就应该考虑到将来的可扩展性。而为了取得程序一丁点的性能提升就大幅度增加技术的复杂性和晦涩性,这个买卖做不得――这不仅仅是因为复杂的代码容易滋生bug,也因为它会使日后的阅读和维护工作更加艰难。
相反,优雅而清晰的代码不仅不容易崩溃――而且更易于让后来的修改者立刻理解。这点非常重要,尤其是说不定若干年后回过头来修改这些代码的人可能恰恰就是你自己。
永远不要去吃力地解读一段晦涩的代码三次。第一次也许侥幸成功,但如果发现必须重新解读一遍――离第一次太久了,具体细节无从回想――那么你该注释代码了,这样第三次就相对不会那么痛苦了。 ――Henry Spencer
3.
组合原则:设计时考虑拼接组合。
如果程序彼此之间不能有效通信,那么软件就难免会陷入复杂度的泥淖。
在输入输出方面,Unix传统极力提倡采用简单、文本化,面向流、设备无关的格式。在经典的Unix下,多数程序都尽可能采用简单过滤器的形式,即将一个输入的简单文本流处理为一个简单的文本流输出。
抛开世俗眼光,Unix程序员偏爱这种做法并不是因为他们仇视图形用户界面,而是因为如果程序不采用简单的文本输入输出流,它们就极难衔接。
Unix中,文本流之于工具,就如同在面向对象环境中的消息之于对象。文本流界面的简洁性加强了工具的封装性,而许多精致的进程间通讯方法,比如远程过程调用,都存在牵扯过多各程序间内部状态的倾向。
要想让程序具有组合性,就要使程序彼此独立。在文本流这一端的程序应该尽可能不要考虑文本流另一端的程序。将一端的程序替换为另一个截然不同的程序,而完全不惊扰另一端应该很容易做到。
GUI可以是个好东西。有时竭尽所能也不可避免复杂的二进制数据格式。但是,在做一个GUI前,
最好还是应该想想可不可以把复杂的交互程序跟干粗活的算法程序分离开,每个部分单独成为一块,然后用一个简单的命令流或者是应用协议将其组合在一起。在构
思精巧的数据传输格式前,有必要实地考察一下,是否能利用简单的文本数据格式:以一点点格式解析的代价,换得可以使用通用工具来构造或解读数据流的好处是
值得的。
当程序无法自然地使用序列化、协议形式的接口时,正确的Unix设计至少是,把尽可能多的编程元素组织为一套定义良好的API。这样,至少你可以通过链接调用应用程序,或者可以根据不同任务的需求粘合使用不同的接口。
4.
分离原则:策略同机制分离,接口同引擎分离。
X系统在设计中的基本抉择是实行“机制,而不是策略”这种做法――使X成为一个通用图形引擎,而将用户界面风格留给工具包或者系统的其它层次来决定。这一点得以证明是正确的,因为策略和机制是按照不同的时间尺度变化的,策略的变化要远远快于机制。GUI工具包的观感时尚来去匆匆,而光栅操作和组合却是永恒的。
所以把策略同机制揉成一团有两个负面影响:一来会使策略变得死板,难以适应用户需求的改变,二来也意味着任何策略的改变都极有可能动摇机制。
相反,将两者剥离,就有可能在探索新策略的时候不足以打破机制。另外,我们也可以更容易为机制写出较好的测试(因为策略太短命,不值得花太多精力在这上面)。
这条设计准则在GUI环境之外也被广泛应用。总而言之,这条准则告诉我们应该设法将接口和引擎剥离开来。
实现这种剥离的一个方法是,比如,将应用按照一个库来编写,这个库包含许多由内嵌脚本语言驱动的C服务程序,而至于整个应用的控制流程则用脚本来撰写而不是用C语言。这种模式的经典例子就是Emacs编辑器,它使用内嵌的脚本语言Lisp解释器来控制用C编写的编辑原语操作。
另一个方法是将应用程序分成可以协作的前端和后端进程,通过套接字上层的专用应用协议进行通讯;前端实现策略,后端实现机制。比起仅用单个进程的整体实现方式来说,这种双端设计方式大大降低了整体复杂度,bug有望减少,从而降低程序的生命周期成本。
5.
简洁原则:设计要简洁,复杂度能低则低。
来自多方面的压力常常会让程序变得复杂(由此代价更高,bug更多),其中一种压力就是来自技术上的虚荣心理。程序员们都很聪明,常常以能玩转复杂东西和耍弄抽象概念的能力为傲,这一点也无可厚非。但正因如此,他们常常会与同行们比试,看看谁能捣鼓出最错综复杂的美妙事物。正如我们经常所见,他们的设计能力大大超出他们的实现和排错能力,结果便是代价高昂的废品。
更为常见的是(至少是商业软件领域里),过度的复杂性往往来自于项目的要
求,而这些要求常常基于当月的推销热点,而不是基于顾客的需求和软件实际能够提供的功能。许多优秀的设计被市场推销所需要的大堆大堆“特性清单”扼杀――
实际上,这些特性功能几乎从未用过。然后,恶性循环开始了:比别人花哨的方法就是把自己变得更花哨。很快,庞大臃肿变成了业界标准,每个人都在使用臃肿不
堪、bug极多的软件,连软件开发人员也不敢敝帚自珍。
无论以上哪种方式,最后每个人都是失败者。
要避免这些陷阱,唯一的方法就是鼓励另一种软件文化,以简洁为美,人人对庞大复杂的东西群起而攻之――这是一个非常看重简单解决方案的工程传统,总是设法将程序系统分解为几个协作的小部分,并本能地抵制任何用过多噱头来粉饰程序的企图。
这就有点Unix文件的意味了。
6.
吝啬原则:除非确无它法,不要编写庞大的程序。
“大”有两重含义:体积大,复杂程度高。程序大了,维护起来就困难。由于人们对花费了大量精力才做出来的东西难以割舍,结果导致在庞大的程序中把投资浪费在注定要失败或者并非最佳的方案上。
7.
透明性原则:设计要可见,以便审查和调试。
因为调试通常会占用四分之三甚至更多的开发时间,所以一开始就多做点工作以减少日后调试的工作量会很划算。一个特别有效的减少调试工作量的方法就是设计时充分考虑透明性和显见性。
软件系统的透明性是指你一眼就能够看出软件是在做什么以及怎样做的。显见性指程序带有监视和显示内部状态的功能,这样程序不仅能够运行良好,而且还可以看得出它以何种方式运行。
设计时如果充分考虑到这些要求会给整个项目全过程都带来好处。至少,调试选项的设置应该尽量不要在事后,而应该在设计之初便考虑进去。这是考虑到程序不但应该能够展示其正确性,也应该能够把原开发者解决问题的思维模型告诉后来者。
程序如果要展示其正确性,应该使用足够简单的输入输出格式,这样才能保证很容易地检验有效输入和正确输出之间的关系是否正确。
出于充分考虑透明性和显见性的目的,还应该提倡接口简洁,以方便其它程序对其进行操作――尤其是测试监视工具和调试脚本。
8.
健壮原则:健壮源于透明和简洁。
软件的健壮性指软件不仅能在正常情况下运行良好,而且在超出设计者设想的意外条件下也能够运行良好。
大多数软件禁不起磕碰,毛病很多,就是因为过于复杂,很难通盘考虑。如果不能够正确理解一个程序的逻辑,就不能确信其是否正确,也就不能在出错的时候修复它。
这也就带来了让程序健壮的方法,就是让程序的内部逻辑更易于理解。要做到这一点有两种方法:透明化和简洁化。
在有异常输入的情况下,保证软件健壮性的一个相当重要的策略就是避免在代码中出现特例。Bug通常隐藏在处理特例的代码以及处理不同特殊情况的交互操作部分的代码中。
上面说过,软件的透明性就是指一眼就能够看出来是怎么回事。如果“怎么回事”不算复杂,即人们不需要绞尽脑汁就能够推断出所有可能的情况,那么这个程序就是简洁的。程序越简洁,越透明,也就越健壮。
模块性(代码简朴,接口简洁)是组织程序以达到更简洁目的的一个方法。另外也有其它的方法可以得到简洁。接下来就是另一个。
9.
表示原则:把知识叠入数据以求逻辑质朴而健壮。
即使最简单的程序逻辑让人类来验证也很困难,但是就算是很复杂的数据,对人类来说,还是相对容易地就能够推导和建模的。
数据要比编程逻辑更容易驾驭。所以接下来,如果要在复杂数据和复杂代码中选择一个,宁愿选择前者。更进一步:在设计中,你应该主动将代码的复杂度转移到数据之中去。
此种考量并非Unix社区的原创,但是许多Unix代码都显式受其影响。特别是C语言对指针使用控制功能,促进了在内核以上各个编码层面上动态修改引用结构。在结构中用非常简单的指针操作就能够完成的任务,在其它语言中,往往不得不用更复杂的过程才能完成。
10.
通俗原则:接口设计避免标新立异。
(也就是众所周知的“最少惊奇原则”)
最易用的程序就是用户需要学习新东西最少的程序――或者,换句话说,最易用的程序就是最切合用户已有知识的程序。
因此,接口设计应该避免毫无来由的标新立异和自作聪明。如果你编制一个计算器程序,“+”应该永远表示加法。而设计接口的时候,尽量按照用户最可能熟悉的同样功能接口和相似应用程序来进行建模。
关注目标受众。他们也许是最终用户,也许是其他程序员,也许是系统管理员。对于这些不同的人群,最少惊奇的意义也不同。
最小立异原则的另一面是避免表象相似而实际却略有不同。这会极端危险,因为表象相似往往导致人们产生错误的假定。所以最好让不同事物有明显区别,而不要看起来几乎一模一样。 ――Henry Spencer
11.
缄默原则:如果一个程序没什么好说的,就保持沉默。
Unix中最古老最持久的设计原则之一就是:若程序没有什么特别之处可讲,就保持沉默。行为良好的程序应该默默工作,决不唠唠叨叨,碍手碍脚。沉默是金。
我认为简洁是Unix程序的核心风格。一旦程序的输出成为另一个程序的输入,就是很容易把需要的数据挑出来。站在人的角度上来说――重要信息不应该混杂在冗长的程序内部行为信息中。如果显示的信息都是重要的,那就不用找了。-Ken Arnold
设计良好的程序将用户的注意力视为有限的宝贵资源,只有在必要时才要求使用。
12.
补救原则:出现异常时,马上退出并给出足够错误信息。
软件在发生错误的时候也应该与在正常操作的情况下一样,有透明的逻辑。最理想的情况当然是软件能够适应和应付非正常操作;而如果补救措施明明没有成功,却悄无声息地埋下崩溃的隐患,直到很久以后才显现出来,这就是最坏的一种情况。
因此,软件要尽可能从容地应付各种错误输入和自身的运行错误。但是,如果做不到这一点,就让程序尽可能以一种容易诊断错误的方式终止。
同时也请注意Postel的规定:“宽容地收,谨慎地发”。Postel谈的是网络服务程序,但是其含义可以广为适用。就算输入的数据很不规范,一个设计良好的程序也会尽量领会其中的意义,以尽量与别的程序协作;然后,要么响亮地倒塌,要么为工作链下一环的程序输出一个严谨干净正确的数据。
然而,也请注意这条警告:
最初HTML文档推荐“宽容地接收数据”,结果因为每一种浏览器都只接受规范中一个不同的超集,使我们一直倍感无奈。要宽容的应该是规范而不是它们的解释工具。 ――
Dong Mcllroy
Mcllory要求我们在设计时要考虑宽容性,则不是用过分纵容的实现来补救标准的不足。否则,正如他所指出的一样,一不留神你会死得很难看。
13.
经济原则:宁花机器一分,不花程序员一秒。
在Unix早期的小型机时代,这一条观点还是相当激进的(那时机器比现在慢得多也贵得多)。如今,随着技术的发展,开发公司和大多数用户(那些需要对核爆炸进行建模或处理三维电影动画的除外)都能够得到廉价的机器,所以这一准则的合理性就显然不用多说啦。
但不知何故,实践似乎还没完全跟上现实的步伐。如果我们在整个软件开发中很严格的遵循这条原则的话,大多数的应用场合都应该使用高一级的语言,如Perl、Tcl、Python、Java、Lisp,甚至Shell――这些语言可以将程序员从自行管理内存的负担中解放出来。
这种做法在Unix世界中已经开始施行,尽管Unix之外的大多数软件商仍坚持采用旧Unix学派的C(或C++)编码方法。
另一个可以显著节约程序员时间的方法是:教会机器如何做更多低层次的编程工作,这就引出了... …
14.
生成原则:避免手工hack,尽量编写程序去生成程序。
众所周知,人类很不善于干辛苦的细节工作。因此,程序中的任何手工hacking都是滋生错误和延误的温床。程序规格越简单越抽象,设计者就越容易做对。由程序生成代码几乎(在各个层次)总是比手写代码廉价并且更值得依赖。
我们都知道确实如此(毕竟这就是为什么会有编译器、解释器的原因),但我
们却常常不去考虑其潜在的含义。对于代码生成器来说,需要手写的重复而麻木的高级语言代码,与机器码一样是可以批量生产的。当代生成器能够提升抽象度时
――即当生成器的说明性语句要比生成码简单时,使用代码生成器会很合算,而生成代码后就根本无需再费力地去手工处理了。
15.
优化原则:雕琢前先要有原型,跑之前先学会走。
原型设计最基本的原则最初来自于Kernighan和Plauger所说的“90%的功能现在能实现,比100%的功能永远实现不了强”。做好原型设计可以帮助你避免为蝇头小利而投入过多的时间。
由于略微不同的一些原因,Donald
Knuth广为普及了这样的观点:“过早优化是万恶之源”。他是对的。
还不知道瓶颈所在就匆忙进行优化,这可能是唯一一个比乱加功能更损害设计的错误。从畸形的代码到杂乱无章的数据布局,牺牲透明性和简洁性而片面追求速度、内存或者磁盘使用的后果随处可见。滋生无数bug,耗费以百万计的人时――这点芝麻大的好处,远不能抵消后续排错所付出的代价。
经常令人不安的是,过早的局部优化实际上会妨碍全局优化(从而降低整体性能)。在整体设计中可以带来更多效益的修改常常会受到一个过早局部优化的干扰,结果,出来的产品既性能低劣又代码过于复杂。
在Unix世界里,有一个非常明确的悠久传统:先制作原型,再精雕细琢。优化之前先确保能用。或者:先能走,再学跑。“极限编程”宗师Kent Beck从另一种不同的文化将这一点有效地扩展为:先求运行,再求正确,最后求快。
所有这些话的实质其实是一个意思:先给你的设计做个未优化的、运行缓慢、很耗内存但是正确的实现,然后进行系统地调整,寻找那些可以通过牺牲最小的局部简洁性而获得较大性能提升的地方。
制作原型对于系统设计和优化同样重要――比起阅读一个冗长的规格说明,判断一个原型究竟是不是符合设想要要容易得多。我记得Bellcore有一位开发经理,他在人们还没有谈论“快速原型化”和“敏捷开发”前好几年就反对所谓的“需求”文化。他从不提交冗长的规格说明,而是把一些Shell脚本和awk代
码结合在一起,使其基本能够完成所需要的任务,然后告诉客户派几个职员来使用这些原型,问他们是否喜欢。如果喜欢,他就会说“在多少多少个月之后,花多少
多少的钱就可以获得一个商业版本”。他的估计往往很精确,但由于当时的文化,他还是输给了那些相信需求分析应该主导一切的同行。 ――Mike
Lesk
借助原型化找出哪些功能不必实现,有助于对性能进行优化;那些不用写的代码显然无需优化。目前,最强大的优化工具恐怕就是delete键了。
我最有成效的一天就是扔掉了1000行代码 ――Ken Thompson
16.
多样原则:决不相信所谓“不二法门”的断言。
即使最出色的软件也常常会受限于设计者的想象力。没有人能聪明到把所有东西都最优化,也不可能预想到软件所有可能的用途。设计一个僵化、封闭、不愿与外界沟通的软件,简直就是一种病态的傲慢。
因此,对于软件设计和实现来说,Unix传统有一点很好,即从不相信任何所谓的“不二法门”。Unix奉行的是广泛采用多种语言、开放的可扩展系统和用户定制机制。
17.
扩展原则:设计着眼未来,未来总比预想来得快。
如果说相信别人所宣称的“不二法门”是不明智的话,那么坚信自己的设计是
“不二法门”简直就是愚蠢了。决不要认为自己找到了最终答案。因此,要为数据格式和代码留下扩展的空间,否则,就会发现自己常常被原先的不明智选择捆住了
手脚,因为你无法既要改变它们又要维持对原来的兼容性。
设计协议或是文件格式时,应使其具有充分的自描述性以便可以扩展。一直,总是,要么包含一个版本号,要么采用独立、自描述的语句,按照可以随时插入新的、换掉旧的而不会搞乱格式读取代码的方法组织格式。Unix经验告诉我们:稍微增加一点让数据部署具有自描述性的开销,就可以在无需破坏整体的情况下进行扩展,你的付出也就得到了成千倍的回报。
设计代码时,要有很好的组织,让将来的开发者增加新功能时无需拆毁或重建整个架构。当然这个原则并不是说你能随意增加根本用不上的功能,而是在编写代码时要考虑到将来的需要,使以后增加功能比较容易。程序接合部要灵活,在代码中加入“如果你需要……”的注释。有义务给之后使用和维护自己编写的代码的人做点好事。
也许将来就是你自己来维护代码,而在最近项目的压力之下你很可能把这些代码都遗忘了一半。所以,设计为将来着眼,节省的有可能就是自己的精力。
l
Unix哲学之一言以蔽之
所有的Unix哲学浓缩了一条铁律,那就是各地编程大师们奉为圭臬的“KISS”原则:Keep It Simple,Stupid!
l
应用Unix哲学
这些富有哲理的原则决不是模糊笼统的泛泛之谈。在Unix世界中,这些原则都直接来自于实践,并形成了具体的规定:
n 只要可行,一切都应该做成与来源和目标无关的过滤器。
n 数据流应尽可能文本化(这样可以使用标准工具来查看和过滤)。
n 数据库部署和应用协议尽可能文本化(让人可以阅读和编辑)
n 复杂的前端(用户界面)和后端应该泾渭分明。
n 如果可能,用C编写前,先用解释性语言搭建原型。
n 当且仅当只用一门语言编程会提高程序复杂度时,混用语言编程才比单一语言编程来得好。
n 宽收严发(对接收的东西要包容,对输出的东西要严格)。
n 过滤时,不需要丢弃的信息决不丢。
n 小就是美。在确保完成任务的基础上,程序功能尽可能少。
毫不奇怪,这些往往与其它传统中最优秀的软件工程实践不谋而合。
l
态度也要紧
看到该做的就去做――短期来看似乎是多做了,但从长期来看,这才是最佳捷径。如果不能确定什么是对的,那么就只做最少量的工作,确保任务完成就行,至少直到明白什么是对的。
要良好的运用Unix哲
学,你就应该不断追求卓越。你必须相信,软件设计是一门技艺,值得你付出所有的智慧、创造力和激情。否则,你的视线就不会超越那些简单、老套的设计和实
现;你就会在应该思考的时候急急忙忙跑去编程。你就会在该无情删繁就简的时候反而把问题复杂化――然后你还会反过来奇怪你的代码怎么会那么臃肿、那么难以
调试。
要良好地运用Unix哲学,你应该珍惜你的时间决不浪费。一旦某人已经解决了某个问题,就直接拿来利用,不要让骄傲或偏见拽住你又去重做一遍。永远不要蛮干;要多用巧劲,省下力气到需要的时候再用,好钢用在刀刃上。善用工具,尽可能将一切都自动作。
软件设计和实现应该是一门充满快乐的艺术,一种高水平的游戏。如果这种态度对你来说听起来有些荒谬,或者令你隐约感到有些困窘,那么请停下来,想一想,问问自己是不是已经把什么给遗忘了。如果只是为了赚钱或是打发时间,你为什么要搞软件设计而不是别的什么呢?你肯定曾经也认为软件设计值得付出激情……
要良好地运用Unix哲学,你需要具备(或者找回)这种态度。你需要用心。你需要去游戏。你需要乐于探索。
l 一个模块的代码数最好控制在200到400之间逻辑行,最佳物理行数大约为400到800之间。
l 《魔数七:加二或减二:人类信息处理能力的局限性》
l 紧凑性就是一个设计是否能装进人脑中的特性。测试软件紧凑性的一个很实用的好方法是:有经验的用户通常需要操作手册吗?如果不需要,那么这个设计就是紧凑的。
l 正交性是有助于使复杂设计也能紧凑的最重要特性之一。在纯粹的正交设计中,任何操作均无副作用;每一个动作(无论是API调用、宏调用还是语言运算)只改变一件事,不会影响其它。无论你控制的是什么系统,改变每个属性的方法有且只有一个。
l 如果一个程序做好一件事之外,顺带还做了其它的事情(我理解这就是副作用,这在大多数情况下就是非正交)的时候既不增加系统的复杂度也不会使系统更易产生bug,就没什么问题(我想这种情况发生在某程序算法的中间产物对其他程序也有价值的时候)。如果副作用扰乱了程序员或用户的思维方式,带来种种不便甚至可怕的结果,这就是出现了非正交性问题。尤其在没有忘记这些副作用时,你总要被迫做额外工作来抑制或修正它们。
l
“不要重复自身(Don’t Repeat Yourself)”意思就是:任何一个知识点在系统内部应当有一个唯一、明确、权威的表述。这个原则也称为“真理的单点性(Single Point of Truth)”(SPOT)。SPOT原则用在数据结构中:“无垃圾,无混淆”(No junk,no confusion)。“无垃圾”是说数据结构(模型)应该最小化,不要让数据结构太通用,居然还能表达不可能存在的情况。“无混淆”是指在真实世界中绝对明确清晰的状态在模型中也应该同样明确清晰。简言之,SPOT原则就是提倡寻找一种数据结构,使得模型中的状态跟真实世界系统的状态能够一一对应。
我们可以从SPOT原则得出以下推论:
n 是不是因为缓存了某个计算或查找的中间结果而复制了数据?仔细考虑一下,这是不是一种过早优化;陈旧的缓存(以及保持缓存同步所必需的代码层)是滋生bug的温床,而且如果(实际经常是)缓存管理的开销比预想的要高,甚至可能降低整体性能。
n 如果有大量重复的样板代码,是不是可以用单一的更高层表现形式生成这些代码,然后通过提供不同的细调选项生成不同个例呢?
l 软件是多层的。分层的方向:一个方向是自底向上,从具体到抽象――从问题域中你确定要进行的具体操作开始,向上进行。另一个方向是自顶向下,从抽象到具体――从最高层面描述整个项目的规格说明或应用逻辑开始,向下进行,直到各个具体操作。
l
自从二十世纪六十年代有关结构化程序设计的论战后,编程新手往往被教导以“正确的方法是自顶向下”:逐步求精,在拥有具体的工作码前,先在抽象层面上规定程序要做些什么,然后用实现代码逐步填充。当以下三个条件都成立时,自顶向下不失为好的方法:(a)能够精确预知程序的任务,(b)在实现过程中,程序规格不会发生重大变化,(c)在底层,有充分自由来选择程序完成任务的方式。
这些条件容易在相对接近最终用户和软件设计的较上层――应用软件编程――
中得到满足。但即便如此,这些前提也常常满足不了。在用户界面经过最终用户测试前,别指望提前知道什么算是字处理软件或绘图程序的“正确”行为方式。如果
纯粹地自顶向下编程,常常产生在某些代码上的过度投资效应,这些代码因为接口没有通过实际检验而必须废弃或重做。
为了应对这种情况,出于自我保护,程序员尽量双管齐下――一方面以自顶向下的应用
逻辑表达抽象规范,另一方面以函数或库来收集底层的域原语,这样,当高层设计变化时,这些域原语仍然可以重用。
无论是否是系统程序员,当你用一种探索的方式编程,想尽量领会你还没有完
全理解的软件、硬件抑或真实世界的现象时,自底向上法看起来也会更有吸引力。它给你时间和空间去细化含糊的规范,同时也迎合了程序员身上人类通有的懒惰天
性――当必须丢弃和重建代码时,与之相比,如果用自顶向下的设计,需要抛弃的代码往往更多。
因此实际代码往往是自顶向下和自底向上的综合产物。同一个项目中经常同时兼有自顶向下的代码和自底向上的代码。这就导致了“胶合层”的出现。
l
胶合层:
当自顶向下和自底向上发生冲突时,其结果往往是一团糟。顶层的应用逻辑和底层的域原语集必须用胶合逻辑层来进行阻抗匹配。Unix程序员几十年的教训之一就是:胶合层是个挺讨厌的东西,必须尽可能薄,这一点极为重要。胶合层用来将东西粘在一起,但不应该用来隐藏各层的裂痕和不平整。
一个容易产生bug的胶合层还不是设计所能遇到的最坏命运。如果设计者意识到胶合层的存在,并试图围绕自身的一套数据结构或对象把胶合层组织成一个中间层,结果却导致出现两个胶
合层――一个在中间层之上,另一个在中间层之下,那些天资聪慧但经验不足的程序员特别容易掉进这种陷阱;他们将每种类别(应用逻辑、中间层和域原语集)的
基本集都做得很好,就像教科书上的例子一样漂亮,结果却因为整合这些漂亮代码所需的多个胶合层越来越厚,而最终在其中苦苦挣扎。
薄胶合层原则可以看作是分离原则的升华。策略(应用逻辑)应该与机制(域原语集)清晰地分离。如果有许多代码既不属于策略又不属于机制,就很有可能除了增加系统的整体复杂度之外,没有任何其它用处。
l
Unix和面向对象语言
在面向对象的编程中,作用于具体数据结构的函数和数据一起被封装在可视为单元的一个对象中。相反,非OO语言中的模块使数据和作用于该数据的函数的联系变得相当无规律,而且模块间还经常互相泄漏数据或内部细节。
OO设计理念的价值最初在图形系统、图形用户界面和某些仿真程序中被认可。使大家惊讶并逐渐失望的是,很难发现OO设计在这些领域以外还有多少显著优点。其中原因值得我们去探究一番。
在Unix的模块化传系统和围绕OO语言发展起来的使用模式之间,存在着某些紧张对立的关系。Unix程序员一直比其他程序员对OO更持怀疑态度,原因之一就源于多样性原则。OO经常过分推崇为解决软件复杂性问题的唯一正确办法。
Unix的模块化传统就是薄胶合层原则,也就是说,硬件和程序顶层对象之间的抽象层越少越好。这部分是因为C语言的影响。在C语言中模仿真正的对象很费力。正因为这样,堆砌抽象层是一件非常累人的事。这样,C语言中的对象层次倾向于比较平坦和透明。即使Unix程序员使用其它语言,他们也愿意继续沿用Unix模型教给他们的薄胶合\浅分层风格。
OO语言使抽象变得很容易――也许是太容易了。OO语言鼓励“具有厚重的胶合和复杂层次”的体系。当问题域真的很复杂、确实需要大量抽象时,这可能是好事,但如果编码员到头来用复杂的办法来做简单的事情――仅仅是因为他们能够这样做,结果便适得其反。
所有的OO语言都显示出某种使程序员陷入过度分层陷阱的倾向。对象框架和对象浏览器并不能代替良好的设计和文档,但却常常被混为一谈。过多的层次破坏了透明性:我们很难看清这些层次,无法在头脑中理清代码到底是怎样运行的。简洁、清晰和透明原则统统被破坏了,结果代码中充满了晦涩的bug,始终存在维护问题。
可能正是因为许多编程课程都把厚重的软件分层作为实现表达原则的方法来教
授,这种趋势还在恶化。根据这种观点,拥有很多类就等于在数据中嵌入了很多知识。问题在于,胶合层中的“智能数据”却经常不代表任何程序处理的自然实体
――仅仅只是胶合物而已。(这种现象的一个确定标志就是抽象子类或混入类的不断扩散。)
OO抽象的另一个副作用就是程序往往丧失了优化的机会。
“如果你知道自己在做什么,三层就足够了;但如果你不知道自己在做什么,十七层也没用”。
OO在其取得成功的领域之所以能成功,主要原因之一可能是因为在这些领域里很难弄错类型的本体问题。例如:在GUI和图形系统中,类和可操作的可见对象之间有相当自然的映射关系。如果你发现增加的类和所显示的对象没有明显的对应关系,那么很容易就会注意到胶合层太厚了。
Unix风格程序设计所面临的主要挑战就是如何将分离法的优点(将问题从原始的场景中简化、归纳)同代码和设计的薄胶合、浅平透层次结构的优点相结合。
l 美在计算科学中的地位,要比在其它任何技术中的地位都重要,因为软件太复杂了。美是抵御复杂的终极武器。(Beauty is more important in computing than anywhere else in technology because software is so complicated.Beauty is the ultimate defense against complexity.—Machine Beauty: elegance and the Heart of Technology)
l
如果没有阴暗的角落和隐藏的深度,软件系统就是透明的。透明性是一种被动品质。如果实际上能预测到程序行为的全部或大部分情况,并能建立简单的心理模型,这个程序就是透明的,因为可以看透机器究竟在干什么。
如果软件系统所包含的功能是为了帮助人们对软件建立正确的“做什么、怎样
做”的心理模型而设计,这个软件系统就是可显的。因此,举例来说,对用户而言,良好的文档有助于提高可显性:对程序员而言,良好的变量和函数名有助于提高
可显性。可显性是一种主动品质。在软件中要达到这一点,仅仅做到不晦涩是不够的,还必须尽力做到有帮助。
透明性和可显性对用户和软件开发人员都很重要。但是重要性体现在不同的方面。用户喜欢UI中的这些特性,是因为这意味着学习曲线比较平缓。当人们说UI“直观”时,很大程度上是指UI的
透明性和可显性:剩下一部分,则来源于最小立异原则。软件开发者喜欢代码本身(用户不可见部分)的这些品质,因为他们经常需要对代码有很好的理解后才能进
行修改和调试。同时,如果程序的设计使内部数据流程非常容易理解,则这个程序更不可能因为设计者没有注意到的不良交互而崩溃,更可能优雅地向前发展(包括
适应新维护者接手的变化)。
优雅是力量与简洁的结合。优雅的代码事半功倍;优雅的代码不仅正确,而且
显然正确;优雅的代码不仅将算法传达给计算机,同时也把见解和信心传递给阅读代码的人。通过追求代码的优雅,我们能够编写更好的代码。学习编写透明的代码
是学习如何编写优雅代码的第一关,很难的一关――而关注代码的可显性则帮助我们学习如何编写透明的代码。优雅的代码既透明又可显。