《代码大全第二版》学习笔记(三)

第五部分 代码改善

第二十章 软件质量概述

20.1 软件质量的特性

1.         正确性:系统规范、设计和实现方面的错误的稀少程度。

2.         可用性:用户学习和使用一个系统的容易程度。

3.         效率:软件是否尽可能少地占用系统资源,包括内存和执行时间。

4.         可靠性:在指定的必需条件下,一个系统完成所需要功能的能力——应该有很长的平均无故障时间。

5.         完整性:系统阻止对程序或者数据进行未经验证或者不正确访问的能力。

6.         适应性:为特定的应用或者环境设计的系统,在不做修改的情况下,能够在其他应用或者环境中使用的范围。

7.         精确性:输出结果的误差程度。

8.         健壮性:系统在接收无效输入或者处于压力环境时继续正常运行的能力。

软件内在的质量特性:

1.         可维护性:是否能够很容易对系统进行修改、改变或者增加功能、提高性能,以及修正缺陷。

2.         灵活性:假如一个系统是为特定用途或者环境设计的,那么当该系统被用于其他目的或者环境的时候,需要对该系统做修改的程度。

3.         可移植性:为了在原来设计的特定环境之外运行,对系统所进行修改的难易程度。

4.         可重用性:系统的某些部分可被应用到其他系统中的程度,以及此项工作的难易程度。

5.         可读性:阅读并理解代码的难易程度,尤其在细节语句层次上。

6.         可测试性:可进行何种程度的单元测试或系统测试。

7.         可理解性:在系统组织和细节语句的层次上理解整个系统的难易程度。

20.3 不同质量保障技术的相对效能

1.         对所有的需求、架构以及系统关键部分的设计进行正式检查。

2.         建模或者创建原型。

3.         代码阅读或者检查。

4.         执行测试

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

错误越早引入到软件当中,问题就会越复杂。

第二十一章 协同构建

21.1 协同开发实践概要

工作中开发人员总会对某些错误点视而不见,而其他人不会有相同的盲点,所以开发人员让其他人来检查自己的工作是很有好处的。

21.2 结对编程

1.         用编码规范来支持结对编程。

2.         不要让结对编程编程旁观。

3.         不要强迫在简单的问题上使用结对编程。

4.         有规律地对结对人员和分配的工作任务进行轮换。

5.         鼓励双方跟上对方的步伐。

6.         确认两个人都能够看到显示器。

7.         不要强迫程序员与自己关系紧张的人组对。

8.         避免新手组合。

9.         指定一个组长。

21.3 正式检查

进行详查的目的是发现设计或者代码中的缺陷,而不是探索替代方案,或者争论谁对谁错,其目的绝不应该是批评作者的设计或者代码。

第二十二章 开发者测试

1.         单元测试Unit testing

2.         组件测试Component testing

3.         集成测试Integration testing

22.2 开发者测试的推荐方法

1.         对每一项相关的需求进行测试,以确保需求都已经被实现。

2.         对每一个相关的设计关注点进行测试,以确保设计已经被实现。

3.         用基础测试basis testing来扩充针对需求和设计的详细测试用例。增加数据流测试data-flow test,然后补充其他所需的测试用例。

4.         使用一个检查表,其中记录着你在本项目迄今为止所犯的,以及在过去的项目中所犯的错误类型。

在设计产品的时候设计测试用例,这样可以帮助避免在需求和设计中产生错误,修正这些错误的代价往往比修正编码错误更昂贵。尽可能早地对测试进行规划并找出缺陷。

测试先行还是测试后行

1.         在开始写代码之前先写测试用例,并不比之后再写要多花功夫,只是调整了下测试用例编写活动的工作顺序而已。

2.         加入你首先编写测试用例,那么你将可以更早发现缺陷,同时也更容易修正它们。

3.         首先编写测试用例,将迫使你在开始写代码之前至少思考一下需求和设计,而这往往会催生更高质量的代码。

4.         在编写代码之前先编写测试用例,能更早地把需求上的问题暴露出来。

5.         如果你保存了最初编写的测试用例——这是你应该做的,那么先进行测试并非唯一选择,你仍然可以最后再进行测试。

于是,测试先行。

22.4 典型错误

三种最为常见错误的源头:缺乏应用领域知识、频繁变动且相互矛盾的需求,沟通和协调的失效。

第二十三章 调试

23.2 寻找缺陷

科学的调试方法

1.         将错误状态稳定下来。

2.         确定错误的来源

a)         收集产生缺陷的相关数据。

b)         分析所收集的数据,并构造对缺陷的假设。

c)         确定怎样去证实或证伪这个假设,可以对程序进行测试或是通过检查代码。

d)         按照2c确定的方法对假设做出最终结论。

3.         修补缺陷。

4.         对所修补的地方进行测试。

5.         查找是否还有类似错误。

寻找缺陷的一些小建议

1.         在构造假设时考虑所有的可用数据。

2.         提炼产生错误的测试用例。

3.         在自己的单元测试族unit test suite中测试代码。

4.         利用可用的工具。

5.         采用多种不同的方法重现错误。

6.         用更多的数据生成更多的假设。

7.         利用否定性测试用例的结果。

8.         对可能的假设尝试头脑风暴。

9.         在桌上放一个笔记本,把需要尝试的事情逐条列出。

10.     缩小嫌疑代码的范围。

11.     对之前出现过缺陷的类和子程序保持警惕。

12.     检查最近修改过的代码。

13.     扩展嫌疑代码的范围。

14.     增量式继承。

15.     检查常见缺陷。

16.     同其他人讨论问题。

17.     抛开问题,休息一下。

语法错误

1.         不要过分信任编译器信息中的行号。

2.         不要迷信编译器信息。

3.         不要轻信编译器的第二条信息。

23.3 修正缺陷

1.         在动手之前先要理解问题。

2.         理解程序本身,而不仅仅是问题。

3.         验证对错误的分析。

4.         放松一下。

5.         保留最初的源代码。

6.         治本,而不是治标。

7.         修改代码时一定要有恰当的理由。

8.         一次只做一个改动。

9.         检查自己的改动。

10.     增加能暴露问题的单元测试。

11.     搜索类似的缺陷。

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

1.         源代码比较工具 Source-Code Comparators

2.         编译器的警告消息 Compiler Warning Messages

3.         增强的语法检查和逻辑检查 Extended Syntax and Logic Checking

4.         执行性能剖测器 Execution Profilers

5.         测试框架/脚手架 Test Frameworks/Scaffolding

6.         调试器 Debuggers

第二十四章 重构

24.2 重构简介

重构的理由

1.         代码重复。

2.         冗长的子程序。

3.         循环过长或嵌套过深。

4.         内聚性太差的类。

5.         类的接口未能提供层次一致的抽象。

6.         拥有太多参数的参数列表。

7.         类的内部修改往往被局限于某个部分。

8.         变化导致对多个类的相同修改。

9.         对继承体系的同样修改。

10.     case语句需要做相同的修改。

11.     同时使用的相关数据并未以类的方式进行组织。

12.     成员函数使用其他类的特征比使用自身类的特征还要多。

13.     过多使用基本数据类型。

14.     某个类无所事事。

15.     一系列传递流浪数据的子程序。

16.     中间人对象无事可做。

17.     某个类同其他类关系过于亲密。

18.     子程序命名不恰当。

19.     数据成员被设置为公用。

20.     某个派生类仅使用了基类的很少一部分成员函数。

21.     注释被用于解释难懂的代码。

22.     使用了全局变量。

23.     在子程序调用前使用了设置代码,或在调用后使用了收尾代码。

24.     程序中的一些代码似乎是在将来的某个时候才会用到的。

24.3 特定的重构

数量级的重构Data-Level Refactorings

1.         用具名常量代替神秘数值。

2.         使变量的名字更为清晰且传递更多信息。

3.         将表达式内联化。把与一个中间变量换成给它赋值的那个表达式本身。

4.         用函数来代替表达式。

5.         引入中间变量。中间变量的命名应能准确概括表达式的用途。

6.         用多个单一用途变量代替某个多用途变量。

7.         在局部用途中使用局部变量而不是参数。

8.         将基础数据类型转化为类。

9.         将一组类型码type codes转化为类或枚举类型。

10.     将一组类型码转换为一个基类及其相应派生类。

11.     将数组转换为对象。

12.     把群集collection封装起来。

13.     用数据类来替代传统记录。

语句级的重构

1.         分解布尔表达式。引入命名准确的中间变量。

2.         将复杂布尔表达式转换成命名准确的布尔函数。

3.         合并条件语句不同部分中的重复代码片段。

4.         使用breakreturn而不是循环控制变量。

5.         在嵌套的if-then-else语句中一旦知道答案就立即返回,而不是去赋一个返回值。

6.         用多态来替代条件语句(尤其是重复的case语句)。

7.         创建和使用null对象而不是去检测空值。

子程序级重构

1.         提取子程序或者方法。

2.         将子程序的代码内联化。

3.         将冗长的子程序转换为类。

4.         用简单算法替代复杂算法。

5.         增加参数。

6.         删除参数。

7.         将查询操作从修改操作中独立出来。

8.         合并相似的子程序,通过参数区分它们的功能。

9.         将行为取决于参数的子程序拆分开来。

10.     传递整个对象而非特定成员。如果发现有同一对象的多个值被传递给了一个子程序。

11.     传递特定成员而非整个对象。如果发现创建对象的唯一理由只是你需要将它传入某个子程序。

12.     包装向下转型的操作。档子程序返回一个对象时,应当返回其已知的最精确的对象类型。

类实现的重构

1.         将值对象转化为引用对象。针对大型对象。

2.         将饮用对象转化为值对象。针对小型对象。

3.         用数据初始化替代虚函数。如果一组派生类,差别仅仅是虚函数返回的常量不同。

4.         改变成员函数或成员数据的位置。考虑对类的继承体系做出修改。

减少派生类的重复工作:

a)         将子程序上移到基类中。

b)         将成员上移到基类中。

c)         将构造函数中的部分代码上移到基类中。

反之,则是对派生类进行特殊化。

5.         将特殊代码提取为派生类。

6.         将相似的代码结合起来放置到基类中。

类接口的重构

1.         将成员函数放到另一个类中。

2.         将一个类变成两个。

3.         删除类。

4.         去除委托关系。

5.         去掉中间人。

6.         用委托代替继承。如果某类需要用到另一个类,但有打算获取对该类接口更多的控制权,可以让基类成为原派生类的一个成员,并公开它的一组成员函数,以完成一种内聚的抽象。

7.         用继承代替委托。如果某个类公开了委托类(成员类)所有的成员函数,就改成继承。

8.         引入外部的成员函数。如果一个客户类需要被调用类的某个额外的成员函数,而你又无法去修改被调用类,那么可以通过在客户类中创建新成员函数的方式来提供此功能。

9.         引入扩展类。如果一个类需要多个额外的成员函数,你同样无法修改该类,可以创建一个新类,包括原类的功能以及新增功能。

10.     对暴露在外的成员变量进行封装。public改为private

11.     对于不能修改的类成员,删除相关的set()成员函数。

12.     隐藏那些不会再类之外被用到的成员函数。

13.     封装不使用的成员函数。

14.     合并那些实现非常类似的基类和派生类。

系统级重构

1.         为无法控制的数据创建明确的索引源。

2.         将单向的类联系改为双向的类联系。各自需要用到对方的功能。

3.         将双向的类联系改为单向的类联系。只有一个类需要访问另一个类。

4.         Factory Method模式而不是简单地构造函数。在需要基于类型码创建对象,或者希望使用引用对象而非值对象的时候,应当使用factory Method

5.         用异常取代错误处理代码,或者做相反方向的变换。

24.4 安全的重构

1.         保存初始代码。

2.         重构的步伐请小些。

3.         同一时间只做一项重构。

4.         把要做的事情一条条列出来。

5.         设置一个停车场。现在不急将来要做的事情。

6.         多使用检查点。

7.         利用编译器警告信息。

8.         重新测试。

9.         增加测试用例。

10.     检查对代码的修改。

11.     根据重构风险级别来调整重构方法。

不宜重构的情况

1.         不要把重构当做先写后改的代名词。

2.         避免用重构代替重写。

24.5 重构策略

1.         在增加子程序时进行重构。

2.         在添加类的时候进行重构。

3.         在修补缺陷的时候进行重构。

4.         关注易于出错的模块。

5.         关注高度复杂的模块。

6.         在维护环境下,改善你手中正在处理的代码。

7.         定义清楚干净代码和拙劣代码之间的边界,然后尝试把代码移过这条边界。

第二十五章 代码调整策略

25.1 性能概述

对用户来说,程序员按时交付软件,提供一个清爽的用户界面,避免系统死机常常比程序的性能更为重要。

性能和代码调整

1.         程序需求。在花费时间处理一个性能问题之前,想清楚你的确是在解决一个确实需要解决的问题。

2.         程序的设计。如果程序的资源占用量和速度至关主要,设计时就应该优先考虑整体性能。

3.         类和子程序设计。合适的数据类型和算法。

4.         同操作系统的交互。

5.         代码编译。选择优秀的编译器。

6.         硬件。

7.         代码调整。Tuning

25.2 代码调整简介

但是,高效的代码不一定就是“更好”的代码。

完美是优良之大敌。越是追求完美,越有可能完不成任务。程序员们首先应该实现程序应该具备的所有功能,然后再使程序臻于完美。

一些无稽之谈

1.         在高级语言中,减少代码的行数就可以提升所生成的机器代码的运行速度,或是减少其资源占用——错误!

2.         特定运算可能比其他的快,代码规模也较小——错误!

3.         应当随时随地进行优化——错误!

4.         程序的运行速度同其正确性同等重要——错误!

何时调整代码

程序员应当使用高质量的设计,把程序编写正确。使之模块化并易于修改,将让后期的维护工作变得很容易。在程序已经完成并正确之后,再去检查系统的性能。

除非你对需要完成的工作一清二楚,否则绝不要对程序做优化。

25.3 蜜糖和哥斯拉

常见的低效率之源

1.         输入/输出操作。如果你可以选择在内存中处理文件,就不要费力通过磁盘、数据库、或是跨越网络访问相同的文件。除非程序对空间占用非常敏感,否则数据都应放在内存里面。

2.         分页。引发操作系统交换内存页面的运算会比在内存同一页中进行的运算慢许多。

3.         系统调用。编写自己的服务程序/避免进入系统。

4.         解释型语言。

5.         错误。比如,没有去掉调试代码(例如继续吧调试信息记录到文件中去)、忘了释放内存、数据库表设计失误、轮询并不存在的设备直至超时,等等。

常见操作的相对效率

绝大部分常用操作所耗费的时间相差无几——成员函数调用、赋值、整型运算和浮点运算等等之间都差不多。超越函数(浮点方根、浮点正弦、浮点对数、浮点指数)的计算则会用去非常多的时间。多态函数调用比其他函数调用略微费时。

25.4 性能测量

底层往往已经做过优化了。如果你认为没有必要通过测量来证实哪种方法更为有效,那么同样也没有必要牺牲代码的可读性,而把赌注押在性能的提高上。

性能测量应使用专门的性能剖析工具。

25.6 代码调整方法总结

第二十六章 代码调整技术

26.1 逻辑

1.         在知道答案后停止判断。

2.         按照出现的频率来调整判断顺序。

3.         用查询表替代复杂表达式。

4.         使用惰性求值。避免做任何事情,直到迫不得已。

26.2 循环

1.         将判断外提。把循环放在条件语句内。

2.         合并。把两个对相同一组元素进行操作的循环合并在一起。

3.         展开。如,把循环10次的循环展开为10行代码。

4.         尽可能减少在循环内部做的工作。

5.         哨兵值。当循环的判断条件是一个复合判断的时候,可以通过简化判断来节省代码运行时间。如果该循环是一个查找循环,简化方法之一就是使用一个哨兵值,可以把它放在循环范围的末尾,从而保证循环一定能够中止。

6.         把最忙的循环放在最内层。

7.         削减强度。用多次轻量级运算(例如加法)来代替一次代价高昂的运算(例如乘法)。

26.3 数据变换

1.         使用整型数而不是浮点数。

2.         数组维度尽可能少。

3.         尽可能减少数组引用。

4.         使用辅助索引。如,字符串长度索引,独立的平行的索引结构(当每一个数据条目都很大的时候,可以创建一个辅助结构,里面存放关键码和指向详细信息的指针。可以把关键码条目存放在内存里,而把数据存放在外部。这使所有的查找和排序都可以在内存里完成,知道了所需访问条目的具体地址之后,进行一次磁盘访问就够了)。

5.         使用缓存机制。把某些值存起来,使得最常用的值会比不太常用的值更容易被获取。

26.4 表达式

1.         利用代数恒等式。

2.         削弱运算强度。如:

a)         用加法代替乘法。

b)         用乘法代替幂乘。

c)         利用三角恒等式代换等价的三角函数。

d)         longint来代替longlong整数。

e)         用定点数或整型数代替浮点数。

f)          用单精度数代替双精度数。

g)         用移位操作代替整数乘2或除2

3.         编译期初始化。如果在一个子程序调用中使用了一个具名常量或是神秘数值,而且它是子程序唯一的参数,这就是一种暗示,你应当提前计算这一数值,把它放到某个常量中,避免上面那种子程序调用。

4.         小心系统函数。系统函数运行起来很慢,提供的精度常常也是根本不需要的。

C++示例:使用右移运算的以2为底的对数函数

unsigned int Log2(unsigned int x) {
         unsigned int i = 0;
         while ( ( x = ( x >> 1) ) ! = 0 ) {
                   i++;
         }
         return i;
}

5.         使用正确的常量类型。所使用的具名常量和应该同被赋值的相应变量具有相同的类型。如果不同,编译器发生类型转换,花时间。

6.         预先算出结果。如:

a)         在程序执行之前算出结果,然后把结果写入常量,在编译时赋值。

b)         在程序执行之前计算结果,然后把它们硬编码在运行时使用的变量里。

c)         在程序执行之前计算结果,把结果存放在文件中,在运行时载入。

d)         在程序启动时一次性计算出全部结果,每当需要时去引用。

e)         尽可能在循环开始之前计算,最大限度地减少循环内部需要做的工作。

f)          在第一次需要结果时计算,然后将结果保存起来以备后用。

7.         删除公共子表达式。如果发现某个表达式老是在你面前出现,就把它赋给一个变量,然后在需要的地方引用该变量,而非重新计算这个表达式。

26.5 子程序

1.         将子程序重写为内联。

26.6 用低级语言重写代码

简单有效的汇编重编码方法,即启用一个能顺带输出汇编代码列表的编译器。把需要调整子程序的汇编代码提取出来,保存到单独的源文件中。将这段汇编代码作为优化工作的基础,手动调整代码。

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

计算机发展,本章讨论的性能优化提升的意义已如明日黄花。

未经测量的代码优化对性能上的改善充其量是一次投机,然而,其对可读性等产生的负面影响则确凿无疑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值