易飞滔Todd | 次生进化

《代码大全》笔记

⚠️ 文章已过期:本文最后更新于 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 简单易维护的。
  • 易维护:为代码维护者编程,自解释的设计。
  • 松耦合:不同部分之间的联系尽量少。
  • 可扩展性:更改时减少创伤。
  • 可复用性
  • 高扇入:充分复用底层模块
  • 低扇出:不过度依赖其他部分
  • 可移植性
  • 精简:没有多余部分
  • 分层化:隔离变化
  • 标准技术:不要用古怪的技术

设计的层次

  1. 软件系统
  2. 子系统或包:子系统间的通信必须加以限制,否则会不断熵增,一片混乱,可以先紧后松。常见的子系统:商业规则、用户接口、数据存取、系统依赖。
  3. 类:包括接口。大项目由子系统划分,定义一些外部函数;小项目由系统直接划分。
  4. 例程:可能会引起第3层的修改。
  5. 例程内设计

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语言):

  1. 每次使用ADT服务子程序时都明确指定实例(使用id)
  2. 明确地向ADT服务子程序提供所要用地数据
  3. 使用隐含实例(需要跟踪当前实例,容易出错)

类=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 寻找缺陷

科学的调试方法

  1. 将错误状态稳定下来
  2. 确定错误的来源
  3. 修补缺陷
  4. 对所修补的地方进行测试
  5. 查找是否还有类似的错误

寻找缺陷的一些小建议:

  • 在构造假设时考虑所有的可用数据
  • 提炼产生错误的测试用例
  • 在自己的单元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 不如你想象中那样起作用的性格因素

  • 坚持
  • 经验
  • 编程狂人

33.9 习惯

第34章 软件工艺的话题

34.1 征服复杂性

34.2 精选开发过程

34.3 首先为人写程序,其次才是为机器

34.4 Program into Your Language, Not in it

34.5 借助规范集中注意力

34.6 基于问题域编程

34.7 当心落石

34.8 迭代,反反复复,一次又一次

34.9 汝当分离软件于信仰

第35章 何处有更多信息