《Code Complete》(第二版)读书笔记


基本信息 BASICS

作者:Steve McConnell(金戈译)
出版社:电子工业出版社
出版时间:2006年


阅读心得

  • 这本书算是自己主动接触的一本书,一来是受之前看的《软技能》的作者的推荐,二来是在网上看到人们对它的评价很高,甚至被奉为经典,然而我居然没有读过,这引起了我强烈的好奇心。正好疫情期间,也有时间看一些所谓的闲书,于是认真阅读并做了阅读笔记。
  • 书的篇幅确实很长,900多页,看了容易忘,所以本次的阅读方法是按照目录依次对书中的大致内容做总结,希望之后回忆时能有一个大纲。


目录

第一部分:打好基础

第1章、欢迎进入软件创建世界

本章要点

  • 创建活动是总体设计和系统测试之间承上启下的工作。
  • 创建活动主要包括:详细设计、编码、调试和单元测试。
  • 关于创建活动的其它称谓有:实现、编程等。
  • 创建活动质量对软件质量有潜在影响。
  • 在最后的分析中,对创建活动理解的好坏,决定了一个程序员素质的高低。

1.1 什么是软件构建

创建活动主要指编码和调试过程, 但也包括详细设计和测试中的某些
工作。

  • 问题定义
  • 需求分析
  • 实现计划
  • 总体设计
  • 详细设计
  • 创建即实现
  • 系统集成
  • 单元测试
  • 系统测试
  • 校正性的维护
  • 功能强化

1.2 软件构建为何如此重要

1、创建活动是开发软件的重要组成部分。

2、创建活动在软件开发中处于枢纽地位;

3、把主要精力集中于创建活动,可以极大地提高程序员的生产效率。

4、创建活动的产品,源代码,往往是软件的唯一精确描述。

5、创建活动是唯一一项必不可少的工作。


第2章、利用隐喻对编程进行更深刻的理解

本章要点

  • 隐喻仅仅是启发,而不是公式;
  • 把软件创建与建造建筑物类比,表明开发软件前要精心准备,并表明了大规模项目与小规模项目之间的差别;
  • 认为软件开发实践是智能工具箱中的工具进一步表明,每个程序员都有许多自己的工具,没有任何一种工具是万能的。为每件工作选择合适的工具,是成为一个优秀程序员的首要素质之一;
  • 不同的隐喻彼此并不排斥,应当使用对你最有益处的某种隐喻组合。

2.1 隐喻的重要性

隐喻:即类比,建模

重大发现往往是从类比中产生的。通过把一个你所陌生的事物与你所熟知的事物比较;

对模型理解得好坏,便决定了你对软件开发理解的好坏。

2.2 如何使用软件隐喻

将问题概念化,用它来帮助你获得关于编程过程的内在理解,利用它们来帮助你考虑编程活动,想象解决问题的更好办法。

2.3 常见的软件隐喻

软件书写:写代码(Writing Code)---但是写代码不能全面彻底地描述软件开发过程

软件播种:生成系统(Growing a System)---把创建软件当作播种或培植庄稼,一次干一点儿

软件珍珠培植法:系统积累(System Accretion)---学会每次向你的系统增量开发

软件创建:建造软件(building software)---软件体系结构、搭结构架、构造、分割代码、插入子程序

实用软件技术:智能工具箱(The Intellectual Toolbox)---工具箱隐喻有助于我们保留一切方法、技巧、技术等,并在适当的时候使用它们


第3章、软件创建的先决条件

本章主要论述在软件创建之前所要做的准备工作

本章要点

  • 如果想开发一个高质量的软件,必须自始至终重视质量问题。在开始阶段强调质量往往比在最后强调质量更为有效。
  • 程序员的份内工作之一便是向老板和同事宣传软件的开发过程,包括在编程开始前从事先决条件准备工作的重要性。
  • 如果问题定义工作做得不好,那么在创建阶段,所解决的问题可能并不是用户真正要解决的问题。
  • 如果需求分析工作做得不好,很可能因此而漏掉要解决问题中的重要细节。在创建工作后更改需求,要比在需求分析阶段进行更改的成本高 20 到 100 倍。所以,在开始编程前一定要确认需求定义工作一切正常。
  • 在编程前规定好约定,在创建工作结束后再改变代码来满足约定几乎是不可能的。
  • 在创建活动开始之前如果无法完成准备工作,可以尝试在不太稳固的基础上进行创建活动。

3.1 先决条件的重要性

准备工作的中心目标就是降低风险

造成准备不足的原因:

  • 程序员抵制不了立刻开始进行编码工作的渴望
  • 管理人员往往不理解那些在创建先决条件上花费时间的程序员

在进行创建工作之前必须做准备工作的论据

  • 逻辑推理,认识到准备工作是非常重要的
  • 类比,软件软件系统与其他需要耗费人力与财力的工程是一样的
  • 数据,过去的研究表明,一次完成是最好的选择,不必要的修改是非常昂贵的。

3.2 问题定义的先决条件

弄清楚你想要解决的问题是什么。
问题定义应该从用户的观点出发,使用用户的语言进行定义。

问题定义错误的后果是你可能浪
费许多时间精力去解决了一个错误问题。

3.3 需求的先决条件

需求详细描述了一个软件系统需要解决的问题

  • 明确的需求可以保证是由用户而不是程序员决定系统的功能。
  • 明确的需求也可以避免引起争议。
  • 重视需求有助于减少开始编程开发之后的系统变更情况
  • 充分详尽地描述需求,是项目成功的关键,它甚至有可能比有效的构建技术而重要.

在创建阶段如何对付需求变化?

  • 用检查表来评估你的需求分析质量
  • 让每个人都知道由于变化需求所付出的代价
  • 建立一套更改控制过程
  • 用开发的方法来容纳变动
  • 放弃项目

检查表

figC1.PNG

3.4 结构设计的先决条件

软件结构设计是持详细设计的框架。

典型的结构要素:

  • 程序的组织形式(在程序中定义主要模块)
  • 变动策略(应该清晰地描述系统应付变动的策略)
  • 购买而不是建造的决定
  • 主要的数据结构(应该给出使用的主要文件、表和数据结构)
  • 关键算法(应该指出考虑过的算法方案,并指出选中最终方案的原因)
  • 主要对象(在面向对象的系统中应指出要实现的主要对象)
  • 用户界面(应该定义命令结构,输入格式和菜单)
  • 错误处理(阐明处理错误的策略)
  • 坚固性(裕度设计,断言,容错性,性能)
  • 通用的结构设计质量准则(对于系统中模块的讨论,每个模块中隐含的信息,选用和不
    选用某方案的原因)

检查表

figC2.PNG

3.7 花费在先决条件上的时间长度

一个运行良好的项目通常把 20~30%的时间用于先决条件。 这 20~30%的时间中不包括进行详细设计的时间,因为它是创建活动的一部分。

3.8 改变先决条件以适应你的项目

先决条件随项目规模和正式性不同而变化。


第4章、关键的“构建”决策

本章要点

  • 每种编程语言都有其优点和弱点.要知道你使用的语言的明确优点和弱点.
  • 在开始编程之前,做好一些约定(convention)。“改变代码使之符合这些约定”是近乎不可能的。
  • “构建的实践方法”的种类比任何单个项目能用到的要多。有意识地选择最适合你的项目的实践方法。
  • 问问你自己,你采用的编程实践是对你所用的编程语言的正确响应,还是受它的控制?请记得“深入一种语言去编程”,不要仅“在一种语言上编程”。
  • 你在技术浪潮中的位置决定了哪种方法是有效的-甚至是可能用到的。确定你在技术浪潮中的位置,并相应调整计划和预期目标。

4.1 选择编程语言

适于不同种类程序的最差和最好语言

figC3.PNG

4.2 编程约定

创建工作开始之前, 一定要写明你将要采用的编程约定、 统一约束说明一定要写得非常详尽

4.3 你在技术浪潮中的位置

"深入一种语言去编程"的例子

4.4 选择主要的构建实践方法



第二部分:创建高质量的代码

第5章、软件构建中的设计

本章要点

  • 软件的首要技术使命就是管理复杂度。以简单性作为努力目标的设计方案对此最有帮助
  • 简单性可以通过两种方式来获取
    1. 减少在同一时间所关注的本质性复杂度的量
    2. 避免生成不必要的偶然的复杂度
  • 设计是一种启发式的过程。固执于某一种单一方法会损害创新能力,从而损害你的程序
  • 好的设计都是迭代的。你尝试设计的可能性越多,你的最终设计方案就会变得越好。
  • 信息隐藏是个非常有价值的概念。通过询问“我应该隐藏些什么?”能够解决很多困难的设计问题。
  • 很多有用有趣的、关于设计的信息存在于本书之外。这里所给出的观点知识对这些有价值资源的一点提示而已

5.1 设计中的挑战

设计是一个险恶的问题

设计是一个了无章法的过程

设计就是确定取舍和调整顺序的过程

设计受到诸多限制

设计是不确定的

设计是一个启发式过程

设计是自然而然形成的

5.2 关键的设计概念

软件的首要技术使命:管理复杂度

如何应对复杂度:

  • 把任何人在同一时间需要处理的本质复杂度的量减到最少
  • 不要把偶然性的复杂度无畏地快速增长

理想的设计特征:

  • 最小的复杂度
  • 易于维护
  • 松散耦合(让程序各组成部分之间的关联最小)
  • 可扩展性
  • 可重用性
  • 高扇入(让大量的类使用某个给定的类)
  • 低扇出(让一个类少量或适中地使用其他的类,一般不超过7个)
  • 可移植性
  • 精简性
  • 层次性(尽量保持系统各个分解层的层次性)

设计的层次

  • 第1层:软件系统
  • 第2层:分解为子系统或包
  • 第3层:分解为类
  • 第4层:分解成子程序
  • 第5层:子程序的内部设计

5.3 设计构造块:启发式方法

1、找出现实世界中的对象

  • 辨识对象及其属性
  • 确定可以对各个对象进行的操作
  • 确定各个对象能对其他对象进行的操作
  • 确定对象的哪些部分对其他对象可见(public or private)
  • 定义每个对象的公开接口

2、形成一致的抽象

  • 基类是一种抽象,可以忽略各个具体派生类的细节;
  • 接口也是一种抽象,可以关注接口本身而不是类的内部工作方式

3、封装实现细节:填补抽象留下的空白

当继承能简化设计时就继承:定义对象之间的相同点和不同点,从普通到特殊

4、信息隐藏

  • 秘密和隐私权
  • 隐藏复杂度或者隐藏变化源
  • 信息隐藏的障碍
    1. 信息过度分散
    2. 循环依赖
    3. 把类内数据误认为全局数据
    4. 可察觉的性能损耗

5、找出容易改变的区域

  • 找出看起来容易变化的项目
  • 把容易变化的项目分离出来
  • 把看起来容易变化的项目隔离开来
    1. 业务规则
    2. 对硬件的依赖性
    3. 输入和输出
    4. 非标准的语言特性
    5. 困难的设计区域和构建区域
    6. 状态变量(使用枚举类型作为状态变量;使用访问器子程序取代状态变量的直接检查)
    7. 数据量的限制
  • 预料不同程度的变化

6、保持松散耦合

  • 耦合标准
    1. 规模(模块之间的连接数)
    2. 可见性(模块之间连接的显著程度)
    3. 灵活性(模块的连接是否容易松动)
  • 耦合的种类
    1. 简单数据参数的耦合
    2. 简单对象耦合
    3. 对象参数耦合
    4. 语义上的耦合

7、查阅常用的设计模式
适配器、桥接、装饰器、外观、工厂方法、观察者、单件、策略及模板方法---《设计模式》

figC4.PNG

设计模式的好处:

  • 通过提供现成的抽象来减少复杂度
  • 通过提供多种设计方案带来启发性的价值
  • 通过把设计对话提升到一个更高的层次来简化交流

8、其他的启发式方法

  • 高内聚性
  • 构造分层结构
  • 严格描述类契约
  • 分配职责
  • 为测试而设计
  • 避免失误
  • 有意识地选择绑定时间
  • 创建中央控制点
  • 考虑使用蛮力突破
  • 画一个图
  • 保持设计的模块化

9、使用启发式方法的原则

  1. 理解问题(未知量,数据,条件)
  2. 设计一个计划,找出数据和未知量的联系
  3. 执行求解计划,检查每一步
  4. 回顾,检视整个解

5.4 设计实践

迭代:在备选方案中循环并尝试不同的做法

分而治之:将程序分解为不同的关注区域,然后分别处理每一个区域。

自上而下和自下而上的设计方法:前者是分解策略,后者是合成策略

建立试验性原型:写出用于回答特定设计问题的、量最少且能够随时扔掉的代码

合作设计

编码前提前做出设计的影响因素考虑

记录你的设计成果

  • 把设计文档插入到代码里
  • 用wiki来记录设计讨论和决策
  • 写总结邮件
  • 将图片照成照片嵌入到文档
  • 保留设计挂图
  • 使用CRC(类、职责、合作者)卡片
  • 在适当的细节层创建UML层

第6章、可以工作的类

本章要点

  • 类的接口应提供一致的抽象,很多问题都是由于违背该原则而引起的
  • 类的接口应隐藏一些信息,如某个接口、某项设计决策或一些实现细节
  • 包含往往比继承更为可取,除非你要对“是一个is/a”的关系建模
  • 继承是一种有用的工具,但它却会增加复杂度,这有违于软件的首要技术使命——管理复杂度
  • 类是管理复杂度的首选工具,要在设计类时给与足够的关注,才能实现这一目标。

6.1 类的基础:抽象数据类型

(ADT,abstract data type)是指一些数据以及对这些数据所进行的操作的集合。

使用ADT的好处:

  • 可是隐藏实现细节
  • 改动不会影响到整个程序
  • 让接口能提供更多的信息
  • 更容易提高性能
  • 让程序的正确性显而易见
  • 程序更具自我说明性
  • 无需再程序内到处传递数据
  • 可以向在现实世界中那样操作实体

指导建议:

  • 把常见的底层数据类型创建为ADT并使用ADT,而不再使用底层数据类型
  • 把像文件这样的常用对象当成ADT
  • 简单的事物也可当做ADT
  • 不要让ADT依赖于其存储介质

在非面向对象环境中用ADT处理多分数据实例(例如在C中):要为ADT添加一些创建和删除实例的操作,同时设计ADT的其他服务操作,使其能支持多个实例。例如:

// 引入fontId,用来在创建和使用多个字体实例是分别控制每个实例
CreatFont(fontId)
DeleteFont(fontId)
SetCurrentFont(fontId)

ADT和类:将每个ADT用它自己的类实现,再加上继承和多态

6.2 良好的接口

1、好的抽象

  • 类的接口应该展现一致的抽象层次
class EmployeeCensus{
 public:
    ...
    // 这里的子程序抽象都在雇员层次上
    void AddEmployee( Employee employee );
    void RemoveEmployee ( Employee employee);
    Employee NextEmployee();
    Employee FirstEmployee();
    Employee LastEmployee();
    ...
private:
    // 使用ListContainer库的实现细节被隐藏起来了
    ListContainer m_EmployeeList;
    ...
};
  • 一定要理解类所实现的抽象是什么

  • 提供成对的服务(提供相应的、相等的以及相反的操作)

  • 把不相关的信息转移到其他类中

  • 尽可能让接口可编程,而不是表达语义

  • 谨防在修改时破坏接口的抽象

  • 不要添加与接口抽象不一致的公用成员

  • 同时考虑抽象性和内聚性

2、良好的封装

  • 尽可能地限制类和成员的可访问性
  • 不要公开暴露成员数据
  • 避免把使用的实现细节放入类的接口中
  • 不要对类的使用者做出任何假设
  • 避免使用友元类
  • 不要因为一个子程序里仅使用公用子程序,就把它归入公开接口
  • 让阅读代码比编写代码更方便
  • 格外警惕从语义上破坏封装性
  • 留意过于紧密的耦合关系

6.3 有关设计和实现的问题

1、包含(“有一个...”的关系)
包含:表示一个类含有一个基本数据元素或对象。

  • 通过包含来实现“有一个/has a”的关系
  • 在万不得已时通过private 继承来实现“有一个”的关系
  • 警惕有超过7个数据成员的类

2、继承(“是一个...”的关系)
继承:一个类是另一个类的一种特化

  • 用public继承来实现“是一个...”的关系
  • 要么使用继承并进行详细说明,要么就不要用它
  • 遵循Liskov替换原则
  • 确保只继承需要继承的部分
  • 不要“覆盖”一个不可覆盖的成员函数
  • 把公用的接口、数据及操作放到继承树中尽可能高的位置
  • 只有一个实例的类值得怀疑
  • 只有一个派生类的基类也值得怀疑
  • 派生后覆盖了某个子程序,但在其中没做任何操作,这种情况也值得怀疑
  • 避免让继承体系过深
  • 尽量使用多态,避免大量的类型检查
  • 让所有的数据多事private(而非protected)

3、成员函数和数据成员

  • 让类中子程序的数量尽可能少
  • 禁止隐式地产生你不需要的成员函数和运算符
  • 减少类所调用的不同子程序的数量
  • 对其他类的子程序的间接调用要尽可能少
  • 尽量减少类和类之间相互合作的范围

4、构造函数

  • 如果可能,在所有的构造函数中初始化所有的数据成员
  • 用私用(private)构造函数来强制实现单件属性(singleton property)
  • 优先采用深层副本,除非论证可行,才采用浅层复本(shallow copies)

6.4 创建类的原因

原因:

  • 为现实世界中的对象建模
  • 为抽象的对象建模
  • 降低复杂度
  • 隔离复杂度
  • 隐藏实现细节
  • 限制变动的影响范围
  • 隐藏全局数据(通过访问器子程序来操控全局数据)
  • 让参数传递更顺畅
  • 建立中心控制点
  • 让代码更易于重用
  • 为程序族做计划(把预计要被改动的部分放到单独的类里)
  • 把相关操作包装到一起
  • 实现某种特定的重构

应该避免的类

  • 避免创建万能类
  • 消除无关紧要的类(让类的数据成员成为其他类的属性)
  • 避免用动词命名的类(只有行为没有数据的类往往不是真正的类)

6.5 超越类:包

包:package

  • 用于区分“公用的类”和“某个包私用的类”的命名规则
  • 为了区分每个类所属的包而制定的命名规则/或代码组织规则
  • 规定什么包可以用的规则,包括是否可以用继承或者包含等

第7章、高质量的子程序

本章重点

  • 创建子程序的最主要目的是提高程序的可管理性,当然也有其他一些好的理由,其中,节省代码只是一个次要原因:提高可读性、可靠性和可修改性等原因都更重要一些
  • 有时候,把一些简单的操作写成独立的子程序也非常有价值
  • 子程序可以按照其内聚性分为很多类,而你应该让大多数子程序具有功能上的内聚性,这是最佳的一种内聚性
  • 子程序的名字是它质量的指示器,如果名字糟糕但恰如其分,就说明这个子程序设计得很差劲,如果糟糕又不准确,那么它就反映不出程序是干什么的,不管怎样,糟糕的名字都意味着程序需要修改。
  • 只有在某个子程序的主要目的是返回其名字所描述的特定结果时,才应该使用函数
  • 细心的程序员会谨慎地使用宏,而且只有在万不得已时才用。

7.1 创建子程序的正当理由

  • 降低复杂度
  • 引入中间、易懂的抽象
  • 避免代码重复
  • 支持子类化
  • 隐藏顺序
  • 隐藏指针操作
  • 提高可移植性
  • 简化复杂的布尔判断
  • 改善性能
  • 确保所有的子程序都很小

7.2 在子程序层上设计

内聚性:子程序中各种操作之间联系的紧密程度

  • 功能内聚性(一个子程序仅执行一项操作)
  • 顺序内聚性(子程序内包含按特定顺序执行的操作)
  • 通信内聚性(子程序中的不同操作使用了同样的程序,但不存在其他任何操作)
  • 临时的内聚性(因为需要同时执行才放到一起操作的子程序)
  • 过程的内聚性(子程序中的操作按照特定的顺序)
  • 逻辑上的内聚性(通过传入的控制标志选择执行子程序中的若干操作)
  • 巧合的内聚性(子程序中的各个操作之间没有任何可以看到的关联)

7.3 好的子程序名字

命名的指导原则:

  • 描述子程序所做的所有事情
  • 避免使用无意义的、模糊或表述不清的动词
  • 不要仅通过数字形成不同的子程序名字
  • 根据需要确定子程序长度的名字
  • 给函数命名时要对返回值有所描述
  • 给过程起名时使用语气强烈的动词加宾语的形式
  • 准确使用对仗词
  • 为常用操作确立命名规则

7.4 子程序可以写多长

复杂的算法总会导致更长的算法,可以允许子程序的长度有序地增长到100至200行(不算源代码中的注释行和空行)

7.5 如何使用子程序参数

指导原则:

  • 按照输入-修改-输出的顺序排列参数
  • 考虑创建自己的in和out关键字
  • 如果几个子程序都用类似的一些参数,应该让这些参数的排列顺序保持一致
  • 使用所有的参数
  • 把状态或出错变量放在最后
  • 在接口中对参数额假定加以说明
  • 子程序的参数个数限制在大约7个以内
  • 考虑对参数采用某种表示输入、修改、输出的命名规则
  • 为子程序传递用以维持其接口抽象的变量或对象
  • 使用具名参数(显式的把形参和实参对应起来)
  • 确保实参与形参相匹配

7.6 使用函数时要特别考虑的问题

1、什么时候用函数,什么时候用过程

写一个用状态变量作为显式参数的过程

2、设置函数的返回值

  • 检查所有可能的返回路径
  • 不要返回指向局部数据的引用或指针

7.7 宏子程序和内联子程序

用预处理的宏语言编写子程序

把宏表达式整个包含在括号内

#define Cube(a)  ((a)*(a)*(a))

把含有多条语句的宏用大括号括起来

#define LookupEntry( key, index )  {\
 index = (key-10)/5;\
 index = min( index, MAX_INDEX );\
 index = max( index, MIN_INDEX );\
}

用给子程序命名的方法战后后代码形同子程序的宏命名,以便在需要是可以用子程序来替换宏

C++取代宏的方案:

  • const 用于定义常量
  • inline 用于定义可被编译为内嵌的代码函数
  • template 用于以类型安全的方式定义各种标准操作,如min、max等
  • enum 用于定义枚举类型
  • typedef 用于定义简单的类型替换

内联子程序

节制使用inline子程序(要求将inline的实现代码写在头文件中,违反了封装原则)


第8章、防御式编程

本章要点

  • 最终产品代码中对错误的处理方式要比“垃圾进,垃圾出”复杂得多。
  • 防御式编程技术可以让错误更容易发现、更容易修改,并减少错误对产品代码的破坏
  • 断言可以帮助人尽早发现错误,尤其是在大型系统和提高可靠性的系统中,以及快速变换的代码中
  • 关于如何处理错误输入的决策是一项关键的错误处理决策,也是一项关键的高层设计决策
  • 异常提供了一种与代码正常流程角度不同的错误处理手段,如果留心是用异常,他可以成为程序员知识工具箱中一项有益补充,同时也应该在异常和其他错误处理手段之间进行权衡比较
  • 针对产品代码的限制并不适用于开发中的软件,你可以利用这一优势在开发中添加有助于更快地排查错误的代码

8.1 保护程序免遭无效输入数据的破坏

处理软件进来垃圾的情况:

  • 检查所有来源于外部的数据的值
  • 检查子程序所有输入参数的值
  • 决定如何处理错误的输入数据

防御式编程的最佳方式是在一开始就不要在代码中引入错误,使用迭代式设计、编码前先写伪代码、写代码前先写测试用例、底层设计检查等活动,都有助于防止引入错误。

8.2 断言

断言包含:

  • 一个描述为真时的情况的布尔表达式;
  • 一个断言为假时需要显示地信息

建立自己的断言机制

#define ASSERT( condition, message ) {    \
    if ( !(condition) ) {                 \
        LogError( "Assertion failed: ",   \
            #condition, message );        \
        exit( EXIT_FAILURE );             \
    }                                     \
}

使用断言的指导意见

  • 用错误处理代码来处理预期会发生的情况,用断言来处理绝不应该发生的状况
  • 避免把需要执行的代码放到断言中
  • 用断言来注解并验证前条件和后条件
  • 对于高健壮性的代码,应该先使用断言再处理错误

8.3 错误处理技术

1、返回中立值

继续执行操作并简单的返回一个没有危害的数值

2、换用下一个正确的数据

3、返回与前次相同的数据

4、换用最接近的合法值

5、把警告信息记录到日志文件中

6、返回一个错误码

7、调用错误处理子程序或对象

8、当错误发生时显示出错信息

9、关闭程序(例如医疗注射试剂的软件)

8.4 异常

基本结构:子程序使用throw跑出一个异常对象,再被调用链上层其他子程序的try-catch语句捕获

一些使用异常的建议:

  • 用异常通知程序的其他部分,发生了不可忽略的错误
  • 只有真正例外的情况下才抛出异常
  • 不要用异常来推卸责任
  • 避免在构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获
  • 在恰当的抽象层次抛出异常
  • 在异常消息中加入关于导致异常发生的全部信息
  • 避免使用空的catch语句
  • 了解所用函数库可能抛出的异常
  • 考虑创建一个集中的异常报告机制
  • 把项目中对异常的使用标准化
  • 考虑异常的替换方案

8.5 隔离程序以免遭由错误造成的损害

隔栏(barricade)是一种容损策略,把某些接口选定为“安全”区域的边界,对穿越安全区域边界的数据进行合法性校验。

隔栏与断言的关系:

在隔栏外部的程序使用错误处理技术;而在隔栏内部的程序应使用断言技术,因为传进来的数据应该已在通过隔栏时被清理过了。

8.6 辅助调试代码

不要自动地把产品版的限制强加于开发版之上

尽早引入辅助调试的代码

采用进攻式编程

  • 确保断言语句使程序终止运行
  • 完全填充分配到的所有内存,检测内存分配错误
  • 完全填充已分配的所有文件或流,排查文件分配错误
  • 确保每一个case语句中的default分支都能产生严重错误
  • 在删除一个对象之前把它填满垃圾数据
  • 让程序把它的错误日志用电子邮件发给你

计划移除调试辅助的代码

  • 使用类似ant和make这样的版本控制工具和make工具
  • 使用内置的预处理器
  • 编写自己的预处理器
  • 使用调试存根(sub routine)

8.7 确定在产品代码中该保留多少防御代码

  • 保留那些检查重要错误的代码
  • 去掉检查细微错误的代码
  • 去掉可以导致程序硬性崩溃的代码
  • 保留可以让程序稳妥地崩溃的代码
  • 为你的技术支持人员巨鹿错误信息
  • 确认留在代码中的错误信息是友好的

第9章、伪代码编程过程

本章要点

  • 创建类和子程序通常是一个迭代的过程,在创建子程序的过程中获得的认识常常会反过来影响类的设计
  • 编写好的伪代码需要使用易懂的英语,要避免使用特定编程语言中才有的特性,同时要在意图的层面上写伪代码
  • 伪代码编程过程是一个行之有效的做详细设计的工具,它同时让编码工作作出了多种方案,然后选出其中最佳的一种方案再开始编码
  • 每一步完成后都要检查你的工作成果,然后鼓励其他人帮你来检查。这样你就会在投入精力最少的时候,用最低的成本发现错误。

9.1 创建类和子程序的步骤概述

1、创建一个类的步骤

  • 创建类的总体设计(标识类的主要子程序)
  • 创建类的子程序
  • 复审并测试整个类

2、创建子程序的步骤

  • 设计子程序
  • 检查设计
  • 编写子程序的代码
  • 检查代码

9.2 伪代码

伪代码的指导原则

  • 用类似英语的语句来精确描述特定的操作
  • 避免使用目标编程语言中的语法元素
  • 在本意层面上编写伪代码
  • 在一个足够低的层次上编写伪代码,以便可以近乎自动地生成代码

同时还可以把伪代码编程编程语言中的注释,这样还可以节省大部分的注释工作

9.3 通过伪代码编程过程创建子程序

1、设计子程序

  • 检查先决条件
  • 定义子程序要解决的问题
  • 为子程序命名
  • 决定如何测试子程序
  • 在标准库中搜寻可用的功能
  • 考虑错误处理
  • 考虑效率问题
  • 研究算法和数据类型
  • 编写伪代码
  • 考虑数据
  • 检查伪代码
  • 在伪代码中试验一些想法,留下最好的想法(迭代)

2、编写子程序的代码

  • 写出子程序的声明
  • 把伪代码转变为高层次的注释
  • 在每条注释下面填充代码
  • 检查代码是否需要进一步分解

3、检查代码

  • 在脑海里检查程序中的错误
  • 编译子程序
    1. 把编译器的警告级别调到最高
    2. 使用验证工具
    3. 消除产生错误消息和警告的所有根源
  • 在调试器中逐行执行代码
  • 测试代码
  • 消除程序中的错误

4、收尾工作

  • 检查子程序的接口
  • 检查整体的设计质量
  • 检查子程序中的变量
  • 检查子程序的语句和逻辑
  • 检查子程序的布局
  • 检查子程序的文档
  • 除去冗余的注释

9.4 伪代码编程过程之外的其他方案

测试先行(在任何代码前先要写出测试用例)

重构(对代码进行一系列保持语义的变换和调整来提高代码的质量)

契约式设计(认为每个程序都具有前条件和后条件)

东拼西凑



第三部分:变量

第10章、使用变量的一般事项

本章要点

  • 数据初始化过程很容易出错,所有请用本章所描述的初始化方法来避免由于非预期的初始值而造成的错误
  • 最小化每个变量的作用域,把同意变量的引用点集中起来,把变量限定在子程序或类的范围之内,避免使用全局数据。
  • 把使用相同变量的语句尽可能集中在一起
  • 早期绑定会减低灵活性,但有助于减小复杂度。晚期绑定可以增加灵活性,同时增加复杂度
  • 把每个变量用于唯一的用途。

10.1 数据认知

数据认知测试

抽象数据类型  文字量     数组             局部变量
位图          局部变量   位图             查找表
布尔变量      数据成员   B-树             指针
字符变量      私用       容器类           retroactive synapse
双精度        引用完整性 elongated stream 栈
枚举类型      字符串     浮点             结构变量
堆            树         下标             typedef
整数          共用体     链表             value chain 
具名变量      变体       

10.2 轻松掌握变量定义

隐式声明(在未声明变量的时候,编译器会自动为你声明变量)

一些建议:

  • 关闭隐式声明
  • 声明全部的变量
  • 遵循魔种命名规则
  • 检查变量名(交叉引用表)

10.3 变量初始化原则

  • 在声明变量的时候初始化
  • 在靠近变量第一次使用的位置初始化它
  • 理想情况下,在靠近第一次使用变量的位置声明和定义该变量
  • 在可能的情况下使用final或者const
  • 特别注意计数器和累加器
  • 在类的构造函数里初始化该类的数据成员
  • 检查是否需要重新初始化
  • 一次性初始化具名变量:用可执行代码来初始化变量
  • 使用编译器设置来自动初始化所有变量
  • 利用编译器的警告信息
  • 检查输入参数的合法性
  • 使用内存访问检查工具来检查错误的指针
  • 在程序开始时初始化工作内存

10.4 作用域

作用域(或者可见性):变量在程序内的可见和可引用的范围

1、使变量引用局部化

  • 把一个变量的的引用局部化
  • 跨度:衡量一个变量的不同引用点的靠近程度

2、尽可能缩短变量的“存活”时间

  • 存活时间:一个变量存在期间所跨越的语句总数
  • 测量变量的生存时间:计算某一变量最初引用点和最后引用点之间的代码行数

3、减小作用域的一般原则

  • 在循环开始之前再去初始化该循环里使用的变量,而不是在该循环所属的子程序的开始处初始化这些变量
  • 直到变量即将被使用时再为其赋值
int receiptIndex = 0;
float dailyReceipts = TodaysReceipts();
double totalReceipts = TotalReceipts( dailyReceipts );
  • 把相关语句放到一起
  • 把相关语句组提取成单独的子程序
  • 开始时采用最严格的可见性,然后根据需要扩展变量的作用域

4、有关缩小变量作用域的说明

全局变量的使用,取决于程序员对“方便性”和“智力上的可管理性”

10.5 持续性

对一项数据的生命周期的另一种描述

措施:

  • 在程序中加入调试代码或者断言来检查哪些关键变量的合理取值。
  • 准备抛弃变量时给他们附上“不合理的数值”。
  • 编写代码是要假设数据并没有持续性
  • 养成在使用所有数据之前声明和初始化的习惯。

10.6 绑定时间

绑定时间:把变量和它的值绑定在一起的时间

  • 编码时(使用神秘代码)
  • 编译时(使用具名变量)
  • 加载时(从windows注册表、java属性文件等外部数据源中读取数据)
  • 对象实例化(例如每次其在窗体创建的时候读取文件)
  • 即时(例如在每次窗体重绘的时候读取数据)

一般而言,绑定时间越早灵活性越差,但复杂度也会越低

10.7 数据类型和控制结构之间的关系

序列性语句翻译为程序中的顺序语句

选择型数据翻译为程序中的if和case语句

迭代型数据翻译成程序中的for、repeat、while等循环结构

10.8 为变量指定单一用途

  • 每个变量只用于单一用途(例如,在两个场合使用temp等临时变量)
  • 避免让代码具有隐含含义
  • 确保使用了所有已声明的变量(研究表明,未引用的变量数量与错误率呈正相关)

第11章、变量名的力量

本章要点

  • 好的变量名是提高程序可读性是一项关键要素。对特殊种类的变量,比如循环下标和状态变量,需要加以特殊的考虑
  • 名字要尽可能地具体,那些太模糊或者太通用用以致于能够用于多种目的的名字通常都是很不好的
  • 命名规则应该能够区分局部数据、类数据和全局数据。它们还应当可以区分类型名、具名常量。枚举类型名字和变量名
  • 无论做那种类型项目,你都应该采用某种变量命名规则。你所采用的规则的种类取决于你的程序的规模,以及项目成员的人数。
  • 现代编程语言很少用到缩写,如果你真的要缩写,请使用项目缩写辞典或者标准前缀帮助理解缩写
  • 代码阅读的次数远远多于编写的次数。确保你所取的名字更侧重于阅读方便而不是编写方便

11.1 选择好变量名的注意事项

最重要的命名注意事项:容易理解,名字长度实用

变量用途 好名字,好描述 坏名字,差描述
到期的支票累计额 runningTotal, checkTotal written, ct, checkes, CHKTTL, x, x1, x2
高速列车的运行速度 velocity, trainVelocity, velocityInMph velt, v, tv, x, x1,x2,train
当前日期 currentDate, todaysDate cd, current, c, x, x1, x2, date
每页的行数 linesPerPage lpp, lines, l, x, x1, x2

以问题为导向,一个好的名字反映的通常的都是问题,而不是解决方案

最适当的名字长度:平均长度在10到16个字符

变量名对作用域的影响:对位于全局命名空间中的名字加以限定词

变量名的计算值限定词:比如Total, Sum等限定词加到名字的最后

变量名中常用的对仗词

  • begin/end
  • first/last
  • locked/unlocked
  • min/max
  • next/previous
  • old/new
  • opened/closed
  • visible/invisible
  • source/target
  • source/destination
  • up/down

11.2 为特定类型的数据命名

1、为循坏下标命名:i,j,k都是约定俗成的,如果是循环之外的,最好取一个更有意义的名字

2、为状态变量命名:取一个比flag更好的命名

if( dataReady ) ...
if ( characterType & PRINTABLE_CHAR ) ...
if ( reportType == ReportType_Annual ) ...
if ( recalcNeeded == false ) ...

dataReady = true;
characterType = CONTROL_CHARACTER;
reportType = ReportType_Annual;
recalcNeeded = false;

3、为临时变量命名:警惕临时变量,使用准确且有描述性的变量名替代

4、为布尔变量命名:给布尔变量赋予隐含“真假”含义的名字

5、为枚举类型命名:使用组前缀,如Color_等明确成员属于同一组

6、为常量命名:根据常量所表示的含义命名

11.3 命名规则的力量

1、为什么要有规则

  • 要求你更多地按规矩行事
  • 有助于在项目之间传递知识
  • 有助于你在新项目更快速地学习代码
  • 有助于减少名字增生
  • 弥补编程语言的不足之处
  • 强调相关变量之间的关系

2、何时采用命名规则

  • 当多个程序员合作开发一个项目是
  • 当计划将程序交给另一位程序员维护的时候
  • 当你所在的组织中的其他程序员评估你写的程序的时候
  • 当你写的程序规模太大
  • 当你写的程序生命期足够长时
  • 当在一个项目存在一些不常见的术语

11.4 非正式命名规则

1、与语言无关的命名规则的指导原则

  • 区分变量和子程序名字
  • 区分类和对象
  • 标识全局变量
  • 标识成员变量
  • 标识类型声明
  • 标识具名常量
  • 标识枚举类型的元素
  • 在不能保证输入参数只读的语言里标识只读参数
  • 格式化命名以提高可读性

2、与语言相关的命名规则的指导原则

  • C的命名规则
. c和ch是字符变量
. i和j是整数下标
. n表示某物的数量
. p是指针
. s是字符串
. 预处理宏全部大写,通常也包括typedef
. 下划线_做分隔符,如letters_in_lowercase
  • C++的命名规则
. i和j是整数下标
. p是指针
. 常量,typedef预处理宏全部大写
. 类和其他类型的名字混合大小写
. 变量名和函数名中的第一个单词小写,后续每个单词首字母大写。例如variableOrRoutineName
. 下划线不再用作名字的分隔符,除非是全部大写的名字或者特定前缀
  • Java的规则
. i和j是整数下标
. 常量全部大写,并用下划线分隔
. 类名和接口名的每一个单词的首字母大写,例如ClassOrInterfaceName
. 变量名和方法名第一个单词的首字母小写,后续的首单词大写,例如variableOrRoutineName
. 除用于全部大写的名字之外,不适用下划线作为名字中的分隔符
. 访问器子程序使用get和set前缀

3、变量名需要包含以下三类信息:

  • 变量的内容(它代表什么)
  • 数据的种类(具名常量、简单变量、用户自定义类型或者类)
  • 变量的作用域(私用的、类的、包的或者全局的作用域)

11.5 标准前缀

1、用户自定义类型的缩写

figC6.PNG

2、语义前缀

figC7.PNG

3、标准前缀的优点

  • 能更为精确地描述一些含义比较模糊的名字
  • 使名字变得更加紧凑
  • 帮助程序员准确地对类型做出判断

11.6 创建具备可读性的短名称

1、缩写的一般性指导

  • 使用标准的缩写
  • 去掉所有的前置原因,如integer变成intgr
  • 去掉虚词and,or,the等
  • 使用每个单词的第一个或前几个字母
  • 统一地在每个单词的第一、第二字母后截断
  • 保留每个单词的第一个和最后一个字母
  • 使用名字中的每一个重要单词,最多不超过三个
  • 去除无用的后缀——ing,ed等
  • 确保不要改变变量的含义

2、有关缩写的评论

  • 不要用从每个单词中删除一个字符的方式来缩写
  • 缩写要一致
  • 创建你能读出来的名字
  • 避免使用容易看错或者读错的字符组合
  • 使用辞典来解决命名冲突
  • 在代码里用缩写对照表解释极短的名字的含义
  • 名字对于代码读者的意义要比对作者更重要

11.7 应该避免的名称

  • 避免在名字中使用数字
  • 避免在名字中拼错单词
  • 避免使用英语中常常拼错的单词
  • 不要仅靠大小写来区分变量名
  • 避免使用多种自然语言
  • 避免使用标准类型、变量和子程序的名字
  • 不要使用与变量含义完全无关的名字
  • 避免在名字中包含易混淆的字符

第12章、基本数据类型

本章要点

  • 使用特定的数据类型就意味要记住用于各个类型的很多独立的原则。
  • 如果你的语言支持,创建自定义类型会使得你的程序更容易修改,并更具有自描述性
  • 当你用typedef或者其等价方式创建了一个简单类型的时候,考虑是否更应该创建一个新的类

12.1 数值概论

一些建议:

  • 避免使用“神秘数值”
  • 如果需要,可以使用硬编码的0和1
  • 预防除零错误
  • 使类型转换变得明显
  • 避免混合类型的比较
  • 注意编译器的警告

12.2 整数

  • 检查整数的除法
  • 检查整数溢出
    figC8.PNG
  • 检查中间结果溢出

12.3 浮点数

  • 避免数量级相差巨大的数之间的加减运算
  • 浮点数应避免等量判断,==
  • 处理舍入误差问题
  • 检查语言和函数库对特定数据类型的支持

12.4 字符和字符串

  • 避免神秘字符和神秘字符串(指程序中随处可见的字面形式表示的字符串)
  • 避免off-by-one错误(下标的索引)
  • 了解你的语言和开发环境是如何支持unicode.
  • 在程序生命期中尽早决定国际化/本地化策略
  • 如果你知道只需要支持一种文字的语言,请考虑使用ISO 8859字符集
  • 如果需要支持多种语言,请使用UNICODE
  • 采用某种一致的字符串类型转换策略

C语言中的字符串

  • 注意字符串指针和字符数组之间的差异
  • 把C-style字符串的长度申明为CONSTANT+1
  • 用null初始化字符串以避免没有终端的字符串
  • 用字符数组取代C中的指针
  • 用strcpy()取代以避免无终端的字符串

12.5 布尔变量

  • 用布尔变量对程序加以文档说明
  • 用布尔变量来简化复杂的判断
  • 如果需要的话,创建自己的布尔类型
typedef int BOOLEAN;
// 或者
//enum Boolean
//{
//    True = 1,
//    False = (!True)
//};

12.6 枚举类型

  • 用枚举类型来提高可读性
  • 用枚举类型来提高可靠性
  • 用枚举类型来简化修改
  • 将枚举类型作为布尔变量的替代方案
  • 检查非法数值
  • 定义出枚举的第一项和最后一项,以便于循环边界
  • 将枚举的第一个元素留作非法值
  • 明确定义项目编写标准中第一个和最后一个元素的使用原则,并且在使用时保持一致
  • 警惕给枚举元素明确赋值而带来的失误
  • 如果语言里没有枚举类型,可以用全局变量或者类来模拟它

12.7 命名常量

  • 在数据声明中使用具名常量可以提高程序的可读性和可维护性
  • 避免使用文字量,即使是“安全”的
  • 用具有适当作用域的变量或类来模拟具名常量
  • 统一地使用具名常量

12.8 数组

  • 确认所有的数组下标都没有超出数组的边界
  • 考虑用容器来取代数组,或者将数组作为顺序化结构来处理
  • 检查数组的边界点
  • 如果数组是多维的,确认下标的使用顺序是正确的
  • 提防下标串话(cross-talk)
  • 在C中结合ARRAY_LENGTH()宏来使用数组
#define ARRAY_LENGTH(x) (sizeof(x)/sizeof(x[0]))

12.9 创建自己的类型(类型别名)

一些指导原则:

  • 给所创建的类型取功能导向的名字
  • 避免使用预定义类型
  • 不要重定义一个预定义的类型
  • 定义替代以便于移植
  • 创建一个类而不是使用typedef

第13章、不常见的数据类型

本章要点

  • 结构体可以使得程序更简单,更容易理解,以及更容易维护
  • 每当你打算使用结构体的时候,考虑采用类是不是会工作得更好
  • 指针很容易出错。访问器子程序或类以及防御式编程实践来保护自己的代码
  • 避免用全局变量,不只是他们很危险,还是因为你可以用其他更好的方法来取代他们
  • 如果你不得不使用全局变量,那么就通过访问子程序来使用它,访问器子程序能为你带来全局变量所能带来的一切优点,还有一些额外好处。

13.1 结构体

使用结构体的理由

  • 用结构体来明确数据关系
  • 用结构体简化对数据块的操作
  • 用结构体简化参数列表
  • 用结构体来减少维护

13.2 指针

1、使用指针的一般技巧

  • 把指针操作限制在子程序或者类里面
  • 同时声明和定义指针
  • 在与指针分配相同的作用域中删除指针
  • 在使用指针之前检查指针
  • 先检查指针所引用的变量再使用它
  • 用标记字段(tag field)来检测损毁的内存
  • 增加明显的冗余
  • 用额外的指针变量来提高代码的清晰度(不要节约指针变量)
  • 简化复杂的指针表达式
  • 画一个图来解释指针
  • 按照正确的顺序删除聊表中的指针(在释放当前指针之前,确保已经有指向链表下一个元素的指针了)
  • 粉碎垃圾数据
  • 在删除或者释放指针之后把它们设为空值
  • 在删除变量之前检查非法指针
  • 跟踪指针分配情况
  • 编写覆盖子程序,集中实现避免指针问题的策略
  • 采用非指针技术

2、C++指针

  • 理解指针和引用之间的区别
  • 把指针用于“按引用传递”参数,把const引用用于“按值传递”参数
  • 使用auto_ptr(或者shared_ptr)
  • 灵活运用智能指针

3、C指针

  • 使用显式指针类型而不是默认类型
  • 避免强制类型转换
  • 遵循参数传递的*规则(只有在赋值语句的参数前面加星号,才能把参数从子程序中传回去)
void TryToPassBackValue(int *parameter)
{
    *parameter = SOME_VALUE;
}
  • 在内存分配中使用sizeof()确定变量的大小

13.3 全局数据

1、与全局数据有关的常见问题

  • 无意间修改了全局数据
  • 与全局数据有关的奇异的和令人激动的别名问题
  • 与全局数据有关的代码重入问题
  • 全局数据阻碍代码重用
  • 与全局数据有关的非确定的初始化顺序事宜
  • 全局数据破坏了模块化和智力上的可管理性

2、使用全局数据的理由

  • 保存全局数值
  • 模拟具名常量
  • 模拟枚举类型
  • 简化对极其常用的数据的使用
  • 消除流浪数据(tramp data)

3、只有万不得已时才使用全局数据

  • 仅当需要时才把变量设置为全局的
  • 区分全局变量和类变量
  • 使用访问器子程序

4、用访问器子程序来取代全局数据

  • 要求所有的代码通过访问器子程序来存取数据
  • 不要把你所有的全局数据都扔在一处
  • 用锁定来控制对全局变量的访问
  • 在访问器子程序里构建一个抽象层

figC9.PNG

  • 使得对一项数据的所有访问都发生在同一个抽象层上

5、如何降低使用全局数据的风险

  • 创建一种命名规则突出全局变量
  • 为全局变量创建一份良好的清单
  • 不要用全局变量来存放中间结果
  • 不要把所有的数据都放在一个大对象中并到处传递


第四部分:语句

第14章、组织直线型代码

本章要点

  • 组织直线型代码的最主要原则是按照依赖关系进行排列
  • 可以用好的子程序名、参数列表、注释,以及——如果代码足够重要——内务管理变量来让依赖关系变得更明显
  • 如果代码之间没有顺序依赖关系,那就设法使相关的语句尽可能地接近

14.1 必须有明确顺序的语句

组织语句的原则:

  • 设法组织代码,使依赖关系变得非常明显
  • 使子程序名能凸显依赖关系
  • 利用子程序参数明确显示依赖关系
  • 用注释对不清晰的依赖关系进行说明
  • 用断言或者错误代码来检查依赖关系

14.2 顺序无关的语句

1、使代码易于自上而下地阅读

2、把相关的语句组织在一起

第15章、使用条件语句

本章要点

  • 对于简单的if-else语句,请注意if子句和else子句的顺序,特别是用它来处理大量错误的时候,要确认正常情况是清晰的
  • 对于if-then-else语句串和case语句,选择一种最有利于阅读的顺序
  • 为了捕捉错误,可以使用case语句中的default子句(默认子句),或者使用if-then-else语句串中的最后那个else子句
  • 各种控制结构并不是生来平等的。请为代码的每个部分选用最合适的控制结构

15.1 if语句

1、简单的if-then语句

  • 首先写正常代码路径,再处理不常见情况
  • 确保对于等量的分支是正确的(仔细考虑大于等于的端点情况)
  • 把正常情况的处理放在if后面而不要放在else后面
  • 让if字句后面跟随一个有意义的语句
  • 考虑else子句(或者用一个空语句+注释的形式)
  • 测试else子句的正确性
  • 检查if和else子句是不是弄反了

2、if-then-else语句串

  • 利用布尔函数调用简化复杂的检测,可提高可读性
if ( IsControl( inputCharacter ) )
{
    characterType = CharacterType_ControlCharacter;
}
else if ( IsPunctuation( inputCharacter ) )
{
    characterType = CharacterType_Punctuation;
}
else if ( IsDigit( inputCharacter ) )
{
    characterType = CharacterType_Digit;
}
else if ( IsLetter( inputCharacter ) )
{
    characterType = CharacterType_Letter;
}
  • 把最常见的情况放在最前面
  • 确保所有的情况都考虑到了(最后一个else子句用出错信息或者断言来捕捉你不考虑的情况)
  • 如果语言支持,请把if-then-else语句串换成其他结构

15.2 case语句

1、为case选择最有效的排列顺序

  • 按字母顺序或按数字顺序排列各种情况
  • 把正常的情况放在前面
  • 按执行频率排列case子句

2、使用case语句的诀窍

  • 简化每种情况对应的操作(确保代码短小精悍)
  • 不要为了使用case语句而刻意制造一个变量
  • 用default子句只用于检查真正的默认情况
  • 用default子句来检测错误
  • 在C++和Java里,避免代码执行越过一条case子句的某尾
  • 在C++里,在case末尾明确无误地标明需要穿越执行的程序流程

第16章、控制循环

本章要点

  • 循环很复杂,保持循环简单有助于别人阅读你的代码
  • 保持循环简单的技巧包括:避免使用怪异的循环、减少嵌套层次、让入口和出口一目了然,把内务操作代码放在一处
  • 循环下标容易被滥用。因此命名要准确。并且要把它们各自仅用于一个用途
  • 仔细地考虑循环,确认它在每一种情况下都运行正常,并且在所有可能的条件下都能退出。

16.1 选择循坏的种类

计数循环、连续求值循环、无限循环、迭代器循环

1、什么时候用while循环

  • 检测位于循环的开始
  • 检测位于循环的结尾(do-while)

2、什么时候用带退出的循环

score = 0;
while ( true )
{
    GetNextRating( &ratingIncrement );
    rating = rating + ratingIncrement;
    if ( !( (score < targetScore) && (ratingIncrement != 0) ) )
        break;
    GetNextSore( &scoreIncrement );
    score = score + scorement;
}

3、何时使用for循环

如果需要执行一个次数固定的循环

16.2 循环控制

1、进入循环的指导原则

  • 只从一个位置进入循环
  • 把初始化代码紧放在循环前面
  • 用while(true)表示无限循环
  • 在适当的情况下多使用for循环
  • 在while循环更适用的时候,不要使用for循环

2、处理好循环体

  • 用{和}把循环中的语句括起来
  • 把循环内务操作要么放在循环的开始,要么放在循环的末尾
  • 一个循环只做一件事(即使合并后可以提高效率,也要先写成两个循环,标明可以合并,等到测量数据显示程序性能低下时再去合并)

3、退出循环

  • 设法确认循环能够终止
  • 使循环终止条件看起来明显
  • 不要为了终止循环而胡乱改动for循环下标
  • 避免出现依赖于循环下标最终取值的代码
  • 考虑使用安全计数器
// 改动前
do
{
    node = node->Next;
    ...
}while ( node->Next != NULL );

// 使用安全计数器后
safetyCounter = 0;
do
{
    node = node->Next;
    if ( safetyCounter >= SAFETY_LIMIT )
    {
        Assert( false, "Internal Error: Safety-Counter Violation." );
    }
}while( node->Next !=  NULL);
  • 提前退出循环

3、检查端点

注意开始的情况,任意选择中间情况以及最终的情况,以确认该循环不会出现任何的off-by-one错误

4、使用循环变量

  • 用整数或者枚举类型表示数组和循环的边界
  • 在嵌套循环中使用有意义的变量名来提高其可读性
  • 用有意义的名字来避免循环下标串话(cross-talk)
  • 把循环下标变量的作用域限制在本循环内

5、循环应该有多长

  • 循环要尽可能短,以便能够一目了然
  • 把嵌套限制在3层以内
  • 把长循环的内容移到子程序里
  • 要让长循环格外清晰

16.3 轻松创建循环——由内而外

从一种情况开始,用字面量来编写代码,然后缩进它,在外面加上一个循环,然后用循环的下标或者计算表达式替换那些字面量。根据你的需要持续这一过程,等你做完以后,再加上所有需要的初始化。这一过程可以看做是由内而外的编码。

16.4 循环和数组的关系

循环和数组之间有着密切的联系。在许多情况中,循环就是用来操纵数组的,而且循环计数器和数组下标一一对应。


第17章、不常见的控制结构

本章要点

  • 多个return可以增强子程序的可读性和可维护性,同时可以避免很深的嵌套逻辑。但是使用它的时候要多加小心
  • 递归能够很优雅地解决一小部分问题,对它的使用也要倍加小心
  • 在少数情况下,goto是编写可读性和可维护代码的最佳方法,但这种情况非常罕见,除非万不得已,不要使用goto.

17.1 子程序中的多个返回

  • 如果能增加可读性,那么就使用return
Comparison Compare( int value1, int value2 )
{
    if (value1 < value2)
    {
        return Comparison_LessThan;
    }
    else if (value1 > value2)
    {
        return Comparison_GreaterThan;
    }
    return Comparison_Equal;
}
  • 用防卫子句(guard clause)来简化复杂的错误处理
  • 减少每个子程序中return的数量

17.2 递归

使用递归的技巧:

  • 确认递归能够停止
  • 使用安全计数器防止出现无穷递归
  • 把递归限制在一个子程序内,循环递归(A调用B,B调用C,C调用A)是非常危险的
  • 留心栈空间,换句话说,要用new在堆上创建对象,而不要让编译器在栈上自动创建对象
  • 不要用递归去计算阶乘或者斐波拉契数列
  • 用递归能做到的,同样可以用栈和循环队列来做到。

17.3 goto

  • 在那些不支持结构化控制语句的语言里,用goto去模拟那些控制结构
  • 如果语言内置了等价的控制结构,那么就不要用goto
  • 如果是为了提高代码效率而使用goto,请衡量词句实际带来的性能提升
  • 精良在每个子程序内只使用一个goto标号
  • 尽量让goto向前跳转而不要向后跳转
  • 确认所有的goto标号都被用到了
  • 确认goto不会产生某些执行不到的代码

第18章、表驱动法

表驱动法是一种编程模式——从表里面查找信息而不是用逻辑语句(if和case)

本章要点

  • 表提供了一种复杂的逻辑和继承结构的替代方案,如果你发现自己对某个应用程序的逻辑或者继承树关系感到困惑,那么问问自己它是否可以通过一个查询表来加以简化
  • 使用表的一项关键决策是决定如何去访问表,你可以采取直接访问、索引访问或者阶梯访问
  • 使用表的另一项关键决策是决定应该把什么内容放入表中。

18.1 表驱动方法使用总则

使用表驱动法需要解决的两个问题:

  • 怎样从表中查询条目
  • 从表里面查询记录的方法(直接访问,索引访问,阶梯访问)

18.2 直接访问表

示例:一个月中的天数

  • 如果需要计算每个月的天数,笨拙的做法是用if-else判断每月的情况
  • 一个更简单,更容易修改的方法是把这些数据都存到一张表里
Dim daysPerMonth() as Integer = _
{ 31, 28, 31, 30 ,31, 30, 31, 31 30, 31,30, 31 }
days = daysPerMonth( month-1 )

18.3 索引表访问

索引表不是直接访问,而是经过居间的索引去访问,查询表的索引数组基本为空,而查询表本身基本为满。

索引访问的优点:

  • 如果主查询表中的每一天记录都很大,那么常见一个浪费了很多空间的索引数组所用空间,就要比创建一个浪费了很多空间的主查询表所用的空间小得多;
  • 即使用了索引以后没有节省内存空间,操作位于索引中的记录又是也要比操作位于主表中的记录更方便更廉价
  • 表查询技术在可维护性上所具有的普遍优点。

18.4 阶梯访问表

基本思想是表中记录对于不同的数据范围有效,而不是对不同的数据点有效,通过确定每项命中的阶梯层次确定其归类,它命中的台阶确定其类属。

使用阶梯技术的一些关注点

  • 留心端点
  • 考虑用二分查找取代顺序查找
  • 考虑用索引访问来取代阶梯技术
  • 把阶梯表查询操作提取成单独的子程序

第19章、一般控制问题

本章要点

  • 使布尔表达式简单刻度,将非常有助于你提高代码质量
  • 深层次的嵌套使得子程序变得难以理解。所幸的是,你可以相对容易地避免这么做;
  • 结构化编程是一种简单并且仍然适用的思想:你可以通过把顺序、选择和循环三者结合起来而开发出任何程序
  • 将复杂度降低到最低水平是编写高质量代码的关键。

19.1 布尔表达式

  • 用标识符true和false做布尔判断
  • 将复杂的判断移入布尔函数,用新的中间值使判断更清晰
  • 编写肯定形式的布尔表达式
if ( statusOK )
{
    // do something else
    ...
}
else
{
    // dosomething
    ...
}

figC10.PNG

  • 用括号使布尔表达式更清晰
  • 理解布尔表达式是如何求值的
  • 按照数周的顺序编写数值表达式

布尔表达式的常见问题

  • 在C语言家族中应该把常量放在比较的左端
  • 在C++中,可以考虑创建预处理宏来替换&&,||和==(不得已才这么做)

19.2 复合语句

“复合语句”或者“语句块”指的是一组语句,该组语句被视为一条单一的语句,用于控制程序流,一般的{}里面的语句为复合语句

19.3 空语句

在C++中可以写空语句,即一条仅含有分号的语句

while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() )
{
;
}

//或者
while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() )
{
}

19.4 驯服危险的深层嵌套

如果你写出了深层的嵌套,那么可以重新设计if和else子句中执行的判断,或者把代码重构为更简单的子程序。

  • 通过重复检测条件中的某一部分来简化嵌套的if语句
  • 用break块来简化嵌套if
  • 把嵌套if转换成一组if-then-else语句
  • 把嵌套if转换成case语句
  • 把深层嵌套的代码抽取出来放进单独的子程序
  • 使用一种更面向对象的方法(例如多态机制和object factory)
  • 重新设计深层嵌套的代码

19.5 编程基础:结构化编程

核心思想:一个应用程序应该只采用一些单入单出的控制结构(单入单出的控制结构指,代码块只能从一个位置开始执行,并且只能结束于一个位置)

结构化编程的三个组成部分

  • 顺序
  • 选择(if-else,case)
  • 迭代(循环)

中心论点:任何一种控制流都可以由顺序、选择和迭代三种结构生成。

19.6 控制结构与复杂度

如何衡量复杂度

计算子程序的决策点

  1. 从1开始,一直往下通过程序
  2. 一旦遇到以下关键字或者同类词就加1:if,while,repeat, for, and, or
  3. 给case语句中的每一种情况加1
if ( ((status=Success) and Done) or (not Done and (numLines >= maxLines)) )
then ...

在上述代码中,从1算起,遇到if得2,and得3,or得4,and得5,加起来,代码里一共有5个决策点


处理复杂度的衡量结果

得分 结论
0-5 子程序可能还不错
6-10 得想办法简化子程序了
10+ 把子程序的某一部分拆分并调用它


第五部分:代码改善

第20章、软件质量概述

本章要点

  • 开发高质量代码最终并没有要求你付出更多,只是你需要对资源进行重新分配,以廉价的成本防止缺陷出现,从而避免代价高昂的修正工作;
  • 并非所有的质量保证目标都可以全部实现,明确哪些目标是你希望达到的,并就这些目标和团队成员进行沟通;
  • 没有任何一种错误检测方法能够解决全部问题,测试本身不是排除错误最有效方法,成功的质量保证计划应该使用多种不同的技术来检查不同类型的错误
  • 在构建期间应使用一些有效的质量保证技术,但在这之前,一些具有同样强大功能的质量保证技术也是必不可少的,错误越早发现,它和其余代码的纠缠就越少,由此造成的损失就越小
  • 软件领域的质量保证是面向过程的,软件开发与制造业不一样,在这里并不存在会影响最终产品的重复的阶段,因此,最终产品的质量受到开发软件所用的过程的控制。

20.1 软件质量的特性

质量的外在特性:

  • 正确性
  • 可用性
  • 效率
  • 可靠性
  • 完整性
  • 适应性
  • 精确性
  • 健壮性

质量的内在特性

  • 可维护性
  • 灵活性
  • 可移植性
  • 可重用性
  • 可读性
  • 可测试性
  • 可理解性

20.2 改善软件质量的技术

软件质量的要素

  • 软件质量目标
  • 明确定义质量保证工作
  • 测试策略
  • 软件工程指南
  • 非正式技术复查
  • 正式技术复查
  • 外部审查

开发过程

  • 对变更进行控制的过程
  • 结果的量化
  • 制作原型

设置质量目标

20.3 不同质量保障极速的相对效能

缺陷检测率

figC11.PNG

找出缺陷的成本

  • 阅读代码每小时能检测出的缺陷要比测试高80%左右。

修正缺陷的成本

  • 一个缺陷存在的时间越长,消除它的代价就越高昂。

软件质量的推荐阵容:

  • 对所有需求,架构以及系统关键部分设计进行正式检查
  • 建模或者创建原型
  • 代码阅读或者检查
  • 执行测试

20.4 什么时候进行质量保证工作

在早期阶段就开始强调质量保证工作,并且将其贯彻到项目的余下部分中。

20.5 软件质量的普遍原理

普遍原理就是改善质量以降低开发成本。

把时间投入到前期工作中,能让程序员在后期工作中节省更多的时间。


第21章、协同构建

本章要点

  • 协同开发实践往往能比测试发现更多的缺陷,并且更有效率;
  • 协同开发实践所发现错误的类型通常跟测试所发现的不同,这意味着你需要同时使用祥查和测试来保证你软件的质量
  • 正式检查通过运用核对表、准备工作、明确定义的角色以及对方法的持续改善,将缺陷侦测的效率提升至最高,它往往能比走查发现更多的缺陷
  • 通常,结对编程拥有和详查相同的成本,并能产生质量相当的代码,当需要缩短开发周期的时候,结对编程就非常有价值,相对于单独工作来说,有些开发人员更喜欢结对工作
  • 正式检查可以应用在除代码之外的很多工作成果上,例如需求,设计以及测试用例
  • 走查和代码阅读是详查的替代方案,代码阅读更富有弹性,能有效地利用每个人的时间

21.1 协同开发实践概要

协同构建:包括结对编程、正式检查、非正式复查、文档阅读、以及其他让开发人员共同承担创建代码及其他工作产品责任的技术。

协同构建的首要目的是改善软件质量。

  • 协同开发在捕获错误和发现错误类型方面效能比测试高。
  • 协同构建有利于传授公司文化以及编程专业知识
  • 集体所有权适用于所有形式的协同构建

21.2 结对编程

一位程序员敲代码,另一位注意有没有出现错误,并考虑某些策略性的问题。

运用结对编程的关键

  • 用编码规范来支持结对编程
  • 不要让结对编程变成旁观
  • 不要强迫在简单的问题上使用结对编程
  • 有规律地对结构人员和分配的工作任务进行轮换
  • 鼓励双方跟上对方的步伐
  • 确认两个人都能够看到显示器
  • 不要强迫程序员与自己关系紧张的人组队
  • 避免新手组合
  • 指定一个组长

结对编程的好处

  • 在压力之下让人们保持更好的状态
  • 能够改善代码质量
  • 能缩短进度时间表
  • 可以指导初级程序员,以及培养集体归属感

21.3 正式检查

也叫“详查”

详查中的人员角色

  • 主持人
  • 作者
  • 评论员
  • 记录员
  • 经理

一般步骤

  • 计划(代码提交至主持人,由主持人制定计划)
  • 概述(作者花时间描述设计或者代码的技术背景)
  • 准备(评论员独立地进行详查,找出错误)
  • 详查会议(主持人挑选除作者之外的某人来阐述设计或者阅读代码,在此期间,记录员负责记录)
  • 详查报告(支持人要写出一份详查报告,列出每一个缺陷,包括它的类型和严重级别)
  • 返工(将缺陷分配给作者来修复)
  • 跟进(主持人负责监督在详查过程中分配的返工任务)
  • 第三个小时的会议(允许有兴趣参与的人在正式检查结束之后讨论解决方案)

有效详查的核对表

figC12.PNG

21.4 其他类型的协同开发实践

走查:涉及两个或者更多的人,进行设计或者代码的相关讨论。以工作会议的形式,重点在于检查错误,通常持续30到60分钟

代码阅读:直接阅读源代码并从中找出错误,同时也从质量的角度对代码做出评价,例如代码的设计、风格、可读性、可维护性以及效率。形式通常是两到三个人参与,独立阅读代码,然后与代码作者开会进行讨论。

公开演示:向客户展示软件茶品的另一种复查形式。


第22章、开发者测试

单元测试:将完成的类、子程序或者小程序在完整地系统中隔离出来测试;

组件测试:将一个类、包、小程序或者其他程序元素,从一个更加完整地系统中隔离出来进行测试

集成测试:对两个或者更多的类、包、组件或者子系统进行的联合测试,该测试一直持续到整个系统的开发完成

回归测试:重复执行以前的测试用例,以便在原先通过了相同测试集合的软件中查找缺陷

系统测试:在最终的配置下运行整个软件,以便测试安全、性能、资源消耗、时序方面的问题

本章要点

  • 开发人员测试是完整测试策略的一个关键部分。独立测试也很重要,但是这一主题超出了本书的范围;
  • 通编码之后编写测试用例相比较,编码开始之前编写测试用例,工作量和花费的时间差不多,但是后者可以缩短缺陷-侦测-调试-修正这一周期
  • 即使考虑到了各种可用的测试手段,测试仍然只是软件质量的一部分,高质量的开发方法也很重要,这包括尽可能减少需求和设计阶段的缺陷,在检测错误方面,协同开发的成效至少与测试相当,这些方法所检测错误的类型也各不相同;
  • 你可以根据不同的思路来产生很多测试的用例,包括基础测试,数据流分析、边界分析、错误数据类型以及正确数据类型等,还可以通过猜测错误的方式得到更多的测试用例
  • 错误往往集中在容易出错的类和子程序上,找出这部分代码,重新设计和编写他们
  • 测试数据本身出错的密度往往比被测代码还要高,查找这种错误完全是浪费时间,又不能对代码有所改善,要想写代码一样小心地开发测试用例
  • 自动化测试总体来说是很有用的,也是进行回归测试的基础
  • 从长远来看,改善测试过程最好办法就是将其规范化,并对其进行评估,然后用从评估中获得经验教训来改善这个过程。

22.1 开发者测试在软件质量中的角色

1、在一个典型项目中,开发者测试应该占多少时间?

  • 根据项目大小和复杂程度不同,开发者测试应该占整个项目的8%~25%;

2、怎样利用开发者测试的结果?

  • 评估正在开发的产品的可靠性
  • 指导对软件的修正
  • 有助于归纳出程序中最常见错误的类型,用于指引技术复查活动,设计未来的测试用例

22.2 推荐的开发者测试方法

1、测试先行

2、开发者测试的局限性

  • 开发者测试倾向“干净测试”(只检测代码是否工作,而不是做所有能让代码失效的测试)
  • 开发者测试对测试覆盖率有过于乐观的估计
  • 开发者测试往往会忽略一些更复杂的测试覆盖率类型

22.3 测试技巧锦囊

1、结构化的基础测试

所需基础测试用例的最少数量计算方法

  • 对子程序的直路,开始的时候记1
  • 遇到下面关键字或者其等价物时加1:if、while、repeat、for、and、or
  • 遇到一个case语句就加1,如果case语句没有缺省情况,就再加1
Statement1;
Statement2;

if (x < 10){
    statement3;
}
Statement4;

# 这个例子中有1个if,因此我们需要2个测试用例
- 由if控制的语句执行(x<10);
- 由if控制的语句不执行(x>=10)

2、数据流测试

数据的状态

  • 已定义:数据已初始化,但未使用
  • 已使用:数据已用于计算,或作为子程序调用的一个参数;
  • 已销毁:曾经定义过,但是已通过某种途径取消了对它的定义
  • 已进入:控制流已进入子程序,但会没有使用该变量
  • 已退出:在变量产生影响之前,控制接立即退出子程序

数据状态的组合

  • 已定义-已定义
  • 已定义-已退出
  • 已定义-已销毁
  • 已经入-已销毁
  • 已进入-已使用
  • 已销毁-已销毁
  • 已销毁-已使用
  • 已使用-已定义

3、等价类划分

4、猜测错误

5、边界值分析

6、几类坏数据

  • 数据太少(没有数据)
  • 太多的数据
  • 错误的数据情况(无效数据)
  • 长度错误的数据
  • 未初始化的数据

7、几类好数据

  • 正常的情形
  • 最小的正常局面
  • 最大的正常局面
  • 与旧数据的兼容性

8、采用容易手工检查的测试用例

22.4 典型错误

哪些类包含最多的错误

  • 绝大多数错误往往与少数几个具有严重缺陷的子程序有关。

错误的分类

  • 大多数的错误影响范围是相当有限
  • 许多错误发生在构建的范畴之外
  • 大多数的构建期错误是由编程人员的失误造成的
  • 笔误是一个常见的问题根源
  • 错误理解设计是常犯错误
  • 大多数错误是容易修正的

不完善的构建过程引发错误所占的比例

  • 构建缺陷至少占总缺陷的35%
  • 修正构建错误的代价虽然比修正需求和设计的错误相对低廉,但从绝对值来看仍然是高昂的

测试本身的错误

  • 对测试进行双重检查
  • 开发软件的时候就要计划好测试用例
  • 保留你的测试用例
  • 将单元测试纳入测试框架

22.5 测试支持工具

1、为测试各个类构造脚手架

2、Diff工具

3、测试数据生成器

4、覆盖率监视器

5、数据记录器/日志记录器

6、符号调试器

7、系统干扰器

8、错误数据库

22.6 改善测试过程

1、有计划的测试

2、重新测试(回归测试)

3、自动化测试

22.7 保留测试记录

  • 缺陷的管理方面描述
  • 问题的完整描述
  • 浮现错误所需要的步骤
  • 绕过该问题的建议
  • 相关的缺陷
  • 问题的严重程度
  • 缺陷根源
  • 对编码缺陷的分类:off-by-one错误、错误赋值、错误数据下标、子程序条用错误
  • 修正粗偶所改变的类和子程序
  • 缺陷所影响的代码行数
  • 查找该错误所花的小时数
  • 修正错误所花费的小时数
  • 每一个类中的缺陷数目
  • 按照同样的方式列出每个子程序的缺陷数
  • 发现一个错误平均所需要花费的测试时间
  • 每个测试用例所发现缺陷的平均数
  • 修正一个缺陷花费的平均编程时间
  • 全部测试用例的代码覆盖率
  • 在各个严重级别中未处理缺陷的数量

第23章、调试

本章要点

  • 调试同整个软件开发的成败息息相关,最好的解决之道是使用本书介绍的其他方法来避免缺陷的产生,然而,花点时间来提高自己的调试技能还是很划算的,因为优秀和拙劣的调试表现之间的差距至少是10:1;
  • 要想成功,系统化地查找和改正错误的方法至关重要,要专注于你的调试工作,让每一次测试都能让你朝着正确的方向前进一步,要使用科学的调试方法
  • 在动手解决问题之前,要理解问题的根本,胡乱猜测错误的来源和随机修改将会让你的程序陷入比刚开始调试时更为糟糕的境地
  • 将编译器的警告级别设置为最严格,把警告信息所报告的错误都改正,如果你忽略了明显的错误,那么要改正那些微妙的错误就会非常麻烦;
  • 调试工具对软件开发而言是强有力的支持手段,找出这些工具并加以应用,当然,请记得在调试的时候开动脑筋。

23.1 调试概述

1、调试在软件质量中所扮演的角色

  • 调试是诊断代码缺陷的一种方法,并不是改进代码质量的方法

2、让你有所收获的缺陷

  • 理解你正在编写的程序
  • 明确你犯了哪些类型的错误
  • 从代码阅读者的角度分析代码质量
  • 审视自己解决问题的方法
  • 审视自己修正缺陷的方法

3、一种效率低下的调试方法

  • 凭猜测找出缺陷
  • 不要把时间浪费在理解问题上
  • 用最唾手可得的方式修正错误

23.2 寻找缺陷

调试包括寻找缺陷和修正缺陷

1、科学的调试方法

  • 通过可重复的实验收集数据
  • 根据相关数据的统计构造一个假说
  • 设计一个实验来证明或反证这个假说
  • 证明或反证假说
  • 根据需要重复进行上面的步骤

2、寻找缺陷的一些小建议

  • 在构建假设时考虑所有的可用数据
  • 提炼产生错误的测试用例
  • 在自己的单源测试族中测试代码
  • 利用可用的工具
  • 采用多种不同的方法重现错误
  • 用更多的数据生成更多的假设
  • 利用否定性测试用例的结果
  • 对可能的假设尝试头脑风暴
  • 在桌上放一个记事本,把需要尝试的事情逐条列出
  • 缩小嫌疑代码的范围
  • 检查最近修改过的代码
  • 扩展嫌疑代码的范围
  • 增量式集成
  • 检查常见缺陷
  • 同其他人讨论问题
  • 抛开问题,休息一下
  • 避免蛮力调试
  • 在使用“快速肮脏调试法”的时候设置一个时间上限

3、语法错误

  • 不要过分信任编译器信息中的行号
  • 不要迷信编译器信息
  • 不要轻信编译器的第二条信息
  • 分而治之
  • 找出没有配对的注释或者引号

23.3 修正缺陷

  • 在动手之前先要理解问题
  • 理解程序本身,而不仅仅是问题
  • 验证对错误的分析
  • 放松一下
  • 保存最初的源代码
  • 治本、而不是治标
  • 修改代码时一定要有恰当的理由
  • 一次只做一个改动
  • 检查自己的改动
  • 增加能暴露问题的单元测试
  • 搜索类似缺陷

23.4 调试中的心理因素

变量之间的心理距离

单词首字母要比中间字母更能引起人的注意

figC13.PNG

23.5 调试工具--明显和不那么明显的

1、源代码比较工具

2、编译器

  • 将编译器的警告级别设置为最高级,尽可能不放过任何一个警告
  • 用对待错误的态度来处理警告
  • 在项目组范围内使用统一的编译设置

3、增强语法检查和逻辑检查

4、执行性能剖测器

5、测试框架/脚手架

6、调试器


第24章、重构

本章要点

  • 修改是程序一生都要面对的事情,不仅包括最初的开发阶段,还包括首次发布之后。
  • 在修改中软件的质量要么改进,要么恶化。软件演化的首要法则就是代码演化应当提升程序的内在质量
  • 重构成功的关键在于程序员应学会关注那些标志着代码需要重构的众多的警告或“代码臭味”。
  • 重构成功的另一要素是程序员应当掌握大量特定的重构方法。
  • 重构成功的另一要素是程序员应当掌握大量特定的重构方法
  • 重构成功的最后要点在于要有安全重构的策略。一些重构方法会比其他重构方法要好
  • 开发阶段的重构是提升程序质量的最佳时机,因为你可以立刻让刚刚产生的改变梦想变成现实。请珍惜这些开发阶段的天赐良机。

24.1 软件进化的类型

区分软件演化类型的关键:

  • 程序的质量在这一过程中是提高了还是降低了;
  • 这样的演化是源于程序构建过程中的修改还是维护过程中的修改。

24.2 重构简介

1、重构的理由

  • 代码重复
  • 冗长的子程序
  • 循环过长或嵌套过深
  • 内聚性太差的类
  • 类的接口未能提供层次一致的抽象
  • 拥有太多参数的参数列表(子程序不需要庞大的参数列表)
  • 类的内部修改往往被局限于某个部分
  • 变化导致对多个类的相同修改
  • 对继承体系的同样修改
  • 该是语句需要做相同修改(是否可以使用继承代替)
  • 同时使用的相关数据并未以类的方式进行组织
  • 成员函数使用其他类的特征比使用自身类的特征还要多
  • 过多使用基本数据类型(考虑使用类)
  • 某个类无所事事
  • 一系列传递流浪数据的子程序
  • 某个类同其他类的关系过于亲密
  • 子程序命名不恰当
  • 数据成员被设置为公用
  • 某个派生类仅使用了基类的很少一部分成员函数
  • 注释被用于解释难懂的代码
  • 使用了全局变量
  • 在子程序调用前使用了设置代码或在调用后使用了收尾代码(takedown code)
  • 程序包含的某些代码似乎在将来某个时候才会被用到。

24.3 特定的重构

1、数据级的重构

  • 用具名常量替代神秘数值(PI代替3.14)
  • 使变量的名字更为清晰且传递更多的信息
  • 将表达式内联化
  • 用函数来代替表达式
  • 引入中间变量(中间变量需要准确概括表达式的用途)
  • 用多个单一用途变量代替某个多用途变量
  • 在局部用途中使用局部变量而不是参数
  • 将基础数据类型转化为类
  • 将一组类型码转化为类或枚举类型
  • 将一组类型码转化为一个基类及其相应的派生类
  • 将数组转换为对象
  • 把群集(collection)封装起来
  • 把数据类来代替传统记录

2、语句级的重构

  • 分解布尔表达式
  • 将复杂布尔表达式转换成命名准确的布尔函数
  • 合并条件语句不同部分的重复代码片段
  • 使用break或return而不是循环控制变量
  • 在嵌套的if-then-else语句中一旦知道答案就立即返回,而不是去赋一个返回值
  • 用多态来替代条件语句(尤其是重复的case语句)
  • 创建和使用null对象而不是去检测空值

3、子程序级重构

  • 提取子程序或者方法
  • 将子程序的代码内联化
  • 将冗长的子程序转换为类
  • 将简单算法替代复杂算法
  • 增加参数
  • 删除参数
  • 将查询操作从修改操作中独立出来
  • 合并相似的子程序,通过参数区分它们的功能
  • 将行为取决于参数的子程序拆分开来
  • 传递整个对象而非特定成员
  • 传递特定成员而非整个对象
  • 包装向下转型的操作

4、类实现的重构

  • 将值对象转化为引用对象
  • 将引用对象转化为值对象
  • 用数据初始化替代虚函数
  • 改变成员函数或成员数据的位置
  • 将特殊代码提取为派生类
  • 将相似的代码结合起来放置到基类中

5、类接口的重构

  • 将成员函数放到另一个类
  • 将一个类变成两个
  • 删除类
  • 去除委托关系
  • 去掉中间人
  • 用委托代替继承
  • 引入外部的成员变量
  • 引入扩展类
  • 对暴露在外的成员变量进行封装
  • 对不能修改的类成员,删除相关的set()成员函数
  • 隐藏那些不会在类之外被用到的成员函数
  • 封装不使用的成员函数
  • 合并那些实现非常类似的基类和派生类

系统级重构

  • 为无法控制的数据创建明确的索引源
  • 将单向的类联系改为双向的类联系
  • 将双向的类联系改为单向的类联系
  • 用factory method模式而不是简单地构造函数
  • 用异常取代错误处理代码,或者做相反方向的变换

24.4 安全的重构

1、建议

  • 保存初始代码
  • 重构的步伐请小些
  • 同一时间只做一项重构
  • 把要做的事情一条条列出来
  • 设置一个停车场
  • 多使用检查点
  • 利用编译器警告信息
  • 重新测试
  • 增加测试用例
  • 检查对代码的修改
  • 根据重构风险级别来调整重构方法

2、不宜重构的情况

  • 不要把重构当成先写后改的代名词
  • 避免用重构代替重写

24.5 重构策略

  • 增加子程序时进行重构
  • 添加类时进行重构
  • 修补缺陷时进行重构
  • 关注易于出错的模块
  • 关注高度复杂的模块
  • 在维护环境下,改善你手中正在处理的代码
  • 定义清楚干净代码与拙劣代码之间的边界,然后尝试把代码移过这条边界

第25章、代码调整策略

本章要点

  • 性能只是软件质量的一个方面,通常不是最重要的,精细的代码调整也只是实现整体性能的一种方法,通常也不是决定性的。相对于代码本身的效率而言,程序的架构,细节设计以及数据结构和算法选择对程序的运行速度和资源占用的影响通常会更大。
  • 定量测量是实现性能最优化的关键。定量测量需要找出真正决定程序性能的部分,在修改之后,应当通过重复测量来明确修改是提高还是降低了软件的性能。
  • 绝大多数的程序都有那么一小部分代码耗费了绝大部分的运行时间。如果没有测量,你不会知道是哪一部分代码。
  • 代码调整需要反复尝试,这样才能获得理想的性能提高。
  • 为性能优化工作做好准备的最佳方式就是最最初阶段编写清晰的代码,从而使代码在后续工作中易于理解和修改。

25.1 性能概述

性能和代码调整

  • 程序需求
  • 程序的设计
  • 类和子程序设计(选择合适的数据类型和算法)
  • 同操作系统的交互
  • 代码编译(选择合适的编译器)
  • 硬件
  • 代码调整

从系统设计到代码调整的各个层面上,你都有可能极大的提高程序性能。

25.2 代码调整简介

代码调整并不是改进性能最为有效的方法。完善系统架构,修改类的设计,选择更好的算法常常能带来更大幅度的性能提升。

1、pareto法则:可以用20%的努力取得80%的成效。程序员们首先应该实现程序该具备的所有功能,然后再使程序臻于完美,而这时,需要精益求精的部分通常是很少的。

2、一些错误的观念

  • 高级语言中,减少代码的行数就可以提升所生成的机器代码的运行速度,或是减少其资源占用
  • 特定运算可能比其他快,代码规模也较小(编译器的版本不同可能会削弱程序性能)
  • 应随时随地进行优化
  • 程序的运行速度同其正确性同等重要(过早的优化会对软件的整体质量产生严重威胁)

3、何时调整代码

  • 程序员应使用高质量的设计,把程序编写正确,使之模块化并易于修改,将让后期的维护工作变得容易。程序正确完成后,再去检查其系统性能。除非你对需要完成的工作一清二楚,否则绝对不要对程序优化。

4、编译器优化

  • 与那些充满技巧的代码相比,编译器的优化功能对那些平铺直叙的代码更见效

25.3 蜜糖和哥斯拉

常见的低效率之源

  • 输入输出操作
  • 分页(引发操作系统交换内存页面)
  • 系统调用(调用系统子程序)
  • 解释型语言(在机器代码创建和执行之前,解释型语言必须要处理每一条程序指令)
  • 错误(类似于没有去掉调试代码,没有释放内存)

常见操作的相对效率

超越函数的计算会用去非常多的时间。多态函数调用比其他函数调用略微费时。

25.4 性能测量

测量正在调整的代码的运行时间,应该分配给程序的CPU时钟来计算,而不是日期时钟。

25.5 反复调整

尽管绝大多数的优化方法看起来都收效甚微,但累计起来,效果是惊人的。

25.6 代码调整方法总结

1、用设计良好的代码来开发软件,从而使程序易于理解和修改;

2、如果程序性能很差:

  • 保存代码的可运行版本;
  • 对系统进行分析测量,找出热点;
  • 判断性能拙劣是否源于设计,数据类型或算法上的缺陷,确定是否应该做代码调整,如果不是,请调到第一步;
  • 对上一步所确定的瓶颈代码进行调整;
  • 每次调整后对性能提升进行测量;
  • 如果调整没有改进代码的性能则恢复到可运行的版本

3、重复步骤2.


第26章、代码调整技术

本章要点

  • 优化结果在不同的语言,编译器和环境下有很大差异,如果没有对每一次的优化进行测量,你将无法判断优化到底是帮助还是损害了这个程序
  • 第一次优化通常不会是最好的,即使找到了效果不错的,也不要停下扩大战果的步伐
  • 代码调整这一话题类似于核能,富有争议性,一些人认为代码调整损害了代码的可读性和可维护性,他们绝对将其弃之不用。其他人则认为只要有适当的安全保障,代码调整对程序是有益的。如果你决定使用本章所述的调整方法,请务必谨慎行事。

26.1 逻辑

1、在知道答案后停止判断

  • 短路求值(代码一旦知道判断节后后就自动停止继续判断)

2、按照出现频率来调整判断顺序

  • 把最常见的情况放在最前面

3、用查询表替代复杂表达式

4、使用惰性求值

  • 即时完成策略,仅到工作必须完成的时候才去处理。

26.2 循环

1、将判断外提(将循环中的判断提到循环外面)

2、合并(把两个对相同一组元素进行的操作的循环合并到一起)

3、展开(一个循环被完全展开,所生成的10行代码表现出比之前更快的速度,适用于少量元素)

4、尽量减少在循环内部做的工作(例如在某频繁使用的循环中有一个复杂的指针表达式,如把复杂的指针表达式赋给一个命名准确的变量,不但可以改善代码的可读性,还能提高代码性能)

5、哨兵值(如果循环是一个查找循环,那么可以在循环的末尾使用一个哨兵值,使该循环一定能够终止)

6、把最忙的循环放在最内层

7、削减强度(多次使用轻量级的运算来代替一次代价高昂的运算,如加法换乘法)

26.3 数据变换

1、使用整性数而不是浮点数

2、数组维度尽可能少

3、尽可能减少数组引用

4、使用辅助索引

  • 字符串长度索引
  • 独立的平行的索引结构

5、使用缓存机制

  • 把某些常用的,使用频繁的值存起来

26.4 表达式

1、利用代数恒等式,用代价的操作替代复杂操作,例如sqrt(x) < sqrt(y) ,可以用x < y代替。

2、削弱运算强度

  • 加法代替乘法
  • 乘法代替幂乘
  • 移位操作代替整数乘2或除2

3、编译期初始化,例如log(2)直接用0.69314718代替

4、小心系统函数,例如math.h

5、预先算出结果

  • 在程序执行前算出结果,然后写入常量,在编译时赋值
  • 在程序执行前计算结果,然后把它们硬编码在运行时使用的变量里;
  • 在程序执行前计算结果,把结果存放在文件中,在运行时载入;
  • 在程序启动时一次性计算出全部结果,每当需要时引用;
  • 尽可能在循环开始之前计算,最大限度地减少循环内部需要做的工作
  • 在第一次需要结果时进行计算,然后将结果保存起来以备后用。

6、删除公共子表达式

26.5 子程序

将子程序重写为内联函数

26.6 用低级语言重写代码

如果程序是用C++写的,低级语言或许是汇编,如果程序是用python写的,那么低级语言可能是C。

26.7 变得越多,事情反而更没改变

代码调整无可避免地为性能改善的良好愿望而付出复杂性,可读性,简单性,可维护性方面的代价。



第六部分:系统考虑

第27章、程序规模对构建的影响

本章要点

  • 随着项目规模的扩大,交流需要加以支持,大多数方法论的关键点都在于减少交流中的问题,而一项方法论的存亡关键也应取决于它是否能促进交流。
  • 在其他条件都相等的时候,大项目的生产率会低于小项目
  • 在其他条件都相等的时候,大项目的每千行代码错误率会高于小项目
  • 在小项目里的一些看起来“理当如此”的活动在项目中必须仔细地计划,随着项目规模扩大,构建活动的主导地位逐渐降低。
  • 放大轻量级的方法论要好于缩小重量级的方法论。最有效的办法是使用“适量级”方法论。

27.1 交流和规模

人越多,交流路径就越多,交流出错的机会也就越大,因此需要采取一些组织技术来改善交流效率,常用方法是采用正式的文档,让员工阅读和撰写文档。

27.2 项目规模的范围

在大项目中(50+人)的用人数量中程序员占很大比重(>50%)

27.3 项目规模对错误的影响

在项目较小时,构建错误占错误的绝大多数,而随着项目的扩大,需求错误和架构错误的比重会变大

27.4 项目规模对生产率的影响

生产率主要取决于你所从事的软件类型、人员素质、编程语言、方法论、产品复杂度、编程环境、工具支持、计算“代码行数”、把程序员的支持工作计入“每人年的代码行数”的方法。

但总体上,小项目的生产率比大项目高出2至3倍。

27.5 项目规模对开发活动的影响

项目的大小(以代码行数计)越大,所需要的正式交流越多,所需进行的各种活动的种类也会急剧变化:

  • 需求分析
  • 系统功能设计
  • 接口设计和规格说明
  • 集成
  • 系统测试
  • 文档生成

1、程序、产品、系统和系统产品

  • 软件产品在发布前要做充分的测试,要有文档,并由其他人来维护。
  • 软件系统:一组可以结合起来工作的程序,大体上,系统的开发成本是简单程序的开发成本的3倍
  • 系统产品:既有单一产品的精致特征,又要有一套系统所需具备额成分,系统产品的开发成本大约是简单程序的9倍

2、方法论和规模

  • 在软件开发领域里,项目越正规,你不得不写的文件的数量也会越多,用于确认你已经完成了自己的工作。
  • 写文档的目的并不在于文档本身,先写计划的关键在于,能迫使你仔细考虑配置管理,并且把你的计划向每个人解释。

第28章、管理构建

本章要点

  • 好的编码实践可以通过“贯彻标准”或者“使用更为灵活的方法”来达到。
  • 配置管理,如果应用得当,会使程序员的工作变得更加轻松,特别包括变更控制。
  • 好的软件评估是一项重大挑战,成功的关键包括采用多种方法、随着项目的开展而修缮评估结果,以及很好地利用数据来创建评估等;
  • 度量是构建管理成功的关键,你可以采取措施度量项目的任何方面,而这要比根本不度量好得多,准确的度量是制定准确的进度表、质量控制和改进开发过程的关键
  • 程序员和管理人员都是人,在把他们当人看的时候工作得最好

28.1 鼓励良好的编码实践

如果项目中要有人制定标准。那么应该由一位受人尊敬的架构师来做,在软件项目中,“专家层”起的作用至少与“管理层”相同。


1、设定标准的考虑事项

  • 灵活的指导原则
  • 一些建议(而非指导原则)
  • 一组能够表现最佳实践的例子

2、鼓励良好的编码实践技术

  • 给项目的每一部分分派两个人(结对编程,导师带学生,buddy-system复审等)
  • 逐行复查代码
  • 要求代码签名
  • 安排一些好的代码示例供人参考
  • 强调代码是公有财产
  • 奖励好代码
  • 一份简单的标准:如果你是一个编程项目的管理者,你需要能阅读并理解这个项目里的所有代码

28.2 配置管理

1、什么是配置管理

  • 系统化地定义项目工件和处理变化,以使项目一直保证其完整性的时间活动。
  • 配置管理也通常被称作“软件配置管理(SCM)”.SCM关注于程序的需求,源代码,文档和测试数据。

2、需求变更和设计变更

  • 遵循某种系统化的变更控制手段
  • 成组地处理变更请求
  • 评估每项变更的成本
  • 提防大量的变更请求
  • 成立变更控制委员会或者类似机构
  • 警惕官僚主义

3、软件代码变更

配置管理的另一项内容是源代码控制

  • 版本控制软件

4、机器配置

为标准的开发工作站生成一份磁盘映像,包括全部常用的开发工具和办公软件,然后将这一映像文件加载到每位开发者的机器上。

5、备份计划

定期备份你的工作(源代码,文档,图表,笔记等)

28.3 评估“构筑”进度表

1、评估的方法

  • 建立目标(需要评估什么,为什么要评估,评估构建活动还是评估所有开发活动)
  • 为评估留出时间,并且做出计划
  • 清楚地说明软件需求
  • 在底层细节层面进行评估(考察得越细,评估结构越准确)
  • 使用若干不同的评估方法,并且比较其结果
  • 定期重新进行评估

2、评估构建的工作量

构建可以理解为详细设计、编码与调试,单元测试等。

3、项目进度的影响因素

  • 需求开发者的经验和能力
  • 程序员的经验和能力
  • 团队的动力
  • 管理的质量
  • 重用的代码数量
  • 人员流动性
  • 需求变更
  • 客户关系的质量
  • 用户对需求的参与度
  • 客户对此类应用软件的经验
  • 程序对需求开发的参与程度
  • 计算机、程序和数据的分级安全环境
  • 文档量
  • 项目目标

28.4 度量

  • 任何一种项目特征都是可以用某种方法来度量的,而且总会比根本不度量好得多
  • 留心度量的副作用(人们会倾向于集中做那些被度量的工作,而忽视未被度量的工作)
  • 反对度量就是认为最好不要去了解项目中到底在发生什么
  • 有用的软件开发的度量环节
    figC14.PNG

28.5 把程序员当人看

1、性能差异和质量差异

  • 个体差异(程序员之间有着数量级的差异)
  • 团队差异(不同的编程团队在软件质量和生产率上存在相当大的差异,好的程序员倾向于聚集在一起)

2、信仰问题

  • 编程语言
  • 缩进风格
  • 大括号的摆放位置
  • 所用的集成开发环境
  • 注释风格
  • 效率与可读性的取舍
  • 对方法的选择(scrum,极限编程,渐进交付)
  • 编程工具
  • 亮度,特别是有关于生产力的度量,比如每天编写的代码行数

以上每一项都是程序员个人风格的反映,因此需要:

  • 清楚地知道你是在处理一个敏感的问题
  • 对这些领域要使用“建议”或者“指导原则”
  • 避免流露明显的意图
  • 让程序员指定自己的标准

3、物理环境

在生产力和工作场所质量之间有着很强的相关性,给每个程序员投入基础建设投资能显著地提高生产力

  • 专属的办公空间
  • 可接受的安静的工作场所
  • 电话可以静音
  • 工作场所得到程序员赞许

28.6 管理你的管理者

要诀在于:你需要表现得使你的管理者认为他仍然在管理你,阅读卡耐基的《人性的弱点》


第29章、集成

本章要点

  • 构建的先后次序和继承的步骤会影响设计。编码、测试各类的顺序
  • 一个经过充分思考的集成顺序能减少测试的工作量,并使调试变容易
  • 增量集成有若干变型,而且——除非项目是微不足道的——任何一种形式的增量集成都比阶段式集成好
  • 针对每个特定的项目,最佳的集成步骤通常是自顶向下、自底向上、风险导向及其他集成方法的某种组合。T型集成和竖直分块集成通常都能工作得很好
  • daily bulid 能减少集成的问题,提升开发人员的士气,并提供非常有用的项目管理信息

29.1 集成方式的重要性

集成是在开发人员完成开发者测试之后才进行的,而且集成过程是与系统测试一道进行的,所有集成有时也被认为是一种测试行为。

29.2 集成频率——阶段式集成还是增量集成

1、阶段式集成

  • 设计、编码、测试、调试各个类,这一步称为“单元开发”。
  • 将这些类组合为一个庞大的系统(“系统集成”)
  • 测试并调试整个系统,这称为“系统瓦解”
  • 阶段式集成出现问题的位置的不确定性以及所有问题可能一下子出现,所以又被叫做大爆炸集成。

2、增量集成

  • 开发一个小的系统功能部件
  • 设计、编码、测试、调试某个类
  • 将这个新的类集成到系统骨架上
  • 增量式集成有助于项目增长,就像雪球从山上滚下来时那样

3、增量集成的益处

  • 易于定位错误
  • 及早在项目里取得系统级的成果
  • 改善对进度的监控
  • 改善客户关系,客户喜欢看到项目增进的迹象
  • 更加充分地测试系统中的各个单元
  • 能在更短的开发进度计划内建造出整个系统

29.3 增量集成的策略

1、自顶向下集成

  • 首先加入顶部的类,然后加入底部的类
  • 类之间的接口必须仔细定义
  • 在许多交互式系统中,顶层的位置带有主观性,有时候用户面是顶层,有时候main()是顶层
  • 除了严格的自顶向下进行集成,你也可以在各个竖直划分的功能块中自上而下地进行集成

2、自底向上集成

  • 先集成底部的类,后集成顶部的类
  • 主要问题是把重要的高层系统接口的集成工作留到最后,而系统高层可能存在概念上的设计问题

3、三明治集成

  • 首先集成体系顶部的高层业务对象类,然后集成底部的与设备接口的类和广泛使用的工具类,然后集成中间层类,类似于三明治

4、风险导向集成

  • 形式与三明治集成类似,但是动机不一样
  • 首先集成你认为最棘手的类,然后实现较容易的类
  • 经验表明:顶层的接口是有风险的,因此它通常位于风险清单的顶部。系统接口,通常位于继承体系的底层,也是有风险的,因此它也在风险清单的顶部

5、功能导向的集成

  • 首先搭好一个骨架,选择骨架的标准是它能支撑其他功能
  • 然后以一组“构成一项可确认功能”的类为单位进行集成,通常一次集成多个类

6、T型集成

  • 建造并集成系统的一个直插到底层的块,以验证架构的假设,然后建造并集成系统的挑大梁部件,为开发余下的功能提供一个框架

29.4 Daily build和冒烟测试

1、使用daily build的一些详情

  • 每日构建
  • 检查失败的bulid
  • 每天进行冒烟测试
  • 让冒烟测试与时俱进
  • 将daily build 和冒烟测试自动化
  • 成立build小组
  • 仅当有意义时,才将修订加入build中
  • 要求开发人员在把他的代码添加到系统之前,进行冒烟测试
  • 为即将添加到build的代码准备一块暂存区
  • 惩罚破坏build的人
  • 在早上发布build
  • 即使有压力,也要进行daily build 和测试冒烟

第30章、编程工具

本章要点

  • 程序员有时会在长达数年的时间里忽视某些最强大的工具,之后才发现并使用
  • 好的工具能让你的日子过得安逸得多
  • 下面这些工具已经可用了:编辑、分析代码质量、重构、版本控制、除错、测试、代码调整。
  • 你能打造许多自己专用的工具
  • 好的工具能减少软件开发中最单调乏味的工作的量,但不能消除对“编程”的需要,虽然它会持续地重塑编程的含义。

30.1 设计工具

目前的设计工具就是那些“能创建设计图标”的图形化工具

30.2 源代码工具

1、编辑

  • 集成开发环境(IDE)
  • 针对多个文件和字符串查找和替换(grep)
  • Diff Tools
  • Merge Tools
  • 源代码美化器
  • 生成接口文档的工具(JavaDoc)

2、分析代码质量

  • 语义检查器
  • 尺度报告器(程序的复杂度,代码行,数据声明行,注释行,空行的行数等)

3、重构源代码

  • 重构器
  • 结构改组工具(没有goto)
  • 代码翻译器

4、数据字典

描述项目中所有重要数据的数据库,包含其名称和描述,也可能包含关于如何使用该数据项的注意事项

30.3 可执行码工具

1、产生目标代码

  • 编译器和连接

(编译器将源代码转化为可执行码,连接器将一个或多个目标文件与生成可执行程序所有的标准代码进行链接)

  • build Tools

(使用当前版本的源文件来build出程序)

  • 程序库(一个开源的版本)
  • 代码生成向导
  • 预处理器

2、调试

  • 编译器的警告信息
  • 测试用的脚手架
  • Diff工具
  • 执行剖测器
  • 追踪监视器
  • 交互式调试器

3、测试

  • 自动测试框架
  • 自动化的测试生成器
  • 测试用例的记录和回放工具
  • 覆盖率监视器
  • 符号调试器
  • 系统扰动器(内存填充工具,内存“抖动”工具)
  • 缺陷注入工具
  • 缺陷跟踪软件

4、代码调整

  • 执行剖测器
  • 汇编代码清单和反汇编

30.4 工具导向的环境

某些环境更适合于“工具导向”的编程:UNIX

UNIX有一堆小工具:grep diff sort make line sed 等

C/C++ 语言与UNIX紧密结合

30.5 打造你自己的编程工具

1、项目特有的工具

  • 在为项目做计划时,就应该花一部分时间来思考需要哪些工具,并为制造这些工具分配时间

2、脚本

  • 脚本可以自动执行重复性杂务的工具

30.6 工具幻境

从本质上而言,编程从根本上说就是困难的,即使有好的工具支援,无论用到哪些工具,程序员都必须与凌乱的真实世界较力。

没有哪项新的工具会消灭计算机程序设计



第七部分:软件工艺

第31章、布局与风格

本章要点

  • 可视化布局的首要任务是指明代码的逻辑组织。评估该任务是否实现的标准包括准确性、一致性、易读性和易维护性;
  • 外表悦目比其他指标是最不重要的。然而,如果其他指标都达到了,代码质量好,那么布局效果看上去也会不错;
  • Visual Basic 具有纯代码块风格,而Java的传统做法就是使用纯块风格,所以若用这些语言编程,就请使用纯代码块风格,C++中,模拟纯代码块或者begin-end块边界都行之有效
  • 结构化代码有其自身的目的。始终如一的沿用某个习惯而少来创新。不能持久的布局规范只会损害其可读性
  • 布局有很多方面涉及信仰问题。应试着将客观需要和主观偏好区分开来。定出明确的指标,在此基础上再讨论风格参数的选择。

31.1 基本原则

1、格式化的基本原理

  • 好的布局凸现程序的逻辑结构

2、好布局有什么用

  • 结构能帮助高手去感知、理解和记住程序的重要特性。编程高手通常会恪守自己的风格。

3、良好布局的目标

  • 准确表现代码的逻辑结构
  • 始终如一地表现代码的逻辑结构
  • 改善可读性
  • 经得起修改

31.2 布局技术

1、空白

  • 空白包括空格、制表符、换行、空行。

2、括号

  • 对包含两个以上项的表达式应该用括号去澄清之。

31.3 布局风格

1、纯块结构

  • “If-Then”、“While”、“Swtich-case”等结构,而且有相应的结束语句。将控制结构中的语句按缩进格式组织。

2、模仿纯块结构

  • 如果所用语言不支持纯块结构,可以将关键字begin和end或者{}看成是所在控制结构的扩展
if ( pixelColor == Color_Red)  {
    statement1;
    statement2;
}

3、使用begin-end对(花括号指定块边界)

if ( pixelColor == Color_Red )
    {
    statement1;
    statement2;
    }

4、行尾布局

  • 即一大组代码缩进至行中间或结尾的布局策略。
  • 行尾缩进用来将代码块作为该块开始的关键字对其,使子程序的其余参数与第一个参数对齐。

5、哪种风格最优

  • 对于JAVA,标准操作就是使用纯块缩进结构
  • 对于C++,模仿纯块结构或者使用begin-end指定块边界都行。

31.4 控制结构的布局

1、格式化控制结构块的要点

  • 不要用未缩进的begin-end对
  • 别让begin-end两次缩进

2、其他考虑

  • 段落之间要使用空行
  • 单语句代码块的格式要前后统一
  • 对于复杂表达式,将条件分割放在几行上
  • 不用goto
  • case语句不要有行尾布局的例外

31.5 单条语句的布局

1、语句长度

  • 语句长度应该显示在80个字符以内

2、用空格使语句显得清楚

  • 使用空格会让逻辑表达式更易读
  • 使用空格让数组引用更容易读
  • 使用空格让子程序参数更容易读

3、格式化后续行

  • 使续行明显
  • 把紧密关联的元素放在一起
  • 将子程序调用后续行按标准量缩进
  • 让续行的结尾易于发现
  • 将控制语句的后续行缩进以标准的空格数
  • 不要将赋值语句按等号对齐(看做是其他语句,缩进标准量即可)

4、每行仅写一条语句

  • C++中不要让一行有多个操作

5、数据声明的布局

  • 每行只声明一个数据
  • 变量声明应尽量接近其首次使用的位置
  • 合理组织声明顺序
  • C++中,声明指针变量时请把星号*靠近变量名,或者去声明指针类型
totalBill = totalBill
    + customerPurchases[customerID]
    + CitySalesTax( customerPurchases[customerID] )
    + FootballStadiumTax()

31.6 注释的布局

  • 注释的缩进要与相应代码一致
  • 每行注释至少一个空行分开

31.7 子程序的布局

  • 用空行分隔子程序的各部分
  • 将子程序参数按标准缩进

31.8 类的布局

1、类接口的布局顺序

  • 说明类及其完整用法的头部注释
  • 构造函数与析构函数
  • public子程序
  • protected子程序
  • private子程序和数据成员

2、类实现的布局顺序

  • 描述类所在文件之内容的头部注释
  • 类数据
  • public子程序
  • protected子程序
  • pivate子程序

3、文件和程序的布局

  • 一个文件应只有一个类
  • 文件的命名英语类名有关
  • 在文件中清晰的分隔各子程序
  • 如果编辑器不能方便的查找,按字母顺序排列子程序

第32章、自说明代码

本章要点

  • 该不该注释是个需要认真对待的问题,差劲的注释只会浪费时间,帮倒忙
  • 源代码应该含有程序大部分的关键信息,只要程序依然在用,源代码比其他资料都能保持更新,故而将重要信息融入代码是很有用处的;
  • 好代码本身就是最好的说明,如果代码太糟,需要大量注释,应先试着改进代码,直至无序过多的注释为止
  • 注释应该说出代码无法说出的东西——例如概述或者用意等信息
  • 有的注释风格需要许多重复性劳动,应舍弃之,改用易于维护的注释风格。

32.1 外部文档

  • 单元开发文件夹:一种非正式的文档,其中包含供开发者在编程期间的记录
  • 详细设计文档:描述在类层或子程序层的设计决定,曾考虑过的其他方案,及其采用所用方案的理由

32.2 编程风格作文档

在代码层文档中起主要作用的因素并非注释,而是好的编程风格。包括

  • 良好的程序结构
  • 直率易懂的方法
  • 有意义的变量名和子程序名
  • 具名常量
  • 清晰的布局
  • 最低复杂度的控制流及数据结构

32.3 注释或不注释

提倡加注释,但也不能滥用注释,我们还要审查代码,这样每个人都将了解有益的注释是什么样的。

32.4 高效注释之关键

1、注释种类

  • 重复代码
  • 解释代码
  • 代码标记
  • 概述代码
  • 代码意图说明
  • 传达代码无法表述的信息(版权声明,保密要求,版本号等杂项信息)

2、高效注释

  • 采用不会打断或者抑制修改的注释风格
  • 用伪代码编程法减少注释时间
  • 将注释集成到你的开发风格中
  • 性能不是逃避注释的好借口

32.5 注释技术

1、注释单行

  • 不随意添加无关注释
  • 不要对单行代码做行尾注释
  • 不要对多行代码做行尾注释
  • 行尾注释用于数据声明
  • 避免用行尾注释存放维护标记
  • 用行尾注释标记块尾

2、注释代码段

  • 注释应表达代码的意图
  • 代码本身应尽力做好说明
  • 注释代码段应注重why而不是how
  • 用注释为后面的内容做铺垫
  • 让每一个注释都有用
  • 说明非常规做法
  • 别用缩略语
  • 将主次注释区分开(用省略号区分主次注释)
  • 错误或者语言环境独特点都要加注释
  • 给出违背良好编程风格的理由
  • 不要注释投机取巧的代码,应该重写

3、注释数据声明

  • 注释数值单位
  • 对数值的允许范围给出注释
  • 注释编码含义
  • 注释对输入数据的限制
  • 将变量有关的注释通过变量名关联起来
  • 注释全局数据

4、注释控制结构

  • 应在每个if,case,循环或者代码段前面加上注释
  • 应在每个控制结构后加上注释
  • 将循环结束处的注释看成是代码太复杂的征兆

5、注释子程序

  • 注释应该靠近其说明的代码
  • 在子程序上部用一两句话说明之
  • 在声明参数出注释这些参数
  • 利用诸如javadoc之类的代码说明工具
  • 分清楚输入和输出工具
  • 注释接口假设
  • 对子程序的局限性做注释
  • 说明子程序的全局效果
  • 记录所用算法的来源
  • 用注释标记程序的各部分

6、注释类、文件和程序

  • 说明该类的设计方法
  • 说明类的的局限性,用法假设
  • 注释类接口
  • 不要在类接口处说明实现细节
  • 说明各文件的意图和内容
  • 包含版本控制标记
  • 在注释块中包含法律通告
  • 将文件命名为与其内容相关的名字

32.6 IEEE标准

IEEE的软件工程标准


第33章、个人性格

本章要点

  • 人的个性对其编程能力有直接影响
  • 最有关系的性格为:谦虚、求知欲、诚实、创造性和纪律,以及高明的偷懒。
  • 程序员高手的性格与天分无关,而任何事都与个人发展相关;
  • 出乎意料的是,小聪明、经验、坚持和疯狂既有助也有害
  • 很多程序员不愿主动吸收新知识和技术,只依靠工作时偶尔接触新的信息。如果你能抽出少量时间阅读和学习编程知识,要不了多久就能鹤立鸡群
  • 好性格与培养正确的习惯关系甚大,要成为杰出的程序员,先要养成良好习惯,其他自然水到渠成。

33.1 个人性格是否和本书话题无关

你无法提升自己的聪明程度,但性格在一定程序上能够改进。事实证明,个人性格对于造就出程序员高手更具有决定性意义

33.2 聪明和谦虚

  • 承认自己智力有限并通过学习来弥补,你会成为更好的程序员。你越是谦虚,进步就越快;
  • 谦虚的程序员善于弥补自己的不足之处,所编写的代码让自己和他人都易看懂,其中的错误也较少

33.3 求知欲

  • 对技术事物的求知欲具有压倒一切的重要性
  • 在开发过程中建立自我意识,越了解开发过程,就越能理解变化
  • 试验,对编程和开发过程做试验,是学习编程的有效途径之一
  • 阅读解决问题的有关方法
  • 在行动之前做分析和计划
  • 学习成功项目的开发经验
  • 阅读文档
  • 同专业人士交往,参加某个交流会或者胶乳用户群,参加网上讨论。
  • 向专业开发看齐

33.4 诚实

  • 不是高手时不假装是高手
  • 乐于承认错误
  • 力图理解编译器的警告,而非弃之不理
  • 透彻理解自己的程序,而不要只是编译看看是否能够运行
  • 提供实际的状况报告
  • 提供现实的进度方案,在上司前坚持自己的意见

33.5 交流和合作

编程首先是与人交流(团队至少要能看懂你的代码),其次才是与计算机的交流

33.6 创造力和纪律

33.7 偷懒

编写某个工具来完成烦人的任务

33.8 不像你想象中那样起作用的性格

  • 经验有时并不那么重要,要具有与时俱进的态度
  • 不要当编程狂人。热情不能代替熟练的能力。

33.9 习惯

初涉某事时,就应该端正态度来学,开始做事情时,你会积极思考,轻松决定做的好坏。请确保这些习惯是你所希望的东西。


第34章、软件工艺的话题

本章要点

  • 编程的主要目的之一是管理复杂性
  • 编程过程对最终产品有深远影响
  • 合作开发要求团队成员之间进行广泛沟通,甚于同计算机的交互,而单人开发则是自我交流,其次才是计算机
  • 编程规范一旦滥用,只会雪上加霜;使用得当则能为开发环境带来良好机制,有助于管理复杂性和相互沟通。编程应基于问题域而非解决方案,这样便于复杂性管理
  • 注意警告信息,将其作为编程的疑点,因为编程几乎是纯粹的智力活动;
  • 开发时迭代次数越多,产品的质量越好
  • 墨守成规的方法有悖于高质量的软件开发,请将编程工具箱中填满各种编程工具,不断提高自己挑选合适工具的能力。

34.1 克服复杂性

致力于江都复杂度是软件开发的核心

  • 在架构层将系统划分为多个子系统,以便让思绪在某段时间内能专注于系统的一小部分;
  • 仔细定义类接口,从而可以忽略类内部的工作机理;
  • 保持类接口的抽象性,从而不必记住不必要的细节
  • 避免全局变量,因为他会大大增加总是需要兼顾代码比例
  • 避免深层次的继承,因为这样会耗费很大精力
  • 避免深度嵌套和条件判断
  • 别用goto,因为它引入了非顺序执行
  • 小心定义错误处理的方法,不要滥用不同的错误处理技术
  • 以系统的观点对待内置的异常机制,后者会成为非线性的控制结构
  • 不要让类过度膨胀
  • 子程序应该短小
  • 使用清楚、不言自明的变量名
  • 传递给子程序的参数数目应尽量少
  • 用规范和约定来使大脑从记忆不同代码段的随意性。偶然性差异中解脱出来

34.2 精选编程过程

对于多个程序员参与的项目,组织性的重要程度超过了个人技能。

34.3 为人编程,其次才是为机器

提高程序的可读性:

  • 好的类名
  • 精心的布局,短小的子程序,
  • 将复杂的逻辑测试隐藏在布尔函数中
  • 把中间结果赋给变量,以便复杂运算清晰化

34.4 以所用语言编程,但思路不受约束

不要将编程思路局限在所用语言能自动支持的范围,例如:

  • 语言不支持断言就编写自己的assert()子程序
  • 不要因为语言支持全局数据和goto就使用他们

34.5 借助规范集中注意力

规范是一套用户管理复杂度的智力工具

  • 命名规范能精确地传达重要信息
  • 规范可以使你免除各种风险
  • 增加了对底层工作的可预见性
  • 能够弥补语言的不足之处

34.6 基于问题域编程

尽可能工作于最高的抽象层次

  • 顶层代码不应充斥与文件、栈、队列、数组、字符相关的细节

将程序划分为不同层次的抽象

  • 第0层:操作系统的操作和机器指令
  • 第1层:编程语言结构与工具
  • 第2层:底层实现结构:算法和数据结构
  • 第3层:低层问题域:为用于构造解决方案的更高层的类提供框架
  • 第4层:高层问题域

问题域的底层技术

  • 在问题域使用类,来实现有实际意义的结构
  • 隐藏底层数据及其实现细节的信息
  • 使用具名常量来说明字符串和文字量的意义
  • 对中间计算结构使用中间变量
  • 用布尔函数使复杂逻辑判断更清晰

34.7 当心落石

充分利用警告信息

34.8 反复,再反复

软件设计是一个逐步精化的过程,需要经过反复修正和改进。

34.9 不要固执,分离软件与信仰


第35章、何处有更多信息

35.1 关于软件创建的信息

一些推荐的书:

  • 《pragmatic programmer》(hunt and thomas)
  • (jon bentley)
  • 《extreme programming explained:embrace change》(kent beck)
  • <writing solid code -microsoft's techniquesfor developing bug-free C software>(steve maguire)
  • 《the practice of programing》(brian kernighan & rob pike)

35.2 构建之外的话题

1、综述资料

  • 《facts and fallacies of software engineering》(Glass)
  • 《professional software development》(mcconnell)
  • 《swbok:guide to the software enegineering body of knowledge》(abran)
  • 《程序开发心理学》
  • 《software creativity》(glass)

2、软件工程综述

  • 《Software Engineering: A practitioner's approach》

35.3 期刊

1、初级程序员技术杂志

2、高阶程序员学术期刊

3、专题出版物

35.4 软件开发者的阅读计划

入门级->熟练级->专业级

推荐书籍略

35.5 参加转业组织

  • 了解编程的最好途径是与其他程序员交往
  • IEEE计算机社区
posted @ 2020-03-31 22:13  Super_orange  阅读(425)  评论(0编辑  收藏  举报