找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4466

积分

0

好友

576

主题
发表于 昨天 09:34 | 查看: 15| 回复: 0

Unix 哲学(编程原则)并不是一种正规的设计方法,也不会从计算机科学的理论高度去推导出理论上完美的软件。它更注重实效,立足于丰富的开发经验。对于初学者而言,Unix 哲学就像一座深海灯塔,为提升方向提供指引。

1. Unix 哲学

Unix 哲学强调务实,源于长期积累的经验。它不会出现在正规方法学或标准文档中,更接近一种隐性的、半本能的认知。Unix 程序员在探索和开发过程中沉淀下来的经验,即便你不在 Unix 环境下工作,同样能从中受益。

核心主张可以概括为:

  1. 让每个程序专注做好一件事。遇到新任务时,优先考虑重新开始,而不是往旧程序里不断塞新功能,把逻辑搞得过于复杂。
  2. 假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序尚未出现。因此,输出中不要包含无关信息,避免干扰。
  3. 尽早把设计和编译出来的软件投入试用。面对拙劣的代码,不必犹豫,果断扔掉重写。
  4. 优先使用工具来减轻编程负担,而不是依赖低效的人工辅助。正所谓“工欲善其事,必先利其器”。

2. 编码原则

Unix 哲学中的许多内容,并非由那些先行者口述而成,而是通过他们的实践以及 Unix 系统本身的行为所体现的。整体来看,可以概括为以下原则:

  1. 模块原则:使用简洁的接口拼合简单的部件。
  2. 清晰原则:清晰胜于机巧。
  3. 组合原则:设计时考虑拼接组合。
  4. 分离原则:策略同机制分离,接口同引擎分离。
  5. 简洁原则:设计要简洁,复杂度能低则低。
  6. 吝啬原则:除非确无它法,不要编写庞大的程序。
  7. 透明性原则:设计要可见,以便审查和调试。
  8. 健壮原则:健壮源于透明与简洁。
  9. 表示原则:把知识叠入数据,以求逻辑质朴而健壮。
  10. 通俗原则:接口设计避免标新立异。
  11. 缄默原则:如果程序没什么好说的,就保持沉默。
  12. 补救原则:出现异常时,马上退出并给出足量错误信息。
  13. 经济原则:宁花机器一分,不花程序员一秒。
  14. 生成原则:避免手撕,尽量编写程序去生成程序。
  15. 优化原则:雕琢前先要有原型,跑之前先学会走。
  16. 多样原则:决不相信所谓“不二法门”的断言。
  17. 扩展原则:设计着眼未来,未来总比预想来得快。

如果你是初次接触这些理念,不妨花时间细细体会。谈软件工程的文章常常会推荐其中大部分原则,但多数系统缺乏恰当的工具和传统去落实它们。结果,不少程序员对蹩脚的工具、糟糕的设计、过度的劳作和臃肿的代码早已习以为常。

2.1 模块原则:使用简洁的接口拼合简单的部件

“计算机编程的本质就是控制复杂度”。排错往往占据开发时间的绝大部分,一个拿得出手的可用系统,与其说是出自才华横溢的设计,不如说更多是跌跌撞撞打磨出来的。

汇编语言、编译语言、流程图、过程化编程、结构化编程、面向对象以及各种软件开发方法论,不计其数的“解决之道”被推销者包装得神乎其神。可事实上,很多方法用处有限,原因恰恰在于它们“成功”地把程序复杂度推到了人脑几乎无法驾驭的地步。

要开发复杂软件而不至于失控,唯一可行的路径就是降低整体复杂度:用清晰的接口把若干简单的模块组合成一个复杂系统。这样一来,大多数问题只会局限在局部,也才有希望对局部进行改进而不牵动全局。

2.2 清晰原则:清晰胜于机巧

维护如此重要,成本又如此高昂。写程序时,要清楚自己不是写给执行代码的计算机看,而是写给“人”——将来阅读和维护源码的人,包括未来的自己。

在 Unix 传统里,这一建议并不仅仅指代码注释。良好的 Unix 实践同样奉行:在选择算法和实现方式时,应兼顾未来的可扩展性。为了一丁点性能提升,就大幅增加技术的复杂性和晦涩性,这笔买卖并不划算。复杂代码不仅容易滋生 bug,也会让日后的阅读与维护更加艰难。相反,优雅而清晰的代码不容易崩溃,也更容易让后来的修改者快速理解。这一点尤其重要,说不定若干年后回头改这些代码的人,恰恰就是你自己。

永远不要吃力地解读一段晦涩的代码三次。第一次也许侥幸成功,但如果发现必须重新解读一遍——因为隔得太久,细节已经想不起来了——那就应该立即补上注释,这样第三次就不会那么痛苦。

2.3 组合原则:设计时考虑拼接组合

如果程序彼此之间不能有效通信,软件就难免陷入复杂度的泥淖。

在输入输出方面,Unix 传统极力提倡使用简单、文本化、面向流、设备无关的格式。在经典 Unix 环境中,多数程序都尽可能采用简单过滤器的形式:将一个输入的文本流处理成简单的文本流输出。Unix 程序员偏爱这种做法,并非因为他们仇视图形用户界面,而是因为如果程序不采用简单的文本输入输出流,彼此之间就极难衔接。

在 Unix 中,文本流之于工具,就像面向对象环境中的消息之于对象。文本流接口的简洁性加强了工具的封装性。而许多精致的进程间通讯方法,比如远程过程调用,都存在让各程序过度耦合的倾向。

要让程序具备可组合性,就必须让程序彼此独立。位于文本流一端的程序,应该尽可能不去关心另一端是什么程序。如果把一端的程序替换成另一个截然不同的程序,另一端应该毫无察觉。GUI 可以是个好东西,但在动手做 GUI 之前,不妨想一想:能否把复杂的交互程序与干粗活的算法程序分离开,让每一部分独立成块,再用简单的命令流或应用协议把它们组合起来?

在构思精巧的数据传输格式之前,可以先考察一下是否能利用简单的文本数据格式。以一点点格式解析的代价,换取用通用工具构造或解读数据流的好处,通常是值得的。

当程序无法自然地使用序列化、协议形式的接口时,正确的 Unix 设计至少是把尽可能多的编程元素组织为一套定义良好的 API。这样至少可以通过链接调用应用程序,或根据不同任务需求粘合使用不同接口。

2.4 分离原则:策略同机制分离,接口同引擎分离

策略和机制的变化节奏并不相同,策略的变化远快于机制。把策略同机制揉成一团会带来两个负面后果:一是策略变得死板,难以适应用户需求的改变;二是任何策略变更都可能动摇底层机制。相反,把两者剥离,就有可能在探索新策略时不至于破坏机制。此外,也更容易为机制写出较好的测试。

实现剥离的一种常见方法是:将应用程序拆成可以协作的前端和后端进程,通过套接字上层的专用应用协议通信。前端实现策略,后端实现机制。相比单进程的整体实现,这种双端设计可以显著降低整体复杂度,减少 bug,从而降低软件的全生命周期成本。

2.5 简洁原则:设计要简洁,复杂度能低则低

来自多方面的压力经常让程序变得复杂,随之而来的是更高成本和更多 bug。其中一种压力来自技术上的虚荣心。程序员大多聪明,常常以玩转复杂概念和抽象能力为傲,这本身无可厚非。但正因如此,大家常常互相较劲,看谁能够鼓捣出最错综复杂的美妙事物。结果,设计能力大幅超过了实现和排错能力,最终产出代价高昂的废品。

“错综复杂的美妙事物”本身就有些自相矛盾。Unix 程序员真正比拼的,是谁能做到“简洁而漂亮”。这一点虽然隐含在众多规则之中,但仍值得公开强调。

至少在商业软件领域,过度的复杂性往往来自项目需求,而这些需求常常基于市场营销热点,而非顾客的真实需要和软件实际能提供的功能。许多优秀的设计被市场推销所要求的冗长“特性清单”扼杀,实际上这些特性几乎从未被使用过。随后,恶性循环开始:比别人花哨的方法就是让自己变得更花哨。很快,庞大臃肿成了行业标准,人人都在使用臃肿不堪、bug 频出的软件,连开发人员也不愿敝帚自珍。

避开这些陷阱的唯一方法,是鼓励另一种软件文化:以简洁为美。这是一种看重简单解决方案的工程传统,总是设法把系统拆解为若干可以协作的小部分,并本能地抵制用过多噱头粉饰程序的企图。

2.6 吝啬原则:除非确无它法,不要编写庞大的程序

“大”有两重含义:体积大、复杂程度高。程序一旦变大,维护起来就困难。人对耗费大量精力才完成的东西往往难以割舍,结果就是在庞大的程序里,把投资浪费在注定要失败或并非最优的方案上。避免不必要的代码和逻辑,保持代码精简。

2.7 透明性原则:设计要可见,以便审查和调试

调试通常会占用四分之三甚至更多的开发时间,所以一开始多花点功夫减少日后的调试成本,非常划算。一个有效的方法,就是在设计时充分考虑透明性和显见性。

软件系统的透明性,指的是一眼就能看出软件在做什么、怎样做。显见性则指程序带有监视和显示内部状态的功能,这样程序不仅运行良好,还能让人看出它是以何种方式运行的。

如果设计时充分考虑这些要求,项目全程都会受益。调试选项尽量不要等到事后才设置,而应在设计之初就纳入考虑。程序不但要能展示其正确性,还应该把原开发者的思维模型传递给后来者。

程序若要展示正确性,应当使用足够简单的输入输出格式,以便轻松检验有效输入与正确输出之间的关系。出于透明性和显见性的目的,还应该提倡接口简洁,方便其他程序——尤其是测试监视工具和调试脚本——对其进行操作。

2.8 健壮原则:健壮源于透明与简洁

软件的健壮性,指的是软件不仅在正常情况下运行良好,在超出设想的意外条件下也能维持运行。

大多数软件之所以磕碰不得、毛病不断,正是因为过于复杂,难以通盘考量。如果无法正确理解程序的逻辑,就无法确信其正确性,也就无法在出错时修复它。让程序健壮的方法,就是让内部逻辑更易于理解,这主要依赖两种手段:透明化和简洁化。

就健壮性而言,设计时还应考虑承受极端输入。在异常输入出现时,保证软件健壮的一个关键策略就是避免在代码里制造太多特例。bug 通常藏在处理特例的代码,以及不同特殊情况的交互逻辑中。

软件的透明性,就是一眼能看出它是怎么回事。如果“怎么回事”本身不算复杂,不需要绞尽脑汁就能推断出所有可能情况,那么这个程序就是简洁的。程序越简洁透明,也就越健壮。

模块化——代码简朴、接口简洁——正是组织程序以达到更简洁目标的有效方法。

2.9 表示原则:把知识叠入数据以求逻辑质朴而健壮

数据比编程逻辑更容易驾驭。在设计中,应该主动把代码的复杂度转移到数据中。

这种考量并非 Unix 的原创,但许多 Unix 代码都深受其影响。尤其是 C 语言对指针使用方式的控制,促进了在内核以上各编码层面动态修改引用结构。在结构中用非常简单的指针操作就能完成的任务,在其他语言里往往不得不借助更复杂的过程。

进行数据驱动编程时,需要把代码和代码所作用的数据结构划分清楚。这样,改变程序逻辑时,只需要编辑数据结构而不是代码。数据驱动编程有时会与面向对象混淆,后者是另一种以数据组织为中心的风格。它们至少有两个区别:第一,在数据驱动编程中,数据不仅是某个对象的状态,还定义了程序的控制流;第二,面向对象首先考虑封装,而数据驱动编程看重的是编写尽可能少的固定代码。

2.10 通俗原则:接口设计避免标新立异

这就是众所周知的“最少惊奇原则”。最易用的程序,是用户需要学习新东西最少的程序,也是最贴合用户既有认知的程序。因此,接口设计应该避免毫无来由的标新立异和自作聪明。

如果你编制一个计算器程序,+ 就应该永远表示加法。设计接口时,尽量参照用户最可能熟悉的同类功能接口和相似应用程序来建模。

始终关注目标受众:他们可能是最终用户,也可能是其他程序员,还可能是系统管理员。对不同人群而言,“最少惊奇”的含义有所不同。还要尊重传统惯例,这些惯例存在的一个重要理由就是:缓和学习曲线。

最小立异原则的另一面,是避免“表象相似而实际略有不同”。这种情况极为危险,因为表象相似往往会让人产生错误假定。所以,最好让不同事物有明显区别,而不要看起来几乎一模一样。

2.11 缄默原则:如果程序没什么好说的,就保持沉默

行为良好的程序应该默默工作,决不唠唠叨叨。沉默是金。这条原则起源于 Unix 诞生时还没有视频显示器,每一行多余输出都会严重消耗用户的宝贵时间。如今环境已大不相同,但一切从简的传统流传至今。

简洁是 Unix 程序的核心风格。一旦程序的输出成为另一个程序的输入,就很容易把需要的数据挑出来。从人的角度看,重要信息不应混杂在冗长的程序内部行为信息里。如果显示出来的信息都重要,那就无需费力寻找。设计良好的程序把用户注意力视为有限而宝贵的资源,只有在必要时才请求使用,避免不必要的信息打扰用户。

2.12 补救原则:出现异常时,马上退出并给出足量错误信息

软件在发生错误时,也应像正常操作时一样具备透明的逻辑。最理想的情况当然是软件能适应和应付非正常操作;而最糟糕的情况是:补救措施明明没有成功,却悄无声息地埋下崩溃隐患,直到很久以后才暴露出来。

因此,软件应尽可能从容地应对各种错误输入和自身运行错误。如果做不到这一点,就让程序以尽可能容易诊断错误的方式终止。

“宽容地收,谨慎地发。”即使输入数据不规范,设计良好的程序也应尽量领会其含义,尽量与其他程序协作;然后,要么响亮地失败,要么为工作链的下一环输出严谨干净的正确数据。

设计时要考虑宽容性,但不要用过分纵容的实现去补救标准的不足,否则一不留神就会让自己陷入困境。

2.13 经济原则:宁花机器一分,不花程序员一秒

在 Unix 早期的小型机时代,这一观点还相当激进。随着技术发展,开发公司和大多数用户都能获得廉价机器,这条准则的合理性也就不言而喻了。

在保证质量的前提下,尽量利用计算机资源完成任务,减轻程序员负担。另一个可以显著节约程序员时间的方法,是教会机器完成更多低层次的编程工作。

2.14 生成原则:避免手撕,尽量编写程序去生成程序

众所周知,人类很不擅长处理辛苦的细节工作。程序中的任何手工操作,都是滋生错误和延误的温床。由程序生成代码,几乎总是比手写代码更廉价,也更值得信赖。

对代码生成器来说,需要手写的重复而麻木的高级语言代码,和机器码一样可以批量生产。当代码生成器能够提升抽象度时——也就是生成器的说明性语句比生成码更简单时——使用它会非常划算,生成之后也无需再费力手工处理。Unix 中大量使用代码生成器,正是为了将易出错的细节工作自动化。

2.15 优化原则:雕琢前先要有原型,跑之前先学会走

原型设计最基本的理念是:“90% 的功能现在能实现,比 100% 的功能永远实现不了强。”做好原型设计,可以避免为蝇头小利投入过多时间。

“不应考虑蝇头小利的效率提升,过早优化是万恶之源。”不知道瓶颈所在就匆忙优化,可能是比乱加功能更损害设计的错误。从畸形的代码到杂乱无章的数据布局,牺牲透明性和简洁性而片面追求速度,会滋生无数 bug,耗费巨量人时。那点微小的性能好处,远不能抵消后续排错所付出的代价。

过早的局部优化实际上会妨碍全局优化,从而降低整体性能。整体设计中本可带来更大收益的修改,常常被过早的局部优化所干扰,最终产品既性能低劣,代码又过于复杂。

在 Unix 世界里,有一个非常明确的悠久传统:先制作原型,再精雕细琢。优化之前先确保能用;先能走,再学跑。另一种文化将这一点扩展为:先求运行,再求正确,最后求快。

这些说法本质上是一个意思:先做出一个未优化的、运行缓慢、耗内存但正确的实现,然后进行系统性地调整,寻找那些可以通过牺牲最小局部简洁性而获得较大性能提升的地方。

2.16 多样原则:决不相信所谓“不二法门”的断言

即使最出色的软件,也常常受限于设计者的想象力。没有人能聪明到把所有东西都最优化,也不可能预想到软件所有可能的用途。

在软件设计和实现方面,Unix 传统有一点很好:从不相信任何所谓的“不二法门”。Unix 奉行广泛采用多种语言、开放的可扩展系统和用户定制机制,吸收并借鉴各种优秀设计思想,不断完善自己的设计方法和风格。

2.17 扩展原则:设计着眼未来,未来总比预想快

要为数据格式和代码预留扩展空间。否则,常常会被原先的不明智选择捆住手脚,因为既想改变它们,又必须维持对旧版本的兼容。

设计协议或文件格式时,应使其具备充分的自描述性以便扩展。要么包含版本号,要么采用独立、自描述的语句,按照可以随时插入新内容、替换旧内容而不会破坏格式读取代码的方式来组织格式。Unix 经验表明:稍微增加一点让数据具备自描述性的开销,就能在无需破坏整体的前提下进行扩展,小的付出可能换来成百上千倍的回报。

设计代码时,要有良好的组织结构,让后来的开发者增加新功能时无需拆毁或重建整个架构。这个原则并不是说随意添加根本用不上的功能,而是建议在编写代码时考虑未来需要,使以后增加功能更加容易。程序接合部要灵活,可以在代码中加入“如果扩展……需要……”的注释,为之后使用和维护代码的人行个方便。也许将来维护代码的就是自己,设计着眼于未来,节省的可能正是自己的精力。

3. 应用 Unix 哲学

这些富有哲理的原则并不是模糊笼统的泛泛之谈。在 Unix 世界中,它们都直接来自实践,并形成了具体的规定。

运用 Unix 哲学,就应该不断追求卓越。软件设计是一门技艺,值得投入智慧、创造力和激情。否则,就很难超越那些简单、老套的设计与实现:在该思考的时候急急忙忙去编程,在该无情删繁就简的时候反而把问题复杂化,最后又反过来抱怨代码怎么那么臃肿、难以调试。

要良好地运用 Unix 哲学,永远不要蛮干;要多用巧劲,把力气省下来留到需要的时候再用,好钢用在刀刃上。善用工具,尽可能将一切自动化。

4. 态度

软件设计和实现是一门充满快乐的艺术,也是一种高水平的游戏。为什么要从事软件设计,而不是其他行业呢?也许现在只是为了赚钱或打发时间,也许曾经也相信软件设计可以改变世界,值得为之付出激情。

无论动机如何,这些来自 Unix 的编程原则都值得反复咀嚼。它们不是教条,而是经验的结晶。把这些看似朴素的原则真正落到日常编码中,代码质量和设计能力自然会稳步提升。





上一篇:嵌入式代码保养:Astyle 格式化、cppCheck 静态检查与 Git 版本控制
下一篇:Recuris提出Agent记忆递归演化范式,3B到Claude Opus 5全面暴涨
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-3 20:06 , Processed in 1.027916 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表