Unix哲学之体会

发布日期:2023-03-13

很多年以前买过一本非常经典的著作《Unix编程艺术》,主要介绍了Unix系统领域中的设计和开发哲学、思想文化体系、原则与经验,由公认的Unix编程大师、开源运动领袖人物之一Eric S.Raymond倾力多年写作而成。时过境迁,软件开发技术和过程都已发生了很大的改变,但该书中提出的组成Unix哲学基础的17项原则从未被颠覆或被怀疑过。
坦白说,当初这本书我也就是囫囵吞枣地都读了一遍,没有领会其精髓,也没有得到太多的启发。可能是过于期待MDA(Model Driven Architecture)给软件设计和编码方式方法带来革命性的变更,或者过多依赖CMMI和Scum将软件开发过程引入到预先设定的轨道,对现状充满了失望。
最近,我又想起了《Unix编程艺术》这本书,重温了她的17项原则。大家都知道,Unix在操作系统以及工具软件范围内,绝对称得上是占尽了风头的“常青树”,归根结底都归功于Unix哲学。所以作为这个哲学的17项原则,同样适用于任何软件开发过程。大道至简,这是我本次的最大体会!以下将这17项原则的一点体会与大家分享,希望能起到抛砖引玉的作用。

1. 模块原则:使用简洁的接口拼合简单的部件。
对于一个复杂的软件系统,如果希望按期交付,不至于一败涂地的唯一方法就是降低其整体复杂度。
首先,对软件进行层次划分。对于较复杂的层次,再将该层次分割成多个区域。
其次,对软件层级逐一进行模块划分。模块一定要简单,即单一功能和少量代码行。还要考虑完成业务功能和软件技术的代码不应出现在同一模块中。
最后,对软件模块进行接口设计。接口定义要简单,即参数数量要少,参数类型尽量使用最简单的数据类型;接口使用要简单,接口调用方式一定要符合调用方的使用习惯。

2. 清晰原则:清晰胜于机巧。
软件维护成本往往高于编写时的成本。你的源代码不是给执行代码的计算机看的,而是给阅读和维护源代码的人(包括你自己)看的。
首先,一定要遵循公司的编码规范。
其次,为每个源文件、接口函数、数据定义填写注释,对完成关键业务逻辑的代码段添加代码注释。
最后,插入适当的换行、空格、制表符和空行可以使你的源代码更清晰、更优雅。

3. 组合原则:设计时考虑拼接组合。
要想程序具有可组合性,就要使程序彼此独立,相关程序之间要能够进行有效的通信。
首先,对于经常使用的工具软件,为其添加基于文本格式的输入和输出(或者文本数据文件)是个好主意,这样可以方便地将这些工具衔接起来。例如,Unix可以通过管道将一些命令连接起来,如:ls -l | more、stat -ano | grep 8080等。
其次,软件服务之间的消息格式,尽量采用可读性和可通用性强的文本格式,如:Json、XML、XAML等。如果待开发软件需要遵循相关行业或国家标准,消息格式应采用标准中定义的格式,如:B2MML、BatchML、SML等。
最后,当程序无法使用序列化、协议形式的接口时,对相关软件进行组织和封装,并定义一套良好的API接口。

4. 分离原则:策略同机制分离,接口同引擎分离。
策略和接口来源于用户需求,机制和引擎是由开发人员设计实现的。分离原则,可以最大程度地保证新的策略产生时不足以打破旧的机制,接口发生变更时原有的引擎不会受到严重的影响。
首先,在GUI设计时,采用MVC、MVVM等模式将界面视图、界面控制和业务实体分离开来。对于大中型软件,通常将应用程序分成可以协作的前端和后端进程。
其次,对业务流程(或状态机)通过参数设置、脚本编码、内嵌代码等方式进行设计和编码,适当时建议采用特定DSL的软件架构。
最后,在使用遗留和第三方软件时,一定要对这些软件进行封装,并定义其接口。

5. 简洁原则:设计要简洁,复杂度能低则低。
避免过度设计,杜绝软件设计人员鼓捣出“错综复杂的美妙事务”。倡导一种软件文化,程序员相互比的是谁能够做到“简洁而漂亮”,并以此为荣。
首先,根据软件产品的规模、难度和复杂度,综合评估可利用的资源,尽可量选用简单和熟悉的软件架构。
其次,不要生搬硬套设计模式,应将其作为一种设计灵感。更不要为了解决一个问题,而动用多个设计模式,并建立一堆复杂的关联关系。
最后,尽量使用图表展示设计思路或方案,“字不如表,表不如图”,设计模型和设计说明书要让他人很容易看懂。

6. 吝啬原则:除非确无它法,不要编写庞大的程序。
“大”有两重含义:体积大,复杂程度高。程序大了,维护起来就困难。
首先,要限制手动编写的一个源代码文件、一个函数执行体的代码行数。
其次,要限制构建出的一个执行文件、一个动态库或静态库的字节数。
最后,要限制提交到配置库中一个软件包的规模,如果超过限制尺寸,可将其拆分成多个。

7. 透明性原则:设计要可见,以便审查和调试。
通常软件调试会占用四分之三甚至更多的开发时间,为了有效的减少调试工作量,在设计时一定要充分考虑透明性和显见性。
首先,日志是解决此类问题最简单、直接和有效的方式。
其次,选用“适用”的软件运行调试环境,并事先设定好调试选项。
最后,应该使用足够简单的输入输出格式,要方便调试人员阅读。

8. 健壮原则:健壮源于透明与简洁。
软件的健壮性指软件不仅能在正常情况下运行良好,而且在超出设计者设想的意外条件下也能够运行良好。
首先,健全异常处理机制,预先识别出所有可能发生的异常,并形成书面的异常清单或异常树。
其次,避免在代码中出现对异常输入进行“特殊”处理的程序,对接口函数中输入参数的检查及处理一定要严格遵守接口需求,不要“自以为是”地擅自处理。
最后,一定要坚信:程序越简洁,越透明,也就越健壮。

9. 表示原则:把知识叠入数据以求逻辑质朴而健壮。
数据要比编程逻辑更容易驾驭,例如:将状态迁移表定义为结构化的数据,由此实现的状态机肯定比使用switch语句编写的代码更易于阅读和理解。在设计中,应该主动将代码的复杂度转移到数据之中去。
首先,在顶层设计中,除建立对象模型之外,还要设法对业务活动进行建模。
其次,对活动模型的元数据进行持久化,并根据该元数据模型存储实例化的活动信息。很多行业或国标都已制订了相关的活动模型,如:ISA95、ISA88、SEMI等。如有可能,就按标准来做。
最后,如果遇到业务逻辑错综复杂,代码执行路径交叉过多、迭代过深,你就应该考虑使用数据来解决控制流程的问题了。

10. 通俗原则:接口设计避免标新立异。
也就是“最少惊奇原则”,最易用的程序就是用户需要学习新东西最少的程序,换句话说, 最易用的程序就是最切合用户已有知识的程序。
首先,接口设计尽量按照用户最可能熟悉的形式进行定义,千万不能误导用户。
其次,设计上尽可量关注传统惯例,使用这些惯例能够缓和学习曲线。
最后,如果有充分的理由使用新技术,必须经过项目团队的技术评审。

11. 缄默原则:如果一个程序没什么好说的,就沉默。
即“沉默是金”原则,就是说:若程序没有什么特别之处可讲,就保持沉默。设计良好的程序将用户的注意力视为有限的宝贵资源,只有在必要时才要求使用。
首先,对正式发布的软件输出信息进行仔细的核对,删除用户不关心的输出数据,并屏蔽软件调试时使用的输出信息。
其次,软件发布时不要忘记设定合适的日志级别,避免过多的日志信息。
最后,不要向用户主动推送无关紧要的通知,对必要的通知信息进行最小化,并对推送频率进行严格的性能测试。

12. 补救原则:出现异常时,马上退出并给出足够错误信息。
软件在发生错误的时候也应该与在正常操作的情况下一样,具有透明的逻辑。软件要尽可能从容地应付各种错误输入和自身的运行错误,但如果做不到这一点,就让程序尽可能以一种容易诊断错误的方式终止。
首先,不要怀疑负责异常处理的代码比处理正常业务逻辑的代码量多了好多,对所有异常分支同样都要进行严格的设计。
其次,对于业务处理过程中所发生异常,异常信息中应该包含所处理业务的相关信息,并且提供相应的恢复策略。
最后,如果异常致使应用程序中断,并造成系统存储数据(如:数据库、数据文件等)的不一致,那么一定要提供数据恢复的具体操作方法,一般需要提供包含数据维护内容的书面文档。

13. 经济原则:宁花机器一分,不花程序员一秒。
随着技术的发展,开发公司和大多数用户都能够得到廉价的机器,所以这一准则的合理性就显然不用多说啦!
首先,在能够满足业务正常处理性能的情况下,可以考虑使用脚本语言编写部分“粘合”软件构件的代码。
其次,在代码检查和测试中,尽可量选用自动化的代码静态和动态检查工具。
最后,建立公司级的CI/CD开发环境和软件测试平台,使持续集成、持续部署和持续交付自动化,建立每日构建和回归测试机制。

14. 生成原则:避免手工hack,尽量编写程序去生成程序。
程序中的任何手工hacking都是滋生错误和延误的温床。由程序生成代码几乎(在各个层次)总是比手写代码廉价并且更值得信赖。
首先,在基于组件开发中,接口和实现之间的代码是最可能使用代码生成器的地方。
其次,如果具有相同或类似控制结构的代码被重复编写过三次,可能就要考虑引用第三方或自己编写代码生成器了。
最后,对一些需要大量客制化的业务系统,可以搭建基于DSL的低代码平台。通过解析DSL来产生宿主语言的源代码,这样可以大规模的缩短系统实施过程中针对客户特定需求的开发周期。

15. 优化原则:雕琢前先要有原型,跑之前先学会走。
该原则的实质含义是:先给你的设计做个未优化的、运行缓慢、很耗内存但是正确的实现,然后进行系统地调整,寻找那些可以通过牺牲最小的局部简洁性而获得较大性能提升的地方。制作原型对于系统设计和优化同样重要,比起阅读一个冗长的规格说明,判断一个原型究竟是不是符合设想要容易得多。
首先,图形界面和应用服务的原型要分别规划和制作。图形界面的原型采用增量式,界面原型的开发工具与实际的相同;服务器端的程序(或者业务实现代码部分)原型一般采用替换式,使用脚本或更高级别的语言进行编写。
其次,慎重对待系统优化,按照“先全局后局部”和“先对后快”的原则进行规划和实施。
最后,要相信需求文档是绝对必要的。虽然敏捷开发的倡导者们极力鼓吹“去文档化需求”,但实践证明,在具有原型支撑的适当篇幅(不是冗长的)的需求文档,对软件项目的成功交付增加了更大的可能性。

16. 多样原则:决不相信所谓“不二法门”的断言。
即使最出色的软件也常常会受限于设计者的想象力。没有人能聪明到把所有东西都最优化,也不可能预想到软件所有可能的用途。对于软件设计和实现来说,Unix从不相信任何所谓的“不二法门”,而奉行的是广泛采用多种语言、开放的可扩展系统和用户定制机制。
首先,在软件架构设计时,不要过早限定开发语言和依赖库,更要关注系统的扩展和定制机制。
其次,对不太确定的核心的技术解决方案或算法,一定要预先准备好备选方案。
最后,除非存在开发周期过短、可利用资源短缺和现有开发人员技术能力不足等因素的限制,绝对不要只限定某一种具体编程语言,更谨慎采用转换编程语言的代码重构。

17. 扩展原则:设计着眼未来,未来总比预想来得快。
软件设计时,要有很好的组织,让将来的开发者增加新功能时无需拆毁或重建整个架构。 当然这个原则并不是说你能随意增加根本用不上的功能,而是建议在编写代码时要考虑到将来的需要。
首先,在通信消息、数据文件的设计中,要充分考虑实体的自描述性。这样可以更好的响应接口功能和数据结构等方面的变化。
其次,在设计和编码过程中,识别可增加功能的扩展点,并进行记录(在设计文档或设计模型中)和注释(在源代码中)。
最后,制定便于进行产品扩展的代码配置管理机制,规范代码版本和分支管理规范。

总结为一条铁律:KISS

上海市浦东新区郭守敬路498号(浦东软件园一期)15号楼505室