Unix 哲学不算是一种正规设计方法,它并不打算从计算机科学的理论高度来产生理论上完美的软件。那些毫无动力、松松垮垮而且薪水微薄的程序员们,能在短短期限内,如神灵附体般开发出稳定而新颖的软件——这只不过是经理人永远的梦呓罢了。
Unix 哲学注重实效,立足于丰富的经验,并不会在正规方法学和标准中找到它,它更接近于隐性的半本能的知识。Unix程序员在探索开发的过程中积累的经验,哪怕不是Unix的程序员也能够从这些经验汇总获益。
(1) 让每个程序就做好一件事。如果有新任务,就重新开始,不要往原程序中加入新功能而搞得复杂。
(2) 假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序还是未知的,输出中不要有无关的信息干扰。
(3) 尽可能早地将设计和编译的软件投入试用,对拙劣的代码别犹豫,扔掉重写。
(4) 优先使用工具而不是拙劣的帮助来减轻编程任务的负担,工欲善其事,必先利其器。
Unix 哲学中的内容不是这些先哲们口头表述出来的,而是由他们所作的一切和Unix 本身所作出的榜样体现出来的。从整体上来说,可以概括为以下几点:
如果刚开始接触,这些原则值得好好体味一番。谈软件工程的文章常常会推荐大部分的这些原则,但是大多数其它系统缺乏恰当的工具和传统将这些准则付诸实践,所以,多数的程序员还不能自始至终地贯彻这些原则。蹩脚的工具、糟糕的设计、过度的劳作和臃肿的代码对他们已经是家常便饭了。
“计算机编程的本质就是控制复杂度” 。排错占用了大部分的开发时间,一个拿得出手的可用系统,与其说是出自才华横溢的设计成果,不如说是跌跌撞撞的结果。
汇编语言、编译语言、流程图、过程化编程、结构化编程、面向对象、以及软件开发的方法论,不计其数的解决之道被抛售者吹得神乎其神。实际上这些用处不大,原因恰恰在于它们“成功”地将程序的复杂度提升到了人脑几乎不能处理的地步。
要编制复杂软件而又不至于一败涂地的唯一方法就是降低其整体复杂度,用清晰的接口把若干简单的模块组合成一个复杂软件。如此一来,多数问题只会局限于某个局部,那样才有希望对局部进行改进而不至牵动全身。
维护如此重要、而成本如此高昂。在写程序时,要想到不是写给执行代码的计算机看,而是给人,将来阅读维护源码的人,包括自己。
在 Unix 传统中,这个建议不仅意味着代码注释。良好的 Unix 实践同样信奉在选择算法和实现时,应该考虑到将来的可扩展性。为了取得程序一丁点的性能提升,就大幅增加技术的复杂性和晦涩性,这个买卖做不得,这不仅仅是因为复杂的代码容易滋生bug,也因为它会使日后的阅读和维护工作更加艰难。相反,优雅而清晰的代码不仅不容易崩溃,而且更易于让后来的修改者立刻理解。这点非常重要,尤其是,说不定若干年后回过头来修改这些代码的人可能恰恰就是自己。
永远不要去吃力地解读一段晦涩的代码三次。第一次也许侥幸成功,但如果发现必须重新解读一遍——离第一次太久了,具体细节无从回想,那就该注释代码了,这样第三次就相对不会那么痛苦了。
如果程序彼此之间不能有效通信,那么软件就难免会陷入复杂度的泥淖。
在输入输出方面, Unix 传统极力提倡采用简单、文本化、面向流、设备无关的格式。 在经典的 Unix 下,多数程序都尽可能采用简单过滤器的形式,即将一个输入的文本流处理为一个简单的文本流输出。抛开世俗眼光,Unix程序员偏爱这种做法并不是因为他们仇视图形用户界面,而是因为如果程序不采用简单的文本输入输出流,它们就极难衔接。
Unix 中文本流之于工具,就如同在面向对象环境中的消息之于对象。文本流界面的简洁性加强了工具的封装性。而许多精致的进程间通讯方法,比如远程过程调用,都存在牵扯过多各程序间内部状态的倾向。
要想让程序具有组合性,就要使程序彼此独立。在文本流这一端的程序应该尽可能不要考虑文本流另一端的程序。将一端的程序替换为另一个截然不同的程序,而完全不惊扰另一端应该很容易做到。
GUI 可以是个好东西,在做 GUI前,应该想想可不可以把复杂的交互程序跟干粗活的算法程序分离开,每个部分单独成为一块,然后用一个简单的命令流或者是应用协议将其组合在一起。
在构思精巧的数据传输格式前,有必要实地考察一下,是否能利用简单的文本数据格式;以一点点格式解析的代价,换得可以使用通用工具来构造或解读数据流的好处是值得的。
当程序无法自然地使用序列化、协议形式的接口时,正确的 Unix 设计至少是,把尽可能多的编程元素组织为一套定义良好的 API 。这样至少可以通过链接调用应用程序,或根据不同任务的需求粘合使用不同的接口。
策略和机制是按照不同的时间尺度变化的,策略的变化要远远快于机制。把策略同机制揉成一团有两个负面影响: 一来会使策略变得死板,难以适应用户需求的改变,二来也意味着任何策略的改变都极有可能动摇机制。相反,将两者剥离,就有可能在探索新策略的时候不足以打破机制。另外,也更容易为机制写出较好的测试。
实现剥离的一个方法,将应用程序分成可以协作的前端和后端进程,通过套接字上层的专用应用协议进行通讯。前端实现策略,后端实现机制。比起仅用单个进程的整体实现方式来说,这种双端设计方式大大降低了整体复杂度 ,bug 有望减少,从而降低程序的寿命周期成本。
来自多方面的压力常常会让程序变得复杂(由此代价更高, bug 更多),其中 一种压力就是来自技术上的虚荣心理。 程序员们都很聪明,常常以能玩转复杂东西和耍弄抽象概念的能力为傲,这一点也无可厚非。但正因如此常常会与同行们比试,看看 谁能够鼓捣出最错综复杂的美妙事物,他们的设计能力大大超出他们的实现和排错能力,结果便是代价高昂的废品。
”错综复杂的美妙事物”听来自相矛盾。Unix 程序员相互比的是谁能做到 “简洁而漂亮”,这一点虽然只是隐含在这些规则之中,但还是很值得公开提出来强调一下。
至少在商业软件领域里,过度的复杂性往往来自于项目的要求,而这些要求常常基于推销热点,而不是基于顾客的需求和软件实际能够提供的功能。许多优秀的设计被市场推销所需要的大堆“特性清单”扼杀,实际上这些特性功能几乎从未用过。然后,恶性循环开始了,比别人花哨的方法就是把自己变得更花哨。很快,庞大臃肿变成了业界标准,每个人都在使用臃肿不堪、bug 极多的软件,连软件开发人员也不敢敝帚自珍。
避免这些陷阱,唯一的方法就是鼓励另一种软件文化,以简洁为美。这是一个非常看重简单解决方案的工程传统,总是设法将程序系统分解为几个能够协作的小部分,并本能地抵制任何用过多噱头来粉饰程序的企图。
“大”有两重含义:体积大,复杂程度高。程序大了,维护起来就困难。由于对花费了大量精力才做出来的东西难以割舍,结果导致在庞大的程序中,把投资浪费在注定要失败,或并非最佳的方案上。避免不必要的代码和逻辑,保持代码精简。
因为调试通常会占用四分之三甚至更多的开发时间,所以一开始就多做点工作以减少日后调试的工作量会很划算。 一个有效的减少调试工作量的方法,就是设计时充分考虑透明性和显见性。
软件系统的透明性是指一眼就能看出软件在做什么以及怎样做的。显见性指程序带有监视和显示内部状态的功能,这样程序不仅能够运行良好,而且还可以看出它以何种方式运行。
设计时如果充分考虑到这些要求会给整个项目全过程带来好处。调试选项的设置尽量不要在事后,而应该在设计之初便考虑进去,程序不但应该能展示其正确性,也应该能把原开发者解决问题的思维模型告诉后来者。
程序如果要展示其正确性,应该使用足够简单的输入输出格式,这样才能保证很容易地检验有效输入和正确输出之间的关系是否正确。出于充分考虑透明性和显见性的目的,还应该提倡接口简洁,以方便其它程序对其进行操作,尤其是测试监视工具和调试脚本。
软件的健壮性指软件不仅能在正常情况下运行良好,而且在超出设想的意外条件下也能够运行良好。
大多数软件禁不起磕碰,毛病多,就是因为过于复杂,很难通盘考虑。如果不能正确理解一个程序的逻辑,就不能确信其是否正确,也就不能在出错的时候修复它。让程序健壮的方法,就是让程序的内部逻辑更易于理解,要做到这一点主要有两种方法:透明化和简洁化。
就健壮性而言,设计时要考虑到能承受极端输入,这一点也很重要。在有异常输入的情况下,保证软件健壮性的一个相当重要的策略就是避免在代码中出现特例,bug 通常隐藏在处理特例的代码以及处理不同特殊情况的交互操作部分的代码中。
软件的透明性就是指一眼就能够看出来是怎么回事。 如果“怎么回事”不算复杂,即不需要绞尽脑汁就能够推断出所有可能的情况,那么这个程序就是简洁的。程序越简洁,越透明,也就越健壮。
模块化(代码简朴,接口简洁)是组织程序以达到更简洁目的的一个方法。
数据要比编程逻辑更容易驾驭,在设计中,应该主动将代码的复杂度转移到数据之中去。
此种考量并非 Unix 的原创,但是许多 Unix 代码都显示受其影响。特别是C 语言对指针使用控制的功能,促进了在内核以上各个编码层面上对动态修改引用结构。在结构中用非常简单的指针操作就能够完成的任务,在其它语言中,往往不得不用更复杂的过程才能完成。
进行数据驱动编程时,需要把代码和代码作用的数据结构划分清楚,这样,在改变程序的逻辑时,只要编辑数据结构而不是代码。数据驱动编程有时会跟面向对象混淆起来,后者是另一种以数据组织为中心的风格。它们之间至少有两点不同。第一,在数据驱动编程中,数据不仅仅是某个对象的状态,实际上还定义了程序的控制流;第二,面向对象首先考虑的是封装,而数据驱动编程看重的是编写尽可能少的固定代码。
也就是众所周知的“最少惊奇原则”。最易用的程序就是用户需要学习新东西最少的程序,就是最切合用户已有知识的程序。因此,接口设计应该避免毫无来由的标新立异和自作聪明。
如果你编制一个计算器 程序, ‘+’应该永远表示加法。设计接口的时候,尽量按照用户最可能熟悉的同样功能接口和相似应用程序来进行建模。
关注目标受众,他们也许是最终用户,也许是其他程序员,也许是系统管理员。对于这些不同的人群,最少惊奇的意义也不同。关注传统惯例,这些惯例的存在有个极好的理由:缓和学习曲线。
最小立异原则的另一面是避免表象相似而实际却略有不同。这会极端危险, 因为表象相似往往导致人们产生错误的假定。所以最好让不同事物有明显区别,而不要看起来几乎一模一样。
行为良好的程序应该默默工作,决不唠唠叨叨。沉默是金,这个原则的起始是源于Unix 诞生时还没有视频显示器,每一行多余的输出都会严重消耗用户的宝贵时间。现在这种情况已不复存在, 但一切从简的这个优良传统流传至今。
简洁是 Unix 程序的核心风格。 一旦程序的输出成为另一个程序的输 入,就很容易把需要的数据挑出来。站在人的角度上来说,重要信息不应该混杂在冗长的程序内部行为信息中。如果显示的信息都是重要的,那就不用找了。设计良好的程序将用户的注意力视为有限的宝贵资源,只有在必要时才要求使用,避免不必要的信息对用户的打扰。
软件在发生错误的时候也应该与在正常操作的情况下一样,有透明的逻辑。最理想的情况当然是软件能够适应和应付非正常操作;而如果补救措施明明没有成功,却悄无声息地埋下崩溃的隐患,直到很久以后才显现出来,这就是最坏的一种情况。
因此,软件要尽可能从容地应付各种错误输入和自身的运行错误,如果做不到这一点,就让程序尽可能以一种容易诊断错误的方式终止。
“宽容地收,谨慎地发”。就算输入的数据不规范,设计良好的程序也会尽量领会其中的意义,尽量与别的程序协作;然后,要么响亮地倒塌,要么为工作链下一环的程序输出一个严谨干净正确的数据。
设计时要考虑宽容性,不是用过分纵容的实现来补救标准的不足,否则一不留神你会死得很难看。
在 Unix.早期的小型机时代,这一条观点还是相当激进的;随着技术的发展,开发公司和大多数用户都能够得到廉价的机器,所以这一准则的合理性就不用多说。
在保证质量的前提下,尽量使用计算机资源完成任务,减轻程序员负担,另一个可以显著节约程序员时间的方法是,教会机器如何做更多低层次的编程工作。
众所周知,人类很不善于干辛苦的细节工作。程序中的任何手工操作都是滋生错误和延误的温床,由程序生成代码几乎总是比手写代码廉价并且更值得信赖。
对于代码生成器来说,需要手写的重复而麻木的高级语言代码,与机器码一样是可以批量生产的。当代码生成器能够提升抽象度时,即当生成器的说明性语句要比生成码简单时,使用代码生成器会很合算,而生成代码后就根本无需再费力地去手工处理。在 Unix 中大量使用代码生成器使易于出错的细节工作自动化。
原型设计最基本的原则,“90%的功能现在能实现,比100%的功能永远实现不了强”。做好原型设计可以避免为蝇头小利而投入过多的时间。
“ 不应考虑蝇头小利的效率提升,过早优化是万恶之源”,不知道瓶颈所在就匆忙进行优化,这可能是唯一一个比乱加功能更损害设计的错误。从畸形的代码到杂乱无章的数据布局,牺牲透明性和简洁性而片面追求速度,滋生无数 bug, 耗费以百万计的人时,这点芝麻大的好处,远不能抵消后续排错所付出的代价。
过早的局部优化实际上会妨碍全局优化,从而降低整体性能。在整体设计中可以带来更多效益的修改常常会受到一个过早局部优化的干扰,导致出来的产品既性能低劣又代码过于复杂。
在 Unix 世界里,有一个非常明确的悠久传统:先制作原型,再精雕细琢。优化之前先确保能用,先能走,再学跑。从另一种不同的文化将这一点有效地扩展为:先求运行,再求正确,最后求快。
所有这些话的实质其实是一个意思:先设计做个未优化的、运行缓慢、很耗内存但是正确的实现,然后进行系统地调整,寻找那些可以通过牺牲最小的局部简洁性而获得较大性能提升的地方。
即使最出色的软件也常常会受限于设计者的想象力。没有人能聪明到把所有东西都最优化,也不可能预想到软件所有可能的用途。
对于软件设计和实现来说,Unix 传统有一点很好,即从不相信任何所谓的“不二法门”。Unix 奉行的是广泛采用多种语言、开放的可扩展系统和用户定制机制;吸收并借鉴各种优秀的设计思想,不断完善自己的设计方法和风格。
为数据格式和代码留下扩展的空间,否则,就会发现常常被原先的不明智选择捆住了手脚,因为无法既要改变它们又要维持对原来的兼容性。
设计协议或文件格式时,应使其具有充分的自描述性以便可扩展。 要么包含版本号,要么采用独立、自描述的语句,按照可以随时插入新的、换掉旧的,而不会破坏格式读取代码的方法组织格式。Unix 经验表示:稍微增加一点让数据部署具有自描述性的开销,就可以在无需破坏整体的情况下进行扩展,小的付出也可得到成千倍的回报。
设计代码时,要有很好的组织,让将来的开发者增加新功能时无需拆毁或重建整个架构。这个原则并不是说随意增加根本用不上的功能,而是建议在编写代码时要考虑到将来的需要,使以后增加功能比较容易。程序接合部要灵活,在代码中加入“如果扩展…需要…”的注释,有义务给之后使用和维护自己编写的代码的人做点好事,也许将来就是自己来维护代码,设计为将来着眼,节省的有可能就是自己的精力。
嵌入式软件开发,原厂掌握着底层,寄存器细节已经被封装,大部分程序员都是上层开发,上层应用更多的考虑设计模式和开发原则,也即本文所述类似。关于这类文章可以关注微信公众号【嵌入式系统】,提高嵌入式软件开发的思路和境界。
这些富有哲理的原则决不是模糊笼统的泛泛之谈。在Unix 世界中,这些原则都直接来自于实践,并形成了具体的规定。
运用Unix 哲学,就应该不断追求卓越。软件设计是一门技艺,值得付出智慧、创造力和激情。否则就不会超越那些简单、老套的设计和实现;就会在应该思考的时候急急忙忙去编程,就会在该无情删繁就简的时候反而把问题复杂化,就会反过来抱怨代码怎么那么臃肿、难以调试。
要良好地运用 Unix 哲学,永远不要蛮干;要多用巧劲,省下力气到需要的时候再用,好钢用在刀刃上。善用工具,尽可能将一切都自动化。
软件设计和实现是一门充满快乐的艺术, 一种高水平的游戏。为什么要搞软件设计而不是别的什么呢?可能现在只是为了赚钱或是打发时间,也可能曾经也认为软件设计值得付出激情,改变世界。
更多信息请关注微信公众号【嵌入式系统】