第7章 高质量的子程序
7.1 创建子程序的正当理由
- 降低/隔离复杂度,隐藏实现细节,引入中间的、易懂的抽象
- 避免代码重复,支持子类化
- 提高可移植性,限制变化所带来的影响
- 简化复杂的逻辑判断,改善性能
7.2 在子程序层上设计
- 功能的内聚性:只做了一件事并把它做得很好,操作与名称相符
- 顺序上的内聚性:包含需按特定顺序执行的操作,它们共享数据且只有全部执行完功能才完整
- 通信上的内聚性:不同操作使用了同样的数据,但不存在其他任何联系
- 临时的内聚性:包含一些需要同时执行才放在一起的操作
- 其他类型的内聚性:基本是不可取的,它们会导致代码组织混乱、难于调试、不便修改
- 子程序间耦合松散,连接小、明确、可见并灵活
7.3 好的子程序名称
- 能准确描述子程序所做的全部事情:避免使用表述不清的动词;避免用数字区分不同子程序
- 函数命名时应针对返回值有所描述
- 过程命名时用语气强烈清晰的"动词+宾语"形式:类的过程不用加入对象名(宾语)
- 准确使用对仗词:add/remove,source/target,next/previous等
- 为常用操作确立命名规则:如统一命名方式
x.Id() y.GetID()
为其中一种
7.4 子程序可以写多长
- 允许有序增长到100~200行(不含注释),该长度与小而美的子程序一样不易出错,超过200行易遇到可读性等问题
- 限制长度不如关注 – 内聚性、嵌套层次、变量数量、决策点数量、注释数量来决定长度
7.5 如何使用子程序参数
子程序间的接口是最易出错的部分之一:39%错误源于互相通信时
- 参数保持顺序统一:如输入-修改-输出-状态参数最后;多个子程序使用类似参数顺序保持一致
- 参数使用语义提示:如
const
标记输入、&
标记修改,或加i/m/o/s
等前缀 - 使用所有的参数且避免用输入参数做工作变量:传递了就要用到,否则删掉;避免混淆
- 对特征参数的假定加以说明:如输入、修改或输出;参数单位;非枚举状态值含义;数值范围;不该出现的特定值
- 参数个数限制在7个以内:心理学研究表明超过7个单位的信息很难记住,多个参数考虑合成数据类
- 传递对象还是成员做参数 – 取决于子程序接口的抽象:
- 参数传递:期望的几项特定数据碰巧来自一个对象;
- 对象传递:想持有某个特定对象并要进行某些操作(经常修改参数列表且都来自同一个对象)
- 确保实参和形参相匹配:检查参数类型并留意编译器警告
7.6 使用函数时要特别考虑的问题
- 若用途为返回其名称所指明的返回值就应用函数,否则用过程
- 设置函数返回值:
- 检查所有可能的返回路径并在开头用默认值初始化;2. 不要返回指向局部数据的引用或指针
宏/内联子程序:尽量避免使用宏()
包含整个宏表达式;2.{}
括起含有多条语句的宏;3. 展开后形同子程序的宏命名同子程序以便替换
节制使用inline子程序:除非剖测(profile)时得到不错的性能收益
第11章 变量名的力量
11.1 选择好变量名的注意事项
- 准则:变量名要完全准确地描述其所代表的的事物(可读、易记、无歧义、长度适中)
- 问题为导向:好的命名表达的是“什么”(what),而不是“如何”(how)
- 恰当的名字长度:
- 平均长度在8~20个字符最易于调试 – 若发现很多短名时应检查其含义是否足够清晰
- 较长的名字适用于很少用到的、全局变量,较短的适用于局部、循环变量
- 对全局变量名加以限定词:如命名空间(namespace)、包名(package)或带有子系统特征的前缀
- 限定词前置突出含义:为变量赋予主要含义的部分应位于最前面
11.2 为特定类型的数据命名
- 循环变量:短循环用
i、j
等;长循环或循环外变量应使用可读性高的命名 - 状态变量:使用枚举、具名常量;当某段代码不易读懂时就应考虑重命名
- 临时变量:即时“临时”最好也取个可读性高的名称
- 布尔变量:使用肯定且隐含"真/假"含义的名字,如
found
等而非notDone
等难于阅读的否定词 - 枚举类型:使用组前缀;若使用须冠以枚举名则无需前缀如
Color.Red
- 具名常量:应根据常量表示的含义而非具有的值为其命名
11.3 命名规则的力量
- 何时采用命名规则:
- 多人合作、维护或需他人评估的项目时
- 大规模程序,脑海里无法同时了解事情全貌必须分而治之时
- 生命周期长的项目,长到搁置几星期/月后又需重启工作时
- 当项目中存在一些不常见术语,希望在编码阶段使用标准术语或缩写的时候
11.4 非正式命名规则
- 前缀标识全局变量
g_
、具名常量c_
、参数变量a
、局部变量l
、成员变量m_
、类型声明T
- 语法标明并限制只读参数
const
、可修改参数&、*
- 格式化命名保持一致风格:如一直单用驼峰命名或匈牙利命名法
11.5 标准前缀
- 用户自定义类型(UDT):缩写标识出数据类型,如字符
ch
、文档doc
- 语义前缀(不随项目变化):如指针
p
、全局变量g
11.6 创建可读的缩写
- 使用标准缩写(列在字典的常见缩写)
- 去掉虚词
and/or/the
等,去除无用后缀ing/ed
等 - 统一在某处截断或约定成俗的缩写如
src/succ
等 - 使用名字中每一个重要单词且最多不超过三个
- 缩写要一致;可读出来;避免易看/读错的字符组合;
- 项目级缩写辞典:创建新缩写时加以说明并归档,只有不惜花费精力写文档的缩写才的的确确因当被创建
11.7 应该避免的名称
- 避免容易令人误解或混淆(相似含义/易拼错/数字)的名称或缩写,如
I/1/l
等 - 避免含义不同却名字相似,避免仅靠大小写区分
- 避免使用发音相近的名称,不易于讨论
- 避免混合多种自然语言(中英混杂)
第8章 防御式编程
8.1 保护程序免遭非法输入数据的破坏
- 核心思想:子程序不会因传入错误实参而被破坏,哪怕是由其他子程序产生的错误数据
- 检查所有外部来源数据的值;检查所有输入参数的值;决定如何处理错误的输入数据;
8.2 断言(Assertion)
- 断言主要用于开发和维护阶段:
- 检查输入或输出值在预期范围内(如指针非空;数组或容器容量足够;表已初始化存储着有效数据)
- 检查子程序开始(结束)执行时文件或流处于打开(关闭)状态,且读写位置位于开头(结尾),检查读写模式
- 检查仅作为输入的参数值是否被子程序所修改
- 使用断言的指导建议:
- 用错误代码处理预期内(或系统内部)状况,用断言处理绝不应该发生的状况(触发则修改源码)
- 避免将需要执行的代码放到断言中
- 用断言注解并验证前条件(调用方确保参数正确)和后条件(被调用方确保返回正确)
- 对于高健壮性的代码(大规模长周期复杂项目),应先使用断言再使用错误处理代码
8.3 错误处理技术
- 处理预期内可能发生的错误方式:
- 返回中立值/最接近的合法值:如指针操作返回NULL,超出范围返回最大值
- 换用下一个正确的数据,返回与前次相同的数据
- 将警告信息显示/记录到日志文件中(注意数据隐私)
- 返回一个错误码:只处理部分错误其余报告调用方有错误
- 关闭程序:适用于安全攸关倾向于正确性的程序
- 在架构层次确定错误参数处理方式,并始终如一地采用该种处理方式
8.4 异常
- 异常 – 将不甚了解的出错转交给调用链其他子程序更好地解释,使用建议:
- 发生了不可忽略的错误需要通知程序其他部分
- 只在发生真正罕见或无法解决的问题下才抛出异常,可局部处理的直接处理掉
- 避免在构造/析构函数中抛出异常
- 抛出的异常应该与接口的抽象层次一致
- 在异常消息中加入关于导致异常发生的全部信息
- 了解所用函数库可能抛出的异常,未能捕获将导致程序崩溃
- 创建一个集中的(统一存储格式化)标准化的(规定使用场合/异常对象类型等)异常报告机制
8.5 隔离程序,使之包容由错误造成的损害
- 隔栏(手术室)技术 – 数据进入前"消毒",之后都认为是安全的
- 只在类的公开方法检查并清理数据,而类的私有方法不再承担校验职责
- 在输入数据后立即将其转换为恰当的类型
- 隔栏外部的程序使用错误处理技术,而隔栏内部应使用断言(数据已清理,出错为程序问题)
8.6 辅助调试的代码
- 不要把产品版的限制强加于开发版:在开发期牺牲某些,用以换取让开发更顺畅的内置工具
- 尽早引入辅助调试的代码
- 采用进攻式编程:
- 确保断言语句使程序终止运行 – 问题引起的麻烦越大越易被修复
- 完全填充分配到的内存/文件/流 – 易于排查内存分配/文件格式错误
- 确保
case
语句的default/else
分支都产生不可忽视的提示 - 在删除一个对象前把它填满垃圾数据
- 可以的话将错误日志文件发送到email
- 计划移除调试辅助的代码
- 使用类似ant和make的编译工具
- 使用预处理器(内置/自定义的编译条件)
- 使用调试存根:
stub存根子程序
开发阶段用于各种校验日志输出,发布时立即将控制权交还调用方
8.7 确定在代码中该保留多少防御式代码
- 保留那些检查重要错误的代码,去掉检查影响细微错误的代码
- 保留能让程序稳妥地崩溃的代码,去掉会导致硬性崩溃/数据丢失的调试代码
- 为技术支持记录并保存错误日志
- 确认错误显示消息对用户而言是友好的
8.8 对防御式编程采取防御的姿态
- 避免过度使用,因地制宜的调整防御式编程的优先级
第18章 表驱动法
- 从表里查找信息而不使用逻辑语句
if和case
; 将复杂的逻辑链/继承结构用查表法替代
18.1 表驱动法使用总则
- 如何从表中查询条目:直接访问,索引访问,阶梯访问
- 应该在表里存些什么:数据还是函数
18.2 直接访问表
- 无须绕很多复杂的圈子就能在表里找到需要的信息
- 构造查询键值:
- 复制重复信息直接使用键值;
- 转换键值使其能直接使用,将键值转换独立成子程序
18.3 索引访问表
- 将基本数据映射为索引表的一个键值,再用键值关联主数据表
- 优点一:避免主查询表单条记录过大和重复造成的空间浪费:如商品码099→商品类型AE→A~E商品信息
- 优点二:操作索引中的记录比操作主表中的记录更方便更廉价:如员工姓名索引,薪水索引
- 优点三:良好的可维护性:将索引查询提取为单独的子程序,方便更换查询技术等
18.4 阶梯访问表
- 表中的记录对不同数据范围有效,而非不同的数据点 – 适合处理无规则数据
- 留心端点:确认已考虑到每个阶梯区间的上界
- 超多阶梯考虑用二分查找取代顺序查找
- 将查询操作提取为单独的子程序; 考虑用索引替代
第4章 关键的"构建"决策
4.1 选择编程语言
- 更熟悉或更高级的编程语言将达到更好的生产率和质量
- 了解诸如面向对象、面向过程、脚本等语言的明确优点和弱点
4.2 编程约定
- 高质量软件其"架构的概念完整性"与"底层实现"保持着内在的固有的一致性
- 架构的指导方针使得程序的结构平衡:如绘画设计,其中一部分古典主义而一部分印象主义是不可能具有"概念完整性"的
- 针对"构建活动"的指导方针(格式约定等)提供了底层的协调,将每个类都衔接到一种完整的设计中,成为可靠的部件
4.3 你在技术浪潮中的位置
- 编程工具不应该决定你的编程思想:首先决定要表达的思想,再决定如何去表达出来
- 编程原则并不依赖特定的语言,如果语言缺乏就应该试着去弥补它,发明自定的编码约定、标准、类库及其他改进措施
4.4 选择主要的构建实践方法
- 编码
- 是否确定哪些设计工作要预先进行,哪些设计在编码时进行?
- 是否规定了诸如名称、注释、代码格式等"编码约定"?
- 有无规定特定的由软件架构确定的编码实践:如何处理错误条件/安全性事项、类接口有哪些约定、考虑多少性能因素等
- 是否确定你在技术浪潮中的位置并有相应的调整计划和预期目标?是否知道如何不受限于某种编程语言?
- 质量保证
- 编码前是否要先编写测试用例?需要为自己的代码编写单元测试么?
- check in代码前,会用调试器单步跟踪整个代码流程吗?是否进行集成测试?
- 会复审(review)或检查别人的代码吗?
- 工具
- 是否选用SVN/GIT版本控制及相关工具?
- 是否选定了一种语言(版本)或编译器版本?是否允许使用非标准的语言特性?
- 是否选定了某个编程框架(J2EE 或 .NET),或明确决定不使用框架?
- 是否选定并拥有了将要用到的工具——IDE、测试框架、重构工具、CI等?
第33章 个人性格
33.1 个人性格是否和本书话题无关
- 下定决心成为出色的程序员:聪明无法提升,性格却可改进,而个人性格对造就高手有决定性意义
33.2 聪明和谦虚
- 如何专注你的才智比你有多聪明更重要:越了解自己的局限性越能保持谦虚,进步也就越快
- 将系统"分解",使之易于理解
- 进行审查、评审和测试,减少人为失误; 应和他人沟通(三人行必有吾师),以提高软件质量
- 通过各种各样的规范,将思路从相对繁琐的编程事务中解放出来
33.3 求知欲
- 在成长为高手的过程中,对技术的求知欲具有压倒一切的重要性 – 技术不断更新,跟不上就会落伍
- 在开发过程中建立自我意识:阅读学习并实践,若工作中学不到新东西就应考虑换新工作
- 对编程和开发过程做试验:学会迅速编写小实例去试错,并能从中有所收获
- 阅读问题的相关解决方法:人们并不总能自行找出解决问题的巧妙方法,所以要学习他人的解法
- 在行动之前做分析和计划:不要担心分析太久,等钟摆走到"行动"快中央的位置再说
- 学习成功项目的开发经验,研究高手的程序:如《编程珠玑》; 找些一流程序员评论你的代码
- 阅读文档:文档中有许多有用的东西值得花时间去看 – 例如每两月翻翻函数库文档
- 阅读其他书本期刊:每两月看本计算机好书(35页/周),过不了多久就能掌握本行业脉搏并脱颖而出
- 同专业人士交往:与希望提高技术的人为伍,参加交流会/群
- 向专业开发看齐
- 1入门级:会利用某语言基本功能或特性编写类、流程语句
- 2中级:能利用多种语言的基本功能,并会得心应手地使用至少一种语言
- 3熟练级:对语言或环境(或两者兼具)有着专业技能,或精通J2EE的盘根错节,或对C++引用如数家珍
- 4技术带头人级:具有第3级的专业才学,并明白工作中85%的时间都是与人打交道 – “为人写代码,而非机器,所写代码晶莹剔透还配有文档”
33.4 诚实
- 承认自己"不知道",不假装是高手 – 听听别人说法,学到新内容,并了解他们是否清楚讨论的东西
- 犯了错误应立即主动承认 – 复杂的智力活动有潮起潮落,错误情有可原,这也是强调测试的原因之一
- 力图理解编译器的警告而非弃之不理 – 忽略警告,时间就很可能会浪费在调试上
- 透彻理解自己的程序,而不只是编译看看能否运行 – 测试只能找出错误,不能确保"不存在错误"
- 提供实际的状况报告 – 深思熟虑后冷静地在私下汇报项目真实状态,管理者需要准确的信息以便协调
- 提供现实的进度方案,在上司前坚持自己的意见 – 如"无法商量项目该花多少时间,就像不能商量确定一里路有几米一样,自然规律是不能商量的。但我们可协商影响项目进度的其他方面,比如减少些特性,降低性能,分阶段开发,少些人时间