《代码大全》笔记
⚠️ 文章已过期:本文最后更新于 2019-12-17,距今已超过 6 年,文中内容可能已过时,请谨慎参考。
代码大全
什么是代码大全?
代码大全是一本书,讲的是如何写代码,与具体的编程语言无关,而是以方法论为主,很厚,九百多页的砖头。
2008年的时候,我在成都某新华书店第一次看到一本叫《代码大全》的书,我还以为是一本摘抄了各种代码的类似《食谱大全》的书,因此都懒得去翻开看一下,因为觉得抄代码有互联网就足够了,这件事可能说明了两个问题:
- “代码大全”这个书名翻译得太烂;
- 我当年很无知。
《Code Complete》实在是应该翻译为《代码完成》比较合适,据第二版的译者说,因为第一版翻译为《代码大全》,他们也只好将错就错。市面上的计算机类书籍汗牛充栋,大多是针对具体编程语言的,要不就是讲需求分析、软件设计、软件测试,直接讲软件构建的书反而很少,很多人也学了一些软件工程的皮毛,但是代码写出来还是不忍卒读,用本书的观点来说,只有软件构建——直白一点说叫写代码,才是一个程序员才会确实去做的事,至于其他部分,往往都伴随着谎言。
俗话说“走都不会走,还想学跑?”,一个程序员,先老老实实把自己写代码的水平提高,实在是最基本也最重要的,在学校教育经历中,一般人写的都是“一次运行,用完就扔”的学生代码,与做软件产品的要求相距甚远,而这本书,在教你怎么把代码写好这方面,真是事无巨细,处处闪耀着真知灼见,从这个角度来看,叫它大全,也不为错。比如我的编程入门教材是谭浩强的《C程序设计》,里面的变量命名风格是单字母风格,而本书则花了一章来讲变量名的问题,对于单字母门派出来的人,实在是一剂良药。
我从大概2015年买了本英文版读,读了5章后停下来,直到今年(2019)把中文版读完,而这本书的英文版出版于2003年。十几年过去了,书中很多方法论已经是业界公论。但方法论说得溜是当代程序员的特点,而实践水平倒是不见得有什么提升,热衷于谈框架、谈机器学习,但好的代码依然少见。
这份笔记会补充一些我自己近年的思考与近年的技术方法。
第1部分 打好基础
所谓磨刀不误砍柴功,要做什么是一个看似简单,实则复杂的问题。
第1章 欢迎进入软件构建的世界
一般软件工程的书会把过程划分的很清楚,软件开发的活动包括:
- 问题定义
- 需求开发
- 构建计划
- 软件架构
- 详细设计
- 编码和调试
- 单元测试
- 集成测试
- 集成
- 系统测试
- 修正维护
代码构建的核心是编码和调试,为了编码和调试,会或多或少牵涉到其他活动,非正式开发的时候,我们可能直接坐下来敲代码,上面的所有活动都打包了。而软件构建,讲的大概是敲代码的时候干的那些事情,你如果真的敲过代码,就知道大部分时间其实你是对着屏幕发呆状而不是在敲键盘(只有电影里的黑客才不听的噼里啪啦敲键盘),要做的事情包括:
- 检查准备工作是否已经做好,可以安心敲代码了
- 想好如何测试代码
- 设计和实现类和函数
- 创建变量和命名常量
- 选择控制结构,组织语句
- 单元测试,集成测试,调试代码
- 相互Review代码
- 通过格式化和好的注释来打磨代码
- 集成分别开发的组件
- 修改代码使其运行更快占用资源更少
为什么软件构建很重要?
- 软件构建是软件开发的主要活动——大部分时间都在码代码
- 软件构建是软件开发的中心活动——没有代码,再多的文档都是扯淡
- 关注软件构建可以极大提高每个程序员的生产力——大部分的程序员基本功不过关
- 软构建的产品——代码,经常是软件唯一精确的描述——文档都是骗人的
- 软件构建是唯一保证会做的工作——再懒也不能用PPT开发
第2章 用隐喻来更充分地理解软件开发
2.1 隐喻(Metaphors)的重要性
人类理解事物是从已知到未知的,作者举了科学史上很多例子说明metaphor的重要性,我觉得,能打好一个比方其实是很难的,善于打比方的人是最聪明的一类人,这可能需要大脑回路比较丰富才行。对一个事物理解越深刻,想出来的比喻就会越贴切,或者说模型就会越精确。
2.2 如何使用软件的隐喻
比喻不是算法,不会给出明确的指引,而是具有启发性的,会引导你思考的方向。
- 帮你洞察问题和处理
- 帮你找到更好的做事情的方式
2.3 一般的软件隐喻
软件书写:写代码
写代码这个说法很常见,是借喻写文章而来的,这种借喻有很多不恰当的地方,比如代码更易于修改,基本上很难有“完成”的一天,代码不强调原创而强调复用。
基本上,这是一个糟糕的借喻,只对写很小的软件适用。
软件农场:种植系统
软件系统确实有不断增长的一面,但却不是播了种子施肥浇水等收获的概念。
软件牡蛎农场:系统增生
这个隐喻说的是渐进增强,像牡蛎一样,在沙粒上一层层裹碳酸钙,最后长成珍珠。这个概念近年来越来越火了,不单软件工程提,关于创业的理论也提。我个人的体会是:对需求不明的项目,一个基本的初步实现是讨论的基础,也是信心的来源。
软件构建:建造软件
这是最重要的一个隐喻了,它源自建筑行业,开发软件和修建筑有很多相似点
- 复杂度和尺寸的影响很大
- 复用,而不是从头实现
- 个性化定制
- 好的规划能让后续细节的调整更容易
- 不同的项目使用不同的方法
- 越晚修改代价越大
- 对超大型项目,动手构建前的准备极为重要
应用软件技术:智力工具箱
工具箱是一个很好的比喻,以免我们迷失在某个方法论或技术中,“拿个锤子,看什么都像钉子”,好的匠人,必然是有一个好的工具箱的,面对具体的情况,使用不同的工具。一个程序员应当不断扩充自己的工具箱。
合成隐喻
隐喻是启发式的,只要有利于说明我们的问题,都没有必要拒绝。
第3章 三思而后行:前期准备
不同规模的构建,需要不同程度的准备工作,项目的成败,早已埋下伏笔。构建一般占项目时间的65%,而糟糕的准备工作会让构建时间增加两三倍。
3.1 前期准备的重要性
高质量的软件来自高质量的实践,如何保证质量?
- 前期:关注需求、设计。
- 中期:关注构建。
- 后期:关注测试。
本书重点关注中期,但是在开始构建之前,需要弄清前期准备做到了什么程度,贸然开场容易悲剧。
现代软件开发是否需要前期准备?
需要。前期准备可以降低风险,不同的项目可能需要不同的前期准备实践,但不能没有。
不完整的前期准备的理由
- 开发者没有经验——那就去学啊。
- 按捺不住写代码的心——多碰几次壁。
- 上级让你赶紧写代码!——拒绝上级;假装在编程;教育上级;跳槽。
构建之前做好前期准备的理由
你可以拿它来教育别人。
诉诸逻辑
- 需要弄清楚到底想构建个啥。
- 需要弄清楚怎么去构建系统。
诉诸类比
- 造房子
- 软件开发食物链:程序员在食物链尽头。
诉诸数据
- 最后阶段更改的代价是10~100倍。
老板敏捷测试
自我实现预言:
- 我们必须立即开始编码因为还有很多调试工作要做。
- 我们没有计划多少时间去测试因为不会找到太多的缺陷。
- 我们充分研究了需求和设计,在编码和调试的时候不会遇到大的问题。
3.2 确定你开发的软件类型
针对不同的软件类型,需要不同的生命周期模型:
- 商业系统
- 关键任务系统
- 嵌入式性命攸关系统
迭代方法在前期准备中的作用
- 先做80%的需求分析,则类似串行
- 先做20%的需求分析,则类似迭代
在迭代方法与串行方法间的选择
- 需求是否明确?
- 设计是否复杂?
- 团队对领域是否熟悉?
- 项目风险大不大?
- 更改需求、设计的代价如何?
3.3 问题定义
- 好的问题定义弄清楚问题是什么,而不要考虑实现。
- 问题定义在需求分析之前,使用用户语言描述。
- 最好的解决方案可能不需要写软件。
- 除非是为了解决计算机系统问题,否则不使用计算机语言。
- 问题定义出错的代价:浪费了时间,没有解决真正的问题。
3.4 需求分析
需求分析讨论软件怎么解决问题,是解决方案的第一步。
为什么需要正式的需求?
- 确保用户而非程序员决定系统功能,程序员明确知道用户需要什么。
- 避免争吵,以需求为准。
- 好的需求会减少开始构建后的变更。
稳定需求的神话
稳定的需求是软件开发的圣杯。
- 用户往往不能准备描述需求。
- 只有充分使用系统,才能深入理解系统。
- 开发过程会使用户更好的理解系统,这也是需求变更的主要来源。
- 典型的系统会有25%的需求变更,花费70%~85时间重做。
在构建时应对需求变化
- 检查需求分析的质量,如有问题,尽早返工。
- 确保每个人明白需求变更的代价,正确应对“特性中毒者”。
- 建立变更控制过程,做到程序员顾客都满意。
- 使用能适应需求变更的开发方法:原型进化、渐进交付、小开发周期。
- 放弃项目。
- 始终关注项目的商业价值。
3.5 架构设计
架构设计,一般对应“概要设计文档”,架构决定了系统的“概念完整”。架构设计进行工作分解,直至程序员可以上手工作。
第4章 关键构建决策
4.1 选择编程语言
语言选择事关效率,语言熟悉——效率高30%,熟练工——3倍效率。一定程度上,思想表达能力取决于词汇量。 表达能力:C,1;C++,2.5;Python,6. 不要使用了一门新的语言,却不使用它好的机制,还是完全用旧语言的模式编程。 语言描述:
- Ada 数据抽象,信息隐藏,用于军队、航空航天。
- 汇编 处理器相关,代码体积或速度有严格限制时应用。
- C 普适,中等语言,可移植的汇编(指针、地址、位运算、弱类型),高级特性:结构体,控制流,机器独立,丰富的运算符。
- C++ C基础上的面向对象语言,类、多态、异常处理、模板、类型控制、标准库
- C# 类似C、C++、Java(个人认为C#语言本身是很优秀的,融合了部分函数式编程的思想,但是社区支持比Java略差)
- Cobol 通用商业语言(银行还在用)。
- Fortran 最早的高级语言之一,过去科学计算和教学比较多使用(现在Python用得更多吧)。
- Java (最早的虚拟机语言,社区庞大。)
- JavaScript (无处不在了)
- Perl 擅长字符处理(跟Python不同的哲学)
- PHP (世界上最好的语言😀)
- Python (万能胶水,随着机器学习而变成了显学)
- SQL 结构化查询语言,主要用于数据库。
- Visual Basic (拉控件编程的经典)
最近几年的新语言:
- go C++替代之一,代表作品docker
- rust C++替代之二
- dart JavaScript 替代之一,用于框架flutter
4.2 编程约定
高层的架构与底层实现要保持一致,统一的规范不但利于团队合作,还可以解放脑力。
4.3 你在技术浪潮中的位置
处在一个技术的早期,风险与额外的收益并存,技术成熟期则风险小,中规中矩。 注意Program in a language 和 Program into a language的区别,语言提供了工具箱,但是不要局限于语言的工具箱。
如果能多学一些语言,再融汇贯通,就能理解语言的精髓,而不局限于语言。可参考:
4.4 选择你主要的构建实践
当前针对各种编程语言,一般都会有最佳实践一类的东西,或者有优秀的开源项目作为参考,总之刚开始不要自己摸索,用前人的最佳实践做几个项目之后再考虑根据自己的需要做优化。
做好最佳实践,能够把自己的精力集中到代码的逻辑中去,减轻代码管理、调试等工作的复杂性。
第2部分 创建高质量的代码
第5章 构建中的设计
设计不太可能在构建之前完成。
5.1 设计挑战
- 设计是一个邪恶的(wicked)问题:只能通过解决这个问题或部分解决问题才能清楚的定义这个问题。这是 真正的程序和学生作业的区别。
- 设计是一个草率的过程:会犯错;好的方案很精妙;“足够好”很难定义。
- 设计关乎权衡和优先级
- 设计牵涉约束
- 设计是非决定性的:有很多途径
- 设计是一个启发式的过程:没有银弹
- 设计是自然发生的
5.2 关键设计概念
软件的首要技术法则:管理复杂性
偶然的和本质的困难: 此划分来自亚里士多德的哲学。 偶然的:笨拙的语法、非交互式的计算机、难以一起工作的工具; 本质的:与真实世界复杂、无序的接口,异常情况,精确性,准确定义真实世界如何运行。
管理复杂性的重要性失败的原因:糟糕的需求/计划/管理,失控的复杂性管理。 程序太大,人类无法一次性理解。问题分级后易于理解(如包管理),保持从程序短小,写代码时考虑人类的极限。
如何消灭复杂性:减少每个人脑中需要一次性处理的固有复杂性;防止不必要复杂性的增殖。
理想的设计特征
- 最小复杂度:聪明的 vs 简单易维护的。
- 易维护:为代码维护者编程,自解释的设计。
- 松耦合:不同部分之间的联系尽量少。
- 可扩展性:更改时减少创伤。
- 可复用性
- 高扇入:充分复用底层模块
- 低扇出:不过度依赖其他部分
- 可移植性
- 精简:没有多余部分
- 分层化:隔离变化
- 标准技术:不要用古怪的技术
设计的层次
- 软件系统
- 子系统或包:子系统间的通信必须加以限制,否则会不断熵增,一片混乱,可以先紧后松。常见的子系统:商业规则、用户接口、数据存取、系统依赖。
- 类:包括接口。大项目由子系统划分,定义一些外部函数;小项目由系统直接划分。
- 例程:可能会引起第3层的修改。
- 例程内设计
5.3 设计建造的砖块:启发式
一个不断试错的过程。
找到现实世界的对象
- 标识对象和其属性
- 决定改对象能干啥
- 决定每个对象能对其他对象干啥
- 决定对象各部分的可见性
- 决定对象的公开接口
一个迭代过程:重新组织,类的细节修改。
组织一致的抽象
在不同的层次忽略细节。集合=>抽象。基类、接口。
- 函数层次
- 类层次
- 包或子系统层次
封装实现细节
继承——当继承简化设计时使用
信息隐藏
- 结构化设计:黑盒
- 面向对象设计:封装、模块化、抽象
秘密和隐私权
两种策略
- 隐藏复杂性使你的大脑不需要考虑它,除非特别关注时
- 隐藏更改源,隔离变化
信息隐藏的障碍
- 分布过广的信息
- 环形依赖
- 不当的全局数据
- 担心性能损失
信息隐藏的价值
启发性的力量,设计好的公共接口。问正确的问题:这个类需要隐藏什么?
标示易变区域
标示=>分割=>隔离
- 商业规则
- 硬件依赖
- 输入输出
- 非标准语言特性
- 设计和构建困难的区域
- 状态变量:使用枚举代替bool值;使用存取函数代替直接访问。
- 数据大小限制
预期不同程度的变化
先找出最小子集,再做功能性、质量性改进。
保持松耦合
耦合标准
- 大小:模块间联系的数量,如函数的参数个数,类的公开方法数量;
- 可见性:明确引用,胜过更改全局变量。
- 可变性:容易改变连接,被不同的模块调用。
耦合的种类
- 简单数据参数耦合:模块间传递了简单数据类型。常见、可接受。
- 简单对象耦合: 一个对象初始化了另一个对象。合理。
- 对象参数耦合:对象1需要对象2向其传递对象3。耦合较紧密。
- 语义耦合
- 模块1传递了一个标志给模块2,告诉模块2应该去干啥。如果模块2定义了一个数据类型来表示该标志,则合理。
- 模块2使用模块1更改过的全局变量。有时序假设,不好。
- 模块1在调用某函数前必须先调用初始化函数。
- 模块1传递对象给模块2,知道模块2将使用对象的部分方法,只对对象进行部分初始化。
- 模块1传递基类给模块2,模块2知道模块1实际传递的是派生类,强制转化后使用。
危险:使用了编译器无法检测的方法使用别的类。 一旦使用了,你就得自己负责正确性。
寻找一般的设计模式
- 模式通过已经准备好的抽象来降低复杂性
- 模式使一般解决方案制度化以减少错误
- 模式通过建议设计选择提供启发性价值
- 模式通过提升设计语言测层次使沟通更流畅
其他的启发式方法
- 高内聚
- 构建继承体系
- 正式化类协议
- 职责分配
- 为测试设计
- 避免失败
- 有意识的选择绑定时间
- 使控制点集中
- 使用暴力方法
- 画图示
- 保持设计模块化
启发性方法指导
Polya's How to Solve It 不要卡在一个小问题上,你不需要一次性解决所有的设计问题。
5.4 设计实践
迭代
发现不顶用的设计。
分治
自顶向下与自底向上
两种方法论应当结合使用。自顶向下容易入手,但是有时底层的复杂性会影响到上层;自底向下难入手,但是能及早的管理复杂性,使得上层设计更容易。
试验原型
协作设计
- 起身去问协作者
- 和协作者坐在一起用白板讨论草图
- 结对编程
- 开会向其他协作者讲解设计
- 正式的审查
- 冷处理自己的设计,再回头看
- 求助公司外的人,在论坛上提问
多少设计才足够?
一般来说,越细越好;设计文档不是越多越好。
记录你的设计工作
- 在代码中插入设计文档
- 在Wiki中记录设计的讨论和决定
- 写e-mail总结
- 拍照
- 保存设计海报
- 使用“类-职责-协作”卡片
- 在适当的细节层次使用UML图
5.5 对流行方法学的评论
不要过多关注设计方法学,关注设计本身,不设计或事无巨细的设计都不可取,还是要具体情况具体分析。
第6章 可以工作的类
6.1 类的基础:抽象数据类型(ADTs)
一些数据以及对这些数据操作的集合。ADT是类的基础,但不一定以类的形态存在。
使用ADT的益处:
- 可以隐藏实现细节
- 改动不会影响到整个程序
- 让接口能提供更多信息
- 更容易提高性能
- 让程序的正确性显而易见
- 程序更具自我说明性
- 无须在程序内到处传递数据
- 你可以像在现实世界中那样操作实体
ADT示例:
- 把常见的底层数据类型创建为ADT并使用这些ADT,而不再使用底层数据类型
- 把像文件这样的常用对象当成ADT
- 简单的事物也可以当作ADT
- 不要让ADT依赖于其存储介质
在非面向对象环境中用ADT处理多分数据实例(比如C语言):
- 每次使用ADT服务子程序时都明确指定实例(使用id)
- 明确地向ADT服务子程序提供所要用地数据
- 使用隐含实例(需要跟踪当前实例,容易出错)
类=ADT+继承+多态
6.2 良好的类接口
- 展现一致的抽象层次
- 理解类所实现的抽象是什么
- 提供成对的服务
- 把不相关的信息转移到其他类中
- 尽可能让接口可编程,而不是表达语义(编译器可检查)
- 谨防在修改时破坏接口的抽象
- 不要添加与接口抽象不一致的公用成员
- 同时考虑抽象性和内聚性
良好的封装
- 尽可能地限制类和成员的可访问性
- 不要公开暴露成员数据
- 避免把私用的实现细节放入类的接口中
- 不要对类的使用者做出任何假设
- 避免使用友元类
- 不要因为一个子程序仅使用公用子程序,就把它归入公开接口
- 让阅读代码比编写代码更方便
- 要格外警惕从语义上破坏封装性
- 留意过于紧密的耦合关系
6.3 有关设计和实现的问题
包含:有一个的关系 has a
- 通过包含来实现有一个的关系
- 在万不得已时通过private继承来实现有个的关系
- 警惕有超过7个数据成员的类
继承:是一个的关系 is a
- 用public继承来实现是一个的关系
- 要么使用继承并进行详细说明,要么就不要用它
- 遵循Liskov替换原则
- 确保只继承需要继承的部分
- 不要覆盖一个不可覆盖的成员函数
- 把共用的接口、数据以操作放到继承树中尽可能高的位置
- 只有一个实例的类是值得怀疑的
- 只有一个派生类的基类也值得怀疑
- 派生类覆盖了某个子程序,但在其中没做任何操作,这种情况也值得怀疑
- 避免让继承体系过深
- 尽量使用多态,避免大量的类型检查
- 让所有的数据都是private
多重继承
- 太复杂,一般不建议使用
- mixins语义可以使用f
总的来说,继承要慎用,因为它一定程度上有悖于控制复杂性的基本理念。
成员函数和数据成员
- 让类中子程序的数量尽可能少
- 禁止隐式地产生你不需要地成员函数和运算符(可以更改默认行为,比如让其private)
- 减少类所调用的不同子程序的数量
- 对其他类的子程序的间接调用要尽可能少(A可以调用A创建的对象B的子程序)
- 一般来说,应尽量减少类和类之间相互合作的范围
构造函数:
- 如果可能,应该在所有的构造函数中初始化所有的数据成员(类似pycharm的IDE往往会有警告)
- 用私用构造函数来强制实现单件属性
- 优先采用深层复本,除非论证可行,才采用浅层复本
6.4 创建类的原因
- 为现实世界中的对象建模
- 为抽象的对象建模
- 降低复杂度
- 隔离复杂度
- 隐藏实现细节
- 限制变动的影响范围
- 隐藏全局数据
- 让参数传递更顺畅
- 建立中心控制点
- 让代码更易于重用
- 为程序族作计划
- 把相关操作包装到一起
- 实现某种特定的重构
应该避免的类
- 避免创建万能类
- 消除无关紧要的类
- 避免用动词命名的类
6.5 与具体编程语言相关的问题
不同语言在实现类的方法上有很多差别。比如类的覆盖。
6.6 超越类:包
如果语言不支持包,可以自己预定实现,几个关键点:
- 用于区分“共用的类”和“某个包私用的类”的命名规则
- 为了区分每个类所属的包而定制的命名规则和/或代码组织规则(即项目结构)
- 固定什么包可以用其它什么包的规则,包框是否可以用继承和/或包含等
第7章 高质量的子程序
7.1 创建子程序的正当理由
- 降低复杂度
- 引入中间的、易懂的抽象
- 避免代码重复
- 支持子类化
- 隐藏顺序
- 隐藏指针操作
- 提高可移植性
- 简化复杂的逻辑判断
- 改善性能(只需要在一个地方优化)
即使很简单的逻辑,如果能提高可读性,也要考虑写出子程序。
7.2 在子程序层上设计
内聚性
- 功能的内聚性:一个程序仅执行一项操作
不太理想的内聚性
- 顺序上的内聚性:子程序内包含有需要按特定顺序执行的操作,这些步骤需要共享数据,而且只有在全部执行完毕后才完成了一项完整的功能。
- 通信上的内聚性:一个子程序中的不同操作使用了同样的数据,但不存在其它任何联系。
- 临时的内聚性:含有一些因为需要同时执行才放到一起的操作的子程序。(可做作为组织者)
不可取的内聚性:
- 过程上的内聚性:一个子程序中的操作是按特定的顺序进行的。
- 逻辑上的内聚性:若干操作被放入同一个子程序中,通过传入的控制标志选择执行其中的一项操作。
- 巧合的内聚性:子程序中的各个操作之间没有任何可以看到的关联。
7.3 好的子程序的名字
- 描述子程序所做的所有事情
- 避免使用无意义的、模糊或表述不清的动词
- 不要仅通过数字来形成不同的子程序名字
- 根据需要确定子程序的长度
- 给函数命名时要对返回值有所描述
- 给过程起名时使用语气强烈的动词加宾语的形式
- 准确使用对仗词
- 为常用操作确立命名规则
7.4 子程序可以写多长
总的来说,没有必要刻意去限制子程序的长度,而应该去考虑的是内聚性等复杂度事项,不过一般这样考虑了,子程序的长度不太可能超过200行。
7.5 如何使用子程序参数
减少接口错误的指导原则:
- 按照输入-修改-输出的顺序排列参数
- 考虑自己创建in和out关键字
- 如果几个子程序都用了类似的一些参数,应该让这些参数的排列顺序保持一致
- 使用所有的参数
- 把状态或出错变量放在最后
- 不要把子程序的参数用作工作变量
- 在接口中对参数的假定加以说明(注释、断言)
- 把子程序的个数限制在大约7个以内
- 考虑对参数采用某种表示输入、修改、输出的命名规则
- 为子程序传递用以维持其接口抽象的变量或对象
- 使用具名参数
- 确保实际参数与形式参数相匹配(类型要匹配)
7.6 使用函数时要特别考虑的问题
如果一个子程序的主要用途就是返回由其名字所指明的返回值,那么就应该使用函数,否则应该使用过程。对于返回值:
- 检查所有可能的返回路径
- 不要返回指向局部数据的引用或指针
7.7 宏子程序和内联子程序
- 把表达式整个包含在括号内
- 把含有多条语句的宏用大括号括起来
- 用给子程序命名的方法来给展开后代码形同子程序的宏命名,以便在需要时可以用子程序来替换宏
可以用来替代宏的方案:
- const
- inline(也需要节制使用)
- enum
- typedef
第8章 防御式编程
防御式编程概念来自于防御式驾驶:子程序应该不因传入错误数据而被破坏,哪怕是由其它子程序产生的错误数据。
8.1 保护程序免遭非法输入数据的破坏
- 检查所有来源于外部的数据的值
- 检查子程序所有输入参数的值
- 决定如何处理错误的输入数据
8.2 断言
- 用错误处理来处理预期会发生的状况,用断言来处理绝不应该发生的状况
- 避免把需要执行的代码放到断言中
- 用断言来注解并验证前条件和后条件
- 对于高健壮性的代码,应该先使用断言再处理错误
8.3 错误处理技术
- 返回中立值
- 换用下一个正确的数据
- 返回与前次相同的数据
- 换用最接近的合法值
- 把警告信息记录到日志文件中
- 返回一个错误码
- 调用错误处理子程序或对象
- 当错误发生时显示出错消息
- 用最妥当的方式在局部处理错误
- 关闭程序
注意正确性与健壮性的选择
- 人身安全攸关的软件倾向于正确性
- 消费类应用软件倾向于健壮性
高层次设计对错误处理方式的影响:应该在整个程序中采用一致的方式处理非法的参数
8.4 异常
- 用异常通知程序的其他部分,发生了不可忽略的错误
- 只有真正例外的情况下才抛出异常
- 不能用异常来推卸责任
- 避免在构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获
- 在恰当的抽象层次抛出异常:抛出的异常也是程序接口的一部分
- 在异常消息中加入关于导致异常发生的全部信息
- 避免使用空的catch语句
- 了解所用函数库可能抛出的异常
- 考虑创建一个集中的异常报告机制
- 把项目中对异常的使用标准化
- 考虑异常的替换方案
8.5 隔离程序,使之包容由错误造成的损害
把某些接口选定为“安全”区域的边界。对穿越安全区域边界的数据进行合法性校验,并当数据非法时做出敏锐的反应。
- 隔栏外部的程序使用错误处理技术
- 隔栏内部的程序使用断言技术
8.6 辅助调试的代码
不要自动地把产品版地限制强加于开发版之上
尽早引入辅助调试的代码
采用进攻式编程:在开发时惨痛的失败
计划移除辅助调试的代码
- 使用类似ant和make这样的版本控制工具和make工具
- 使用内置的预处理器
- 编写自己的预处理器
- 使用调试存根(代码桩)
8.7 确定在产品代码中该保留多少防御式编程
- 保留那些检查重要错误的代码
- 去掉检查细微错误的代码(开关/记录)
- 去掉可以导致程序硬件崩溃的代码
- 保留可以让程序稳妥地崩溃地代码
- 让你的技术支持人员记录错误信息
- 确认留在代码中的错误信息是友好的
8.8 对防御式编程采取防御的姿态
防御式编程同样会增加软件的复杂度
第9章 伪代码编程过程
9.1 创建类和子程序的步骤概述
创建一个类的步骤:
- 创建类的总体设计
- 创建类中的子程序
- 复审并测试整个类
创建子程序的步骤
- 设计子程序
- 检查设计
- 编写子程序的代码
- 复审并测试代码
9.2 伪代码
指导原则:
- 用类似英语的语句来精确描述特定的操作
- 避免使用目标编程语言中的语法元素
- 在本意的层次上编写伪代码
- 在一个足够低的层次上编写伪代码,以便可以近乎自动地从它生成代码
优点:
- 伪代码使得评审更容易
- 伪代码支持反复迭代精化地思想
- 伪代码使变更更加容易
- 伪代码能使给代码做注释地工作量减到最少
- 伪代码比其他形式的设计文档更容易维护
9.3 通过伪代码编程过程创建子程序
设计子程序
- 检查先决条件
- 定义子程序要解决的问题
- 为子程序命名
- 决定如何测试子程序
- 在标准库中搜寻可用的功能
- 考虑错误处理
- 考虑效率问题
- 研究算法和数据结构
- 编写伪代码
- 考虑数据
- 检查伪代码
- 在伪代码中试验一些想法,留下最好的想法(迭代)
编写子程序的代码
- 写出子程序的声明
- 把伪代码转变为高层的注释
- 在每条注释下面填充代码
- 检查代码是否需要进一步分解
检查代码
- 在脑海中检查程序中的错误
- 编译子程序
- 在调试器中逐行执行代码
- 测试代码
- 消除程序中的错误
收尾工作
- 检查子程序的接口
- 检查整体的设计质量
- 检查子程序的变量
- 检查子程序的语句和逻辑
- 检查子程序的布局
- 检查子程序的文档
- 除去冗余的注释
根据需要重复上述步骤
9.4 伪代码编程过程的替代方案
- 测试先行开发
- 重构
- 契约式设计
- 东拼西凑
第3部分 变量
第10章 使用变量的一般事项
10.1 数据认知
10.2 轻松掌握变量定义
- 关闭隐式声明
- 声明全部的变量
- 遵循某种命名规则
- 检查变量名
10.3 变量初始化原则
- 在声明变量的时候初始化
- 在靠近变量第一次使用的位置初始化它
- 理想情况下,在靠近第一次使用变量的位置声明和定义该变量
- 在可能的情况下使用final或者const
- 特别注意计数器和累加器
- 在类的构造函数里初始化该类的数据成员
- 检查是否需要重新初始化
- 一次性初始化具名常量,用可执行代码来初始化变量
- 使用编译器来设置自动初始化所有变量
- 利用编译器的警告信息
- 检查输入参数的合法性
- 使用内存访问检查工具来检查错误的指针
- 在程序开始时初始化工作内存
10.4 作用域
作用域指的是变量在程序内的可见和可引用的范围
使变量引用局部化
尽可能缩短变量的存活时间
减少作用域的一般原则
- 在循环开始之前再去初始化该循环里使用的变量,而不是在该循环所属的子程序的开始处初始化这些变量
- 直到变量即将被使用时再为其赋值
- 把相关语句放在一起
- 把相关语句组提取成单独的子程序
- 开始时采用最严格的可见性,然后根据需要扩展变量的作用域
有关缩小变量作用域的说明
“方便性”与“智力上的可管理性”的权衡
10.5 持续性
持续性的多种形态
- 特定代码段或子程序的生命期
- 只要你允许,他就会持续下去
- 程序的生命期
- 永远持续
10.6 绑定时间
把变量和它的值绑定在一起的时间
- 编码时
- 编译时
- 加载时
- 对象实例化时
- 即时
10.7 数据类型和控制结构之间的关系
- 序列型数据翻译为程序中的顺序数据
- 选择型数据翻译为程序中的if和case语句
- 迭代型数据翻译成程序中的for、repeat、while等循环结构
10.8 为变量指定单一用途
- 每个变量只用于单一用途
- 避免让代码具有隐含含义
- 确保使用了所有已声明的变量
第11章 变量名的力量
11.1 选择好的变量名的注意事项
最重要的命名注意事项
要完全、准确地描述出该变量所代表的事物。
以问题为导向
一个好记的名字反映的通常都是问题,而不是解决方案。
最适当的名字长度
1016个字符,820个字符,注意不要教条
作用域对变量名的影响
- 作用域越大,倾向于用越长的变量名
- 对于全局命名空间中的名字加以限定词
变量名中的计算值限定词
Total、Sum等限定词放到名字的最后
变量名中的常用对仗词
建议成对使用
11.2 为特定类型的数据命名
为循环下标命名
i, j, k
为状态变量命名
不要用一般性的flag,不要猜测变量名的含义
为临时变量命名
警惕临时变量,使用比temp更有含义的命名
为布尔变量命名
- 谨记典型的布尔变量命:done, error,found,success或ok
- 给布尔变量赋予隐含真/假含义的名字
- 使用肯定的布尔变量名
为枚举类型命名
- 如语言不支持类似类的处理,自己加上前缀
为常量命名
使用常量的含义而不是数值命名
11.3 命名规则的力量
为什么要有规则
- 要求你更多地按规矩行事
- 有助于在项目之间传递知识
- 有助于你在新项目中更快速的学习代码
- 有助于减少名字增生
- 弥补编程语言的不足之处
- 强调变量之间的关系
何时采用命名规则
合作/正式/长期的项目
正式程度
取决于项目规模/生命周期
11.4 非正式命名规则
与语言无关的命名规则指导原则
- 区分变量名和子程序名字
- 区分类和对象
- 标识全局变量
- 标识成员变量
- 标识类型声明
- 标识具名常量
- 标识枚举类型的元素
- 在不能保证输入参数只读的语言标识只读参数
- 格式化命名以提高可读性
11.5 标准前缀
- 用户自定义类型缩写
- 语义前缀
11.6 创建具备可读性的短名字
- 不要用从每个单词删除一个字符的方式来缩写
- 缩写要一致
- 创建你能读出来的名字
- 避免使用容易看错或者读错的字符组合
- 使用辞典来解决命名冲突
- 在代码里用缩写对照表来解释极短的名字的含义
- 在一份项目级的标准缩写文档中说明所有的缩写
- 名字对于代码读者的意义比作者更重要
11. 7 应当避免的名字
避免使用令人误解的名字或缩写
避免使用具有相似含义的名字
避免使用具有不同含义但却有相似名字的变量
避免使用发音相近的名字
避免在名字中使用数字
避免在名字中拼错单词
避免似乎用英语中常常拼错的单词
不要仅靠大小写来区分变量命
避免使用多种自然语言
避免使用标准类型、变量和子程序的名字
不要使用与变量含义完全无关的名字
避免在名字中包含易混淆的字符
第12章 基本数据类型
12.1 数值概论
- 避免使用硬编码的“神秘数字”
- 如果需要,可以使用硬编码的0和1
- 预防除零错误
- 使类型转换变得明显
- 避免混合类型的比较
- 注意编译器的警告
12.2 整数
- 检查整数除法
- 检查整数溢出
- 检查中间结果溢出
12.3 浮点数
- 避免数量级相差巨大的数之间的加减运算
- 避免等量判断
- 处理舍入误差问题
- 检查语言和函数库对特定数据类型的支持
12.4 字符和字符串
- 避免使用神秘字符和神秘字符串
- 避免off-by-one错误
- 了解你的语言和开发环境是如何支持Unicode的
- 在程序生命期中尽早决定国际化/本地化策略
- 如果你知道只需要支持一种文字的语言,请考虑使用ISO 8859字符集
- 如果你需要支持多种语言,请使用Unicode
- 采用某种一致的字符串类型转换策略
C语言中的字符串
- 注意字符串指针和字符数组之间的差异
- 把C-style字符串的长度声明为CONSTANT + 1
- 用null初始化字符串以避免无结束符的字符串
- 用字符数组取代C中的指针
- 用字符串子程序的安全版本,
strlcpy()或strcpy_s()
12.5 布尔变量
- 用布尔变量对程序加以文档说明
- 用布尔变量来简化复杂的判断
- 如果需要的话,创建自己的布尔类型
12.6 枚举类型
- 用枚举类型来提高可读性
- 用枚举类型来提高可靠性
- 用枚举类型来简化修改
- 用枚举类型作为布尔变量的替换方案
- 检查非法值
- 定义处枚举的第一项和最后一项,以便于循环边界
- 把枚举类型的第一个元素留做非法值
- 明确定义项目代码编写标准中第一个和最后一个元素的使用规则,并且在使用时保持一致
- 警惕给枚举元素明确赋值而带来的失误
如果你的语言里没有枚举类型
可以自己设法用全局变量的规范或者类来模拟。
12.7 具名常量
- 在数据声明中使用具名常量
- 避免使用文字量,即使是安全的
- 用具有适当作用域的变量或类来模拟具名常量
- 统一地使用具名常量
12.8 数组
- 确认所有的数组下标都没有超出数组地边界
- 考虑用容器来取代数组,或者将数组作为顺序化结构来处理
- 检查数组地边界点
- 如果数组是多维的,确认下标的使用顺序是正确的
- 提防下标串话
- 在C中结合
ARRAY_LENGTH()宏来使用数组
12.9 创建你自己的类型(类型别名)
- 易于修改
- 避免过多的信息分发
- 增加可靠性
- 弥补语言的不足
指导原则:
- 给所创建的类型取功能导向的名字
- 避免使用预定义类型
- 不要重定义一个预定义的类型
- 定义替代类型以便于移植
- 考虑创建一个类而不是typedef
第13章 不常见的数据类型
13.1 结构体
- 用结构体来明确数据关系
- 用结构体简化对数据块的操作
- 用结构体来简化参数列表
- 用结构体来减少维护
13.2 指针
理解指针的关键:
- 内存中的位置
- 如何解释指针所指的内容
使用指针的一般技巧:
- 把指针操作限制在子程序或者类里面
- 同时声明和定义指针
- 在指针分配相同的作用域中删除指针
- 在使用指针之前检查指针
- 先检查指针所引用的变量再使用它
- 用狗牌字段来检查损毁的内存
- 增加明显的冗余
- 用额外的指针变量来提高代码清晰度
- 简化复杂的指针表达式
- 按照正确的顺序删除链表中的指针
- 分配一片保留的内存后备区域
- 粉碎垃圾数据
- 再删除或者释放指针后把它们设为空值
- 在删除变量之前检查非法指针
- 跟踪指针分配情况
- 编写覆盖子程序,集中实现避免指针问题的策略
- 采用非指针的技术
C++指针
- 理解指针和引用之间的区别
- 把指针用于“按引用传递”参数,把const引用用于“按值传递”参数
- 使用
shared_ptr - 灵活运用智能指针
C指针
- 使用显式指针类型而不是默认类型
- 避免强制类型转换
- 遵循参数传递的星号规则
- 在内存分配中使用
sizeof()确定变量的大小
13.3 全局数据
常见问题:
- 无意间修改了全局数据
- 与全局数据有关的别名问题
- 与全局数据有关的代码重入问题
- 全局数据阻碍代码重用
- 与全局数据有关的非确定的初始化顺序事宜
- 全局数据破坏了模块化和质量上的可管理性
使用全局数据的理由:
- 保存全局数据
- 模拟具名常量
- 模拟枚举类型
- 简化对及其常用的数据使用
- 消除流浪数据
只有万不得已时才使用全局数据
- 首先把每一个变量设置为局部的,仅当需要时才把变量设置为全局的
- 区分全局变量和类变量
- 使用访问器子程序
- 要求所有的代码通过访问器子程序来存取数据
- 不要把你所有的全局数据扔在一处
- 用锁定来控制对全局变量的访问
- 在你的访问器子程序里构建一个抽象层
- 使得对一项数据的所有访问都发生在同一个抽象层
如何降低使用全局数据的风险:
- 创建一种命名规则来突出全局变量
- 为全部的全局变量创建一份注释良好的清单
- 不要用全局变量来存放中间结果
- 不要把所有的数据都放在一个大对象中并到处传递,以说明你没有使用全局变量
第4部分 语句
第14章 组织直线型代码
14.1 必须有明确顺序的语句
- 设法组织代码,使依赖关系变得非常明显
- 使子程序名能凸显依赖关系
- 利用子程序参数明确显示依赖关系
- 用注释对不清晰的依赖关系进行说明
- 用断言或者错误处理代码来检查依赖关系
14.2 顺序无关的语句
- 使代码易于自上而下地阅读
- 把相关的语句组织在一起
第15章 使用条件语句
15.1 if 语句
简单 if-then语句
- 首先写正常代码路径,再处理不常见情况
- 确保对于等量的分支是正确的
- 把正常情况的处理放在if后面而不要放在else后面
- 让if子句后面跟随一个有意义的句子
- 考虑else子句
if-then-else语句串
- 利用布尔函数简化复杂的检测
- 把最常用的情况放在最前面
- 确保所有的情况都考虑到了
- 如果你的语言支持,请把if-then-else语句串替换成其它结构
15.2 case语句
为case语句选择最有效的排列顺序:
- 按字母顺序或按数字顺序排列各种情况
- 把正常的情况放在前面
- 按执行频率排列case子句
使用case语句的诀窍:
- 简化每种情况对应的操作:可以使用子程序
- 不要为了使用case语句而刻意制造一个变量
- 把default子句只用于检查真正的默认情况
- 利用default子句来检测错误
- 在C++和Java里,避免代码执行越过一条case子句的末尾(显式的break)
- 在C++里,在case末尾明确无误地标明需要穿越执行的程序流程
第16章 控制循环
16.1 选择循环的种类
- 计数循环
- 连续求值的循环
- 无限循环
- 迭代器循环
什么时候使用while循环
- 检测位于循环的开始
- 检测位于循环的结尾
什么时候用带退出的循环?
正常的带退出的循环:如果把循环条件检测放在循环开始或结束处,那就需要写一个半循环的代码(在循环开始前或结束后需要循环体的重复代码)
何时使用for循环:执行次数固定的循环
合适使用foreach循环:数组或容器的迭代操作
16.2 循环控制
应该把循环看做是一个黑盒子
进入循环
- 只从一个位置进入循环
- 把初始化代码紧放在循环前面
- 用
while(true)表示无限循环 - 在适当情况下多使用for循环
- 在while循环更适用的时候,不要使用for循环
处理好循环体
- 用 {} 把循环体中的语句括起来
- 避免空循环
- 把循环内务操作要么凡在循环的开始,要么放在循环的末尾
- 一个循环只做一件事
退出循环
- 设法确认循环能够终止
- 使循环终止条件看起来很明显
- 不要为了终止循环而胡乱改动for循环的下标
- 避免出现依赖于循环下标最终取值的代码
- 考虑使用安全计数器
提前退出循环
- 考虑在while循环中使用break语句而不用布尔标记
- 小心那些有很多break散布其中的循环
- 在循环开始处用continue进行判断
- 如果语言支持,请使用带标号break结构
- 使用break和continue时要小心谨慎
检查端点
- 开始的情况
- 任意选择的中间情况
- 最终的情况
使用循环变量
- 用整数或者枚举类型表示数组和循环的边界
- 在嵌套循环终宏使用有意义的变量命来提高其可读性
- 用有意义的名字来避免循环下标串话
- 把循环下标变量的作用域限制在本循环内
循环应该有多长
- 循环要尽可能地短,以便能够一目了然
- 把嵌套限制在3层以内
- 把长循环的内容移到子程序里
- 要让长循环格外清晰
16.3 轻松创建循环——由内而外
从一种情况开始,用字面量来编写代码,然后缩进它,在外面加一个循环,然后用循环下标或计算表达式替换那些字面量。如果需要,在它的外面再套上一个循环,然后再替换掉一些字面量。
16.4 循环和数组的关系
有些语言是支持直接的数组运算的,从而可以减少循环
第17章 不常见的控制结构
17.1 子程序中的多处返回
- 如果能增强可读性,那么就使用return
- 用防卫子句(早返回或早退出)来简化复杂的错误处理
- 减少每个子程序中return的数量
17.2 递归
- 确认递归能够停止
- 使用安全计数器防止出现无穷递归
- 把递归限制在一个子程序内
- 留心栈空间
- 不要用递归去计算阶乘或者斐波拉契数列
17.3 goto
- 在那些不直接支持结构化控制语句的语言里,用goto模拟那些控制结构
- 如果语言内置了等价的控制结构,那么就不要用goto
- 如果是为了提高代码效率而是用goto,请衡量此举实际带来的性能提升
- 除非你要模拟结构化语句,否则尽量在每个子程序只使用一个goto标号
- 除非你要模拟结构化语句,否则尽量让goto向前跳转而不要向后跳转
- 确认所有goto标号都被用到了
- 确认goto不会产生某些执行不到的代码
第18章 表驱动法
18.1 表驱动法使用总则
从表里面查找信息而不使用逻辑语句,使用表驱动法的两个问题:
- 怎样从表中查询条目:直接访问,索引访问,阶梯方位
- 应该在表里存些什么:数据,动作(代码/函数指针)
18.2 直接访问表
如何构造查询键值:
- 复制信息从而能够直接使用键值
- 转换键值以使其能够直接使用
- 把键值转换提取成独立的子程序
18.3 索引访问表
先用一个基本类型的数据从一张索引表中查出一个键值,然后用这一键值查出你感兴趣的主数据,优点:
- 节省内存空间
- 操作位于索引中的记录更廉价
- 可维护性
18.4 阶梯访问表
表中的记录对于不同的数据范围有效,而不是对于不同的数据点有效。把每一个区间的上限写入一个表里,然后循环按照各区间的上限来检查分数。
- 留心端点
- 考虑用二分查找取代顺序查找
- 考虑用索引访问来取代阶梯技术
- 把阶梯表查询操作提取成单独的子程序
第19章 一般控制问题
19.1 布尔表达式
- 用true和false做布尔判断,隐式地比较布尔值与true和false
- 简化复杂地表达式
- 拆分复杂的判断并引入新的布尔变量
- 把复杂的表达式做成布尔函数
- 用决策表代替复杂的条件
- 编写肯定形式的布尔表达式
- 用括号使布尔表达式更清晰
- 理解布尔表达式时如何求值的,注意短路
- 按照数轴的顺序编写数值表达式
- 与0比较的指导原则
- 隐式地比较逻辑变量
- 把数与0相比较
- 在C中显式地比较字符和零终止符
(\0) - 把指针与NULL相比较
- 布尔表达式地常见问题
- 在C家族语言中,应该把常量放在比较地左端
- 在C++中,刻意考虑创建预处理宏来替换&&, ||和==
- 在Java中,应理解a==b和a.equals(b)之间的差异
19.2 复合语句(语句块)
- 把括号一起写出
- 用括号来把条件表达清楚
19.3 空语句
- 小心使用空语句
- 为空语句创建一个DoNothing()预处理宏或者内联函数
- 考虑如果换用一个非空的循环,是否会让代码更清晰
19.4 驯服危险的深层嵌套
- 通过重复检测条件中的某一部分来简化嵌套的if语句
- 用break块来简化嵌套if
- 把嵌套if转换成一组if-then-else语句
- 把嵌套if转换成case语句
- 把深层嵌套的代码抽取出来放进单独的子程序
- 使用一种更加面向对象的方法
- 重新设计深层嵌套的代码
19.5 编程基础:结构化编程
一个应用程序应该只采用一些单入单出的控制结构
- 顺序
- 选择
- 迭代
19.6 控制结构与复杂度
降低程序的复杂度,以及为了理解它所需要的专心程度。
第5部分 代码改善
第20章 软件质量概述
20.1 软件质量的特性
- 外在的质量特性
- 正确性
- 可用性
- 效率
- 可靠性
- 完整性
- 适应性
- 精确性
- 健壮性
- 内在的质量特性
- 可维护性
- 灵活性
- 可移植性
- 可重用性
- 可读性
- 可测试性
- 可理解性
- 这些特性是相互制约的
20.2 改善软件质量的技术
- 软件质量目标
- 明确定义质量保证工作
- 测试策略
- 软件工程指南
- 非正式技术复查
- 正式技术复查
- 外部审查
开发过程
- 对变更控制的过程
- 结果的量化
- 制作原型
20.3 不同质量保证技术的相对效能
代码复查的成本效益要高于测试,推荐阵容:
- 对所有的需求、架构以及系统关键部分的设计进行正式检查
- 建模或者创建原型
- 代码阅读或者检查
- 执行测试
20.4 什么时候进行质量保证工作
在早期阶段就开始强调质量保证工作,并贯彻到项目的余下部分中。
20.5 软件质量的普遍原理
改善质量以降低开发成本:减少代码返工上的时间。
第21章 协同构建
21.1协同开发实践概要
- 协同构建是其它质量保证技术的补充
- 协同构建有利于传授公司文化以及编程专业知识
- 集体所有权适用于所有形式的协同构建
- 在构建前后都应保持协作
21.2 结对编程
成功运用结对编程的关键
- 用编码规范类支持结对编程
- 不要让结对编程变成旁观
- 不要强迫在简单的问题上使用结对编程
- 有规律地对人员和分配地工作任务进行轮换
- 鼓励双方跟上对方的步伐
- 确认两个人都能够看到显示器
- 不要强迫程序员与自己关系紧张的人组队
- 避免新手组合
- 指定一个组长
21.3 正式检查
你期待相差能够带来什么结果:捕捉到60%的缺陷,评估进度。
详查中的人员角色:主持人,作者,评论员、记录员、经理(最好不出席)
详查的一般步骤:计划,概述,准备,详查回忆,详查报告,返工,跟进,第三个小时的会议
对详查进行细调:切忌胡乱的引入变化
注意详查中的自尊心
21.4 其它类型的协同开发实践
- 走查:能找到20%到40%的缺陷,尽量使用更加正式的详查
- 代码阅读:90%的缺陷是在准备复查会议时发现的
- 公开演示:这只是管理层的复查
第22章 开发者测试
22.1 开发者测试在软件质量中的角色
- 一个成功的测试应该弄垮软件
- 测试永远不可能彻底证明程序中没有错误
- 测试本身并不能改善软件的质量
- 测试时要求你假设会在代码里面找到错误
在构建中测试,能更彻底的测试。
22.2 开发者测试的推荐方法
- 对每一项相关的需求进行测试
- 对每一个相关的设计关注点进行测试
- 用基础测试来扩充针对需求和设计的详细测试用例
- 使用一个检查表记录历史错误
可能的情况下,不妨测试先行。
开发者测试的局限性:
- 开发者测试倾向于干净测试
- 开发者测试对覆盖率有过于乐观的估计
- 开发者测试往往会忽略一些更复杂的测试覆盖率类型
22.3 测试技巧锦囊
- 进行完整的测试是不可能的,窍门在于选择那些最有可能找出错误的测试用例
- 结构化的基础测试:需要去测试程序中每一条语句至少一次,尽量覆盖所有的已定义—-已使用数据流形式
- 等价类划分:相同等价类只需要测试一个用例
- 猜测程序会在哪里出错:边界值分析/复合边界值
- 几类坏数据:数据太少/太多的数据/错误的数据/查高度错误的数据/为初始化的数据
- 几类好数据:正常的情况/最小的正常局面/最大的正常局面/与旧数据的兼容性
- 采用容易手工检查的测试用例
22.4 典型错误
绝大多数错误往往与少数几个具有严重缺陷的子程序有关。(二八定律)
- 大多数的错误的影响范围是相当有限的
- 许多错误发生在构建的范畴之外
- 大多数的构建期错误是编程人员的失误造成的
- 笔误是一个常见的问题根源
- 错误理解设计师是犯错的常见原因
- 大多数错误很容易修正
- 总结所在组织中对付错误的经验
小型项目中,构建中的缺陷占了所有错误的大多数,大型项目占比有所下降。
测试本身也可能有错误:
- 检查你的工作
- 开发软件的时候就要计划好测试用例
- 保留你的测试用例
- 将单元测试纳入测试框架
22.5 测试支持工具
- 为测试各个类构建脚手架
- Diff工具
- 测试数据生成器
- 覆盖率监视器
- 数据记录器/日志记录器
- 符号调试器
- 系统干扰器
- 错误数据库
22.6 改善测试过程
- 有计划的测试
- 重新测试(回归测试)
- 自动化测试
22.7 保留测试记录
第23章 调试
23.1 调试概述
错误中学习:
- 理解你正在编写的程序
- 明确你犯了那种类型的错误
- 从代码阅读者的角度分析代码质量
- 审视自己解决问题的方法
- 审视自己修正缺陷的方法
错误的调试方法:
- 凭猜测找出缺陷
- 不要把时间浪费在理解问题上
- 用最唾手可得的方式修正错误
- 迷信式调试
23.2 寻找缺陷
科学的调试方法
- 将错误状态稳定下来
- 确定错误的来源
- 修补缺陷
- 对所修补的地方进行测试
- 查找是否还有类似的错误
寻找缺陷的一些小建议:
- 在构造假设时考虑所有的可用数据
- 提炼产生错误的测试用例
- 在自己的单元cesium族中测试代码
- 利用可用的工具
- 采用多种不同的方法重现错误
- 用更多的数据生成更多的假设
- 利用否定性测试用例的结果
- 对可能测假设尝试头脑风暴
- 在桌上放一个记事本,把需要尝试的事情逐条列出
- 缩小嫌疑代码的范围
- 对之前出现共的缺陷的类和子程序保持警惕
- 检查最近修改过的代码
- 扩展嫌疑代码的范围
- 增量式集成
- 检查常见的缺陷
- 同其他人讨论问题
- 抛开问题,休息一下
蛮力调试有可能说有效的方法,要为“快速肮脏调试法”的时候设置用给时间上限,做出一张蛮力调试方法列表
语法错误:
- 不要过分信任编译器信息中的行号
- 不要迷信编译器信息不要相信编译器的第二条信息
- 分而治之
- 找出没有配对的注释或者引号
23.3 修正缺陷
- 在动手之前先要理解问题
- 理解程序本身,而不仅仅是问题
- 验证对错误的分析
- 放松一下
- 保存最初的源代码
- 治本,而不是治标
- 修改代码时一定要有恰当的理由
- 一次只做一个改动
- 检查自己的改动
- 增加能暴露问题的单元测试
- 搜索类似的缺陷
23.4 调试中的心理因素
人总期望一个新的现象类似于它们见过的某种现象
- 证明了养成良好的编程习惯的重要性
- 程序员会对某些部分的代码视而不见,有时候能提高效率,有时候会陷入盲区
23.5 调试工具
- 源代码比较工具
- 编译器的警告信息
- 增强的语法检查和逻辑检查
- 执行性能剖析器
- 测试框架/脚手架
- 调试器
第24章 重构
24.1 软件演化的类型
演化应当提升程序的内在质量
24.2 重构简介
重构的理由:
- 代码重复
- 冗长的子程序
- 循环过长或嵌套过深
- 内聚性太差的类
- 类的接口未能提供层次一致的抽象
- 拥有太多参数的参数列表
- 类的内部修改往往被局限于某个部分
- 变化导致对多个类的相同修改
- 对继承体系的同样修改
- case语句需要做相同的修改
- 同时使用的相关数据并未以类的方式进行组织
- 成员函数使用其他类的特征比使用自身类的特征还要多
- 过多使用基本数据类型
- 某个类无所事事
- 一系列传递流浪数据的子程序
- 中间人对象无事可做
- 某个类同其他类关系过于亲密
- 子程序命名不恰当
- 数据成员被设置为公用
- 某个派生类仅使用了基类的很少一部分成员函数
- 注释被用于解释难懂的代码
- 使用了全局变量
- 在子程序调用请前使用了设置代码,或在调用后使用了收尾代码
- 程序中的一些代码似乎是在将来的某个时候才会用到的
24.3 特定的重构
数据级的重构
- 用具名常量替代神秘数字
- 使变量的名字更为清晰且传递更多信息
- 将表达式内联化
- 用函数来代替表达式
- 引入中间变量
- 用多个单一用途变量代替某个多用途变量
- 在局部用途中使用局部变量而不是参数
- 将基础数据类型转化为类
- 将一组类型码转化为类或枚举类型
- 将一组类型码转化为一个基类及其相应派生类
- 将数组转换为对象
- 把群集封装起来
- 用数据类来代替传统记录
语句级的重构
- 分解布尔表达式
- 将复杂布尔表达式转换成命名准确的布尔函数
- 合并条件语句中不同部分的重复代码片段
- 使用break或return而不是循环控制变量
- 在嵌套的if-then-else语句中一旦知道答案就立即返回,而不是去赋一个返回值
- 用多态来替代条件语句(尤其是重复的case语句)
- 创建和使用null对象而不是去检测空值
子程序级重构
- 提取子程序或者方法
- 将子程序的代码内联化
- 将冗长的子程序转换为类
- 用简单算法替代复杂算法
- 增加参数
- 删除参数
- 将查询从修改操作中独立出来
- 合并相似的子程序,通过参数区分它们的功能
- 将行为取决于参数的子程序拆分开来
- 传递整个对象而非特定成员
- 传递特定成员而非整个对象
- 包装向下转型的操作
类实现的重构
- 将值对象转化为引用对象
- 将引用对象转化为值对象
- 用数据初始化替代虚函数
- 改变成员函数或成员数据的位置
- 将特殊代码提取为派生类
- 将相似代码结合起来放置到基类中
类接口的重构
- 将成员函数放到另一个类中
- 将一个类变成两个
- 删除类
- 去除委托关系
- 去掉中间人
- 用委托代替继承
- 用继承代替委托
- 引入外部的成员函数
- 引入扩展类
- 对暴露在外的成员变量进行封装
- 对于不能修改的类成员,删除相关的
set()成员函数 - 隐藏那些不会在类之外被用到的成员函数
- 封装不使用的成员函数
- 合并那些实现非常相似的基类和派生类
系统级重构:
- 为无法控制的数据创建明确的索引源
- 将单向的类联系改为双向的类联系
- 将双向的类联系改为单向的类联系
- 用Factory Method模式而不是简单地构造函数
- 用异常取代错误处理代码,或者做相反方向的变换
24.4 安全的重构
- 保存初始代码
- 重构的步伐请小些
- 同一时间只做一项重构
- 把要做的事情一条条列出来
- 设置一个停车场
- 多使用检查点
- 利用编译器警告信息
- 重新测试
- 增加测试用例
- 检查对代码的修改
- 根据重构风险级别来调整重构方法
不宜重构的情况
- 不要把重构当做先写后改的代名词
- 避免用重构代替重写
24.5 重构策略
- 在增加子程序时进行重构
- 在添加类的时候进行重构
- 在修补缺陷的时候进行重构
- 关于易于出错的模块
- 关注高度复杂的模块
- 在维护环境下,改善你手中正在处理的代码
- 定义清楚感干净代码和拙劣代码之间的边界,然后尝试把代码移过这条边界
第25章 代码调整策略
25.1 性能概述
- 相对与代码质量,用户更关心的是程序的外在特性
- 从以下角度来思考效率问题
- 程序需求
- 程序的设计
- 类和子程序的设计
- 程序同操作系统的交互
- 代码编译
- 硬件
- 代码调整
25.2 代码调整简介
- 应当衡量代码的各个部分,找出最需要关注的地方,然后集中火力来对付占用了绝大部分资源的少量代码
- 一些无稽之谈
- 在高级语言中,减少代码的行数就可以提升所生成的 机器代码的运行速度
- 特定运算可能比其他的快,代码规模也较小
- 应当随时随地进行优化
- 程序的运行速度同其正确性同等重要
- 平铺直叙的代码有利于编译器的优化
25.3 常见的低效率之源
- 输入/输出操作
- 分页
- 系统调用
- 解释型语言
- 错误
25.4 性能测量
如果没有策略性能变化,想当然的优化结果往往只是使得代码更加晦涩难懂。
25.5 反复调整
结合多种方法,不断突破瓶颈。
第26章 代码调整技术
以牺牲程序内部结构的某些特性来换取更高的性能
26.1 逻辑
- 在知道答案之后停止判断
- 按照出现的频率来调整判断顺序
- 相似逻辑结构之间的性能比较
- 用查询表替代复杂表达式
- 使用惰性求值
26.2 循环
- 将判断外提
- 合并
- 展开
- 尽可能减少在循环内部做的工作
- 哨兵值
- 把最忙的循环放在最内层
- 削减强度
26.3 数据变换
- 使用整型数而不是浮点数
- 数组维度尽可能少
- 尽可能减少数组引用
- 使用辅助索引
- 使用缓存机制
26.4 表达式
- 利用代数恒等式
- 削弱运算强度
- 编译期初始化
- 小心系统函数
- 使用正确的常量类型
- 预先算出结果
- 删除公共子表达式
26.5 子程序
- 将子程序重写为内联
26.6 用低级语言重写代码
- 用高级语言编写整个应用程序
- 对程序进行完整的测试,验证其正确性
- 如果测试后发现需要改进程序性能,就对程序进行分析,确定出热点。
- 把几小段代码在低级语言中重写,以此提高这个题性能
26.7 变得越多,事情反而越没变
- 对每一次的改进进行量化
第6部分 系统考虑
第27章 程序规模对构建的影响
27.1 交流的规模
交流路径的数量与项目成员数量的平方大致成正比
27.2 项目规模的范围
27.3 项目规模对错误的影响
随着项目规模的增大,通常更大一部分错误要归咎于需求和设计。
27.4 项目规模对生产率的影响
小项目的生产率回避大项目高出2~3倍。
27.5 项目规模对开发活动的影响
- 随着项目的增大,构建活动的主导地位下降
- 程序、产品、系统和系统产品的区别
- 项目越大,复杂度也越大,也就要求有意识地去关注方法论
第28章 管理构建
28.1 鼓励良好地编码实践
设定标准的考虑事项
鼓励良好的编码实践的技术
- 给项目的每一部分分派两个人
- 进行复查代码
- 要求代码签名
- 安排一些好的代码示例供人参考
- 强调代码是公有财产
- 奖励好代码
- 一份简单的标准
28.2 配置管理
系统化地定义项目工件和处理变化,以使项目一直保持完整性。
一些控制设计变更的指导原则:
- 遵循某种系统变化的变更控制手续
- 成组地处理变更请求
- 评估每项变更的成本
- 提防大量的变更请求
- 成立变更控制委员会或者类似机构
- 警惕官僚主义,但也不要因为害怕官僚主义而排斥有效的变更控制
软件代码变更,使用版本控制软件。
工具版本也要纳入版本控制之中。
机器配置也尽量标准化。
做好备份。
28.3 评估构建进度表
- 建立目标
- 为评估留出时间,并且做出计划
- 清楚地说明软件需求
- 在底层细节层面进行评估
- 使用若干不同地评估方法,并且比较其结果
- 定期做重新评估
如果你落后了怎么办?
- 希望自己能赶上
- 扩充团队
- 缩减项目范围
28.4 度量
- 任何一种项目特性都是刻意用某种方法来度量地,而且总会比根本不度量好得多
- 留心度量地副作用
- 反对度量就是认为最好不要去了解项目中到底在发生什么
28.5 把程序员当人看
28.6 管理你的管理者
你要表现得使你的管理者认为他仍然在管理你。
第29章 集成
29.1 集成方法的重要性
29.2 集成频率——阶段式集成还是增量集成
阶段式集成
单元开发:设计、编码、测试、调试各个类
系统集成:将这些类组成一个庞大的系统
系统瓦解:测试并调试整个系统
增量集成
开发一个小的系统功能部件
设计、编码、测试、调试某个类
将这个新的类集成到系统骨架上
增量集成的益处:
- 易于定位错误
- 及早在项目里取得系统级的成果
- 改善对进度的监控
- 改善客户关系
- 更加充分地测试系统中地各个单元
- 能在更短地开发进度计划内建造出整个系统
29.3 增量集成的策略
- 自顶向下集成:首先加入顶部的类,最后加入底部的类
- 自底向上集成:先集成底部的类,后集成顶部的类
- 三明治集成
- 风险导向的集成:困难部件优先集成法
- 功能导向的集成
- T型集成
29.4 Daliy Build与冒烟测试
- 每日构建
- 检查失败的build
- 每天进行冒烟测试:能够暴露主要的问题
- 让冒烟测试与时俱进
- 将daliy build和冒烟测试自动化
- 成立build小组
- 仅当有意义时,才将修订(revsions)加入build中
- 但是别等太久才将修订加入进来
- 要求开发人员把他的代码添加到系统之前,进行冒烟测试
- 为即将build的代码准备一块暂存区
- 惩罚破坏build的人
- 在早上发布build
- 即使有压力,也要进行daily build和冒烟测试
第30章 编程工具
30.1 设计工具
计算机辅助设计工具(CASE)
30.2 源代码工具
编辑
- IDE
- 查找与替换
- diff工具
- merge工具
- 源代码美化器
- 生成接口文档的工具
- 模板
- 交叉引用工具
- 类的继承体系生成器
分析代码质量
- 吹毛求疵的语法/语义检查器
- 尺度报告器
重构源代码
- 重构器
- 结构改组工具
- 代码翻译器
版本控制
数据字典
30.3 可执行代码工具
产生目标码
- 编译器与链接器
- Build工具
- 程序库
- 代码生成向导
- 安装
- 预处理器
调试
测试
代码调整
- 执行剖析器
- 汇编代码清单和反汇编
30.4 工具导向的环境
UNIX工具箱
30.5 打造你自己的编程工具
- 项目特有的工具
- 脚本
30.6 工具幻境
第7部分 软件工艺
第31章 布局与风格
31.1 基本原则
好的布局凸现程序的逻辑结构
良好布局的目标:
- 准确表现代码的逻辑结构
- 始终如一地表现代码的逻辑结构
- 改善可读性
- 经得起修改
31.2 布局技术
- 空白
- 分组
- 空行
- 缩进
- 括号
31.3 布局风格
- 纯块结构
- 模仿纯块结构
- 使用begin-end对(花括号)指定块边界
- 行尾布局
31.4 控制结构的布局
格式化控制结构块的要点
- 不要用未缩进的begin-end对
- 别让begin和end两次缩进
其他考虑
- 段落之间要使用空行
- 单语句代码块的格式要前后统一
- 对于复杂的表达式,将条件分隔放在几行上
- 不用goto
- case语句不要有行尾布局的例外
31.5 单条语句的布局
- 语句长度需要有所限制
- 用空格使语句显得清楚
- 使用空格会让逻辑表达式更易读
- 使用空格会让数组引用更容易读
- 受用空格让子程序参数更容易读
- 格式化后续行
- 使续行明显
- 把紧密关联的元素放在一起
- 将子程序调用的后续行按标准量缩进
- 使后续行的结尾易于发现
- 将控制语句后续行缩进以标准的空格数
- 不要将赋值语句按等号对齐
- 对赋值语句后续行按标准量对齐
- 每行仅写一条语句
- 数据声明的布局
- 每行只声明一个数据
- 变量声明应尽量接近其首次使用的位置
- 合理组织声明顺序
- C++中,声明指针变量是请把星号靠近变量名,或者是去声明指针类型
31.6 注释的布局
- 注释的缩进要与相应代码一致
- 每行注释用至少一个空行分开
31.7 子程序的布局
- 用空行分隔子程序的各部分
- 将子程序参数按标准缩进
31.8 类的布局
- 类接口的布局
- 说明类及其完整用法的头部注释
- 构造函数和析构函数
- public子程序
- protected子程序
- private子程序和数据成员
- 类实现的布局
- 描述类所在文件之内容的头部注释
- 类数据
- public子程序
- protected子程序
- private子程序
- 如果文件包含多个类,要清楚地标出每一个类
- 文件和程序布局
- 一个文件应只有一个类
- 文件的命名应与类名相关
- 在文件中清晰地风格子程序
- 按字母顺序排列子程序
第32章 自说明代码
32.1 外部文档
- 单元开发文件夹
- 详细设计文档
32.2 编程风格作文档
在代码层文档中其主要作用地因素并非注释,而是好的编程风格
32.3 注释或不注释
32.4 高效注释之关键
注释种类
- 重复代码:不要写这种注释
- 解释代码:考虑是不是代码写得不清楚
- 代码标记:如TODO标记
- 概述代码
- 代码意图说明
- 传达代码无法表达的信息
高效注释
- 采用不会打断或抑制修改的注释风格
- 用伪代码编程法减少注释时间
- 将注释集成到你的开发风格中
- 性能不是逃避注释的好借口
最佳注释量:约每十条语句有一个注释?
32.5 注释技术
- 注释单行
- 不要随意添加无关注释
- 不要对单行代码做行尾注释
- 不要对多样代码做行尾注释
- 行尾注释用于数据声明
- 避免用行尾注释存放维护注记
- 用行尾注释编辑块尾
- 注释代码段
- 注释应表达代码的意图
- 代码本身应尽力做好说明
- 注释代码时应注重why而不是how
- 用注释未后面的内容做铺垫
- 让每个注释都有用
- 说明非常规做法
- 别用缩略语
- 将主次注释区分开
- 错误或语言环境独特点都要加注释
- 给出违背良好编程风格的理由
- 不要注释投机取巧的代码,应重写之
- 注释数据声明
- 注释数值单位
- 对数值的允许范围给出注释
- 注释编码含义
- 注释对输入数据的限制
- 注释“位标志”
- 将于变量相关的注释通过变量名关联起来
- 注释全局数据
- 注释控制解构
- 应在if,case、循环或者代码段前加上注释
- 应在每个控制结构后加上注释
- 将循环结束处的注释看成是代码太复杂的征兆
- 注释子程序
- 注释应靠近其说明的代码
- 在子程序上部用一两句话说明之
- 在声明参数处注释这些参数
- 利用诸如javadoc之类的代码说明工具
- 分清输入和输出参数
- 注释接口假设
- 对子程序的局限性作注释
- 说明子程序的全局效果
- 记录所用算法的来源
- 用注释标记程序的各部分
- 注释类、文件和程序
- 说明该类的设计方法
- 说明局限性、用法假设等
- 注释类接口
- 不要在类接口处说明实现细节
- 说明各文件的意图和内容
- 将文件命名为与其内容相关的名字
- 程序注释以书本为范例
32.6 IEEE标准
第33章 个人性格
33.1 个人是否和本书话题无关
33.2 聪明和谦虚
33.3 求知欲
- 在开发过程中建立自我意识
- 试验
- 阅读解决问题的有关方法
- 在行动之前作分析和计划
- 学习成功项目的开发经验
- 阅读文档
- 阅读其他书本期刊
- 同专业人士交往
- 向专业开发看齐
33.4 诚实
33.5 交流与合作
33.6 创造力和纪律
33.7 懒惰
33.8 不如你想象中那样起作用的性格因素
- 坚持
- 经验
- 编程狂人
