笔记 - Unix编程艺术
笃信模块化,注重正交性和紧凑性
- 紧凑型:从api角度来讲,让有经验的用户在不需要额外说明的情况下,可以比较顺利地学习和使用;设计和使用的思路易于理解
- 正交性:单一动作只改变直接相关的一件事,尽量不影响其他模块的动作
用API作为串接的线索,来组织模块
软件开发的良好习惯:先定义接口,编写简要注释,对其进行描述,最后再编写代码
模块粒度过小或过大都会导致缺陷密度上升
SPOT原则
- 不要重复自身(don’t repeat yourself)
- 任何一个知识点在系统内部应该有一个唯一、明确、权威的表述
- “真理的单点性”(single point of truth)——SPOT原则
提高紧凑型的一种方法 围绕“解决一个定义明确的问题”的强核心算法组织设计
分离的价值:崇尚简洁 尽量思考最少能做的事情,不要带着假想行动,而是从零开始
软件是多层的,设计思路: 自顶向下(从抽象到具体)和自底向上(从具体到抽象) 自顶向下适用于以下三个条件都成立:
- 能够精确预知程序的任务
- 实现过程中,规格不会发生重大变化
- 在底层,有充分自由来选择程序完成任务的方式
一般多见于应用软件编程 自底向上的设计更灵活,在高层设计变化后,仍然可以重用
胶合层
- 调和自顶向下和自底向上的矛盾
- 应该尽可能薄
- 虽然起着调和矛盾的作用,但不应该隐藏各层的缺陷
Unix编程风格——强调模块化和定义良好的API——其影响之一: 倾向于把程序分解为由胶合层连接的库的集合,特别是共享库
程序分层 GUI/高层逻辑;库程序;胶合层程序。下图以GIMP为例
OO有其局限性,设计理念价值在图形系统,UI和某些仿真程序之外的领域内认可程度待考
- OO推崇厚胶合层,容易出现用复杂逻辑去实现简单目的的情况
- 容易陷入过度分层的陷阱
模块式编码
- 避免全局变量
- 单个函数的体积:如果不能用一句话来简单描述函数与其调用程序之间的约定,那么这个函数8成偏大
- 内部API的合理设计。逐层次地封装实现细节
- API入口点不宜超过7个
- 每个模块入口点的数量是否均匀,可能也能成为API设计的衡量标准
管道通信文本化
数据文件的元格式
- DSV风格(Delimiter-separated values)分隔符分隔值
- 使用分隔符分隔字段
- 适用于列表、名称(首字段)为关键字,记录较短的文件
- RFC 822 - 适用于具有不同字段集合,而且字段中数据层次扁平的记录
- Cookie-jar - 适用于非结构化文本 - 没有自然顺序
- Record-Jar - DSV配合Cookie-jar的形式
- XML - 适合复杂的数据格式 - 容易检查数据的损坏或错误 - 注意不要滥用,尽量简单处理
- Windows INI - 可读性高 - 适合扁平数据,不适合存在递归或树形结构的情况
代码的可显性
- 让程序处理过程透明
- 对调试器友好。防止因配置错误或外部原因导致程序本身出现错误后,难以诊断的情况发生
推动逻辑的程序应该具备监控开关,这些监控也仅仅是针对组件之间的文本流(表达了执行过程和可能出问题的环节,提高可显性)
在GUI的设计中,让可显性的特质有所体现,但不要妨碍最终用户的使用习惯 聪明的UI设计:找到方法可以访问具体细节,又不让它们太显眼
FreeCiv的案例
- 游戏的大部分固定数据从属性等级表(外部数值文件)中读取,而没有编入代码中。
- 外部数值文件是文本文件,可以独立编辑
- 服务器的启动解析过程会忽略不知道如何使用的属性名,实现了不中断解析前提下对未使用属性的声明 - 这个声明出的问题交给另外一个审核程序来处理 - 这样实现了虽然结构类似windows注册表,但并不像注册表那样容易崩溃。因为解析和处理部分面向客户端,但数据的排错和写入面向游戏维护者
追求代码的透明,不要在具体操作的代码上叠放太多的抽象层
追求透明和可显的编码习惯: - 程序调用层次的最大静态深度(不考虑递归)如果大于4则需要考虑 - 追求代码的不变性质。利于推演代码发现有问题的部分 - API的各个函数调用是否正交。或是否存在太多的标记位,使一个调用要完成多个任务? - 顺手可用的关键数据结构,或全局唯一的记录器用来捕获高层次状态。让这个状态易于形象化或检验 - 数据结构与其代表的外部实体之间,是否是清晰的一对一映射 - 尽量避免函数的hack性质 - no magic number
避免过度保护底层细节,为故障检测带来困难
如果有难以文本化的处理对象需要提升可见性和透明度,编写数据文件文本化的工具。让复杂的二进制对象在调试过程中能和文本格式进行无损转换 如果二进制对象是动态或非常大的,那么可以考虑编写对象专用的浏览器
通过编写开发者手册来增加代码的可维护性
微语言:更具表达力的语言意味着程序更短,bug更少 复杂度上升到一定程度之后,考虑将语言中的控制流从隐式提升到显式
数据驱动 关于配置文件 - 哪些配置不应该暴露给最终用户 - 能用脚本包装或管道实现的任务,就不要用配置开关 - 在增加配置选项之前,先考虑: - 功能是否一定要存在? - 能不能用某种无伤大雅的方式改变常规行为而无需这个配置 - 少考虑GUI的配置性,而多考虑实用性 - 这个配置所附加的行为是否应该抛给单独的独立程序
最小立异原则 - 避免设计违反通用概念的全新接口模型 - 如有可能,尽量允许用户将接口功能委派给熟悉的程序来完成
接口设计评估 - 简洁 - 以较少的动作/状态变化来保证较多的作用效果 - 表现力 - 触发广泛的行为 - 最具表现力的接口可以启动设计者没有预见的行为组合,但仍然对最终用户有效并保持一致性 - 易用 - 为了让用接口用户需要记忆尽量少的内容 - 透明 - 不用记忆多少数据或程序的相关状态 - 自然地给出中间过程的反馈。所见即所得 - 脚本化能力 - 接口能被其他程序共用的能力。提升泛用程度
过滤器设计模式 - 宽进严出(postel原则) - 不丢弃不需要的信息,单独滤出来并保留 - 过滤时不增加无用数据
cantrip模式 - 无输入无输出,只调用一次,产生退出状态数值
源模式 - 无输入,启动条件控制输出内容(如ls)
接收器模式 - 只接纳标准输入,无输出。启动条件控制对输入数据的行为
编译器模式 - 从命令行接受文件或资源,转换之,再用改变后的名字输出。不需要用户交互 - 常见于格式转换命令
ed模式 - 文本编辑器。接受文件并进行所见即所得的编辑
Roguelike模式 - 用ascii字符形成的GUI交互
“引擎和接口分离”模式 - 程序的核心算法和逻辑规格等(引擎)与“接口“(用户命令、显示结果、交互和额外信息等)部分分离 - 引申出MVC设计模式 - 实践中,VC部分通常结合更紧密,往往只在需要多重视图的情况下才分开
MVC衍生 - 配置者/执行者组合 - 逻辑部分专精逻辑,不考虑GUI或表现等对用户更友好的设计 - 配置部分专精打磨GUI和UX,方便用户配置等功能 - 两者通信通道中实现互相配合 - 假脱机/守护进程 - 前端的作用就是把请求或数据扔进池 - 守护进程就是负责轮询看池里面是否有需要处理的内容。成功后从池里面删掉
- 驱动/引擎组合(???) - C/S组合
基于语言的接口模式 - 内嵌脚本语言 - 如emacs,GIMP
长时间的操作要提供进度条——帮助用户有效地分时利用大脑,暗示等待完成的过程中可以去干点别的 GUI不要乱出警告和确认,焦点不在父窗口的时候应该避免这些信息 界面设计师的工作是方便用户,而不是在用户面前碍眼
接口整体应遵从最小立异原则,但信息内容应该符合最大惊奇原则——仅对偏离期望的看情况详细说明
过早优化乃万恶之源
永远不要将核心数据结构和时间关键循环抛出缓存
选择合适的时机刷新GUI,而不是每条都刷,适当攒一波
控制复杂度的困难在于:如何在代码量,接口复杂度和实现复杂度三者之间进行权衡
最简原则暗示:选择需要管理的上下文环境,并且按照边界所允许的最小化方式构建程序
Unix吝啬原则:首先寻找小巧程序的解决方案。如果单个小程序无法完成这项工作,尝试在现有框架结构内构造一个协作小程序工具包来解决问题。如果两者都失败,才自由地构建一个巨型程序(或一个新框架)
创建框架时,牢记分离原则:框架是一种机制,应该尽可能少地包含策略。尽可能多地将需求和行为分解到使用框架的模块中去