CodeReview Learning

时间:2021-07-24 13:11:49

目录

. 引言
. 代码检视的指导思想
. 代码检视的内容
. 回归测试

0. 引言

CodeReview Learning

代码检视(Code Review)是指软件开发人员在完成代码设计、编写、调试后展开的个人或群体性的代码阅读过程,代码检视的目的是发现代码中的设计问题、格式问题、逻辑问题、语法问题等,从而保证代码的高质量交付。从软件工程的角度讲,在代码检视阶段发现代码问题的成本是低廉的,所以严格认真的执行代码检视过程,是提升产品质量,降低产品维护成本的有效手段

Relevant Link:

http://www.distorage.com/code-review%E5%B0%8F%E8%AE%BA/
http://www.ibm.com/developerworks/cn/rational/11-proven-practices-for-peer-review/

1. 代码检视的指导思想

codereview主要包含以下几个观点

. 一次评审少于 – 行的代码。
. 目标为每小时低于 – LOC 的检查速率。
. 花足够的时间进行正确缓慢的评审,但是不要超过 – 分钟。
. 确定代码开发者在评审开始之前就已经注释了源代码。
. 为代码评审和获取制度建立可定量化的目标,这样您才能改进流程。
. 使用检查列表,因为它可以极大地改进代码开发者和评审者的作品。
. 确认缺陷确实得到修复了。
. 培养良好的代码评审文化氛围,在这样的氛围中搜索缺陷被看做是积极的活动。
. 警惕"老大"效应。
. 最少评审一部分代码,就是您不能评审全部的代码,以从 Ego Effect 中受益。
. 采用轻量级,能用工具支持的代码评审。

需要特别注意的是,即搜索缺陷是好的事情,而不是糟糕的,缺陷密度与开发员的能力并不是挂钩的。记住对一个团队来说,缺陷,尤其是团队成员所引入缺陷的数量不应该被回避,也不应该用作能力的评价参数。
代码检视作为一个勘误的过程,核心目的是提升代码质量,同时提升开发人员的能力,而不是*和责备开发人员,要积极引导参与人员的代码检视态度,把代码检视作为提升开发人员能力的一项活动来进行,这样才能达到代码检视的核心目的。同时,代码检视需要包含对设计的检视,更多的时候需要代码开发者讲解其设计思路

0x1: 一次评审量要低于 200–400 行代码

开发员的评审量要低于一次 200-400 行代码(LOC Line Of Code)。超过这个量,搜索缺陷的能力就会降低。以这个速度,您可以找到 70-90% 的缺陷。换句话说,如果存在 10 个缺陷,那么您可以找到其中的 7 到 9 个

CodeReview Learning

图中描述了缺陷密度与评审代码行数量之间的关系,支持该规则。缺陷密度 就是每 1000 行代码之中所发现的错误(bug)数。评审代码行的数量超过 200 时,缺陷密度就会急剧地下降。
在这种情况下,缺陷密度就是"评审有效性"的评价手段。如果两个评审员评审相同的代码,其中一个发现了更多的错误(bug),那么我们就会认为该评审员更有效率

0x2: 每小时低于 300–500 LOC 检查率的目标
快速评审并不总是好的。研究结果显示检查率低于"300-500 LOC/小时"时,可以得到优化的结果。根据所作的决定,评审者的检查率有很大的变化,就算是相似的代码开发者、评审者,文件和评审规模,也存在巨大的差异。
为了找到优化的检查率,我们将 缺陷密度 与评审者检查代码的 速度 进行了比较。得到的结果再一次落在了我们的预料之中:如果在评审您不花足够的时间,那么您就不会发现太多的缺陷。如果评审者面临大量代码的压力,那么他就不会每一行代码投入相同的注意力。他不能研究同一位置处更改的所有版本

CodeReview Learning

调查结果显示

. 每小时小于  LOC 的评审速度下,效率是相对较高的
. 每小时超过 LOC 的评审速度会降低效率

0x3: 花足够的时间进行适当缓慢的评审,但是不要超过 60-90 分钟

永远不要对一个原型代码评审超过 - 分钟

我们应该讨论,为了得到更好的结果,不应该过快地评价。但是您也不应该在一个位置花太多的时间。大约 60 分钟后,评审者就会感到疲劳,于是就不会找到额外的缺陷了。这个结论得到了许多其他研究的支持。实际上,根据我们的常识,当人们从事注意力高度集中的活动时,性能状态在 60-90 分钟之后就会降低了。考虑到人体方面的限制,评审者在性能降低之前,不能评审超过 300–600 行的代码。
但反过来说,评审代码所花的时间不得低于五分钟,就算代码只有一行也是如此。通常来说,单行的代码也会影响到整个的系统,所以花上五分钟时间去检查更改可能造成的结果是值得的。

0x4: 确定在评审开始之前代码开发者已经注释源代码了

0x5: 为代码评审创建可定量化的目标,并获取制度,这样您就可以改进流程了

有了项目,您就该决定代码评审过程的目标,以及怎样评价效率问题了。当您在定义特定的目标时,您就能够决定同行评审是否真的达到了您所需要的结果。最好从外部性的制度开始,例如

. "将支持访问降低 20%"
. "使开发引入的缺陷百分比减半"

这些信息使您能够更好地看清,从外部视角来看,代码能够做些什么,您还需要一个可定量化的评价手段,而不是"修复更多错误(bug)"的模糊目标

但是,在外部制度显示结果之前需要花上一段时间。例如

. 支持性访问将不会得到影响,直到新的版本得到发布并交到客户手中为止

0x6: 使用检查表,因为它能极大地影响代码开发者和评审者的结果

使用检测表对于评审员非常重要,如果代码开发者忘记了某项任务,评审员也同样可能忘记

使用检查表的优点有

. 使用检查表来避免可能会忘记的事情,它对代码开发者和评审者都有用。忽略是最难发现的缺陷
. 评审不在那里的东西是很困难的一件事。检查表是解决这个问题的最好方式,因为它会提醒评审者和代码开发者花点时间去考虑一下可能被遗忘的事情
. 检查表还会提醒代码开发者和评审者确定
) 所有的错误(bug)都得到了处理
) 软件功能已经通过了无效值测验
) 而且已经创建了单元测试

0x7: 确认缺陷得到了修复

使用团队协作工具,例如JIRA是进行bug追踪的一个很好的方案

0x8: 培养良好的代码评审文化氛围,在这样的氛围中可以积极地评审缺陷

与其他我们能看到的大多数技术相比,代码评审对于真实团队构建能够发挥更大的作用,但是只是在管理人员能够以一种积极的,向上的,有技巧的方式进行交流时,这种优势才能发挥出来。将缺陷看做是不好的事物很容易(毕竟,它们是代码之中的错误(bug)),但是形成不好的缺陷检查态度,则会毁掉整个团队的努力,更不要说它会破坏错误(bug)检查过程了
软件代码评审的要点在于

. 尽可能多的消除缺陷,不管是谁"导致"了错误(bug)
. 管理人员必须建立缺陷是积极的这样的观点。毕竟,每一个缺陷的存在,都是改进代码的潜在机会,而错误(bug)评审过程的目的,就在于使代码尽可能地完美。每一个被发现并解决的缺陷,都是客户以后不会看到的缺陷,也是 QA
人员不必花费时间去解决的问题
. 团队需要维持这样一种态度,就是发现缺陷,就意味着代码开发者和评审者 作为一个团队 去改进产品的质量成功了。而不是"代码开发者产生了一个缺陷,而评审者负责去发现它"。它更像是配对编程的一种有效形式。
. 评审员要向所有的开发者展示收集坏习惯,学习新技巧,并展开功能的机会。开发员可以从他们的错误(bug)中学习,但是只是在他们警惕错误(bug)时才会这样。如果开发员害怕发现错误(bug),那么积极的结果就会消失。
. 如果您是一名初级开发员,或者是一个团队的新成员,那么其他人发现缺陷时,就意味着您强有力的队友在帮助您成长为一个合格的开发员。这就比您单枪匹马地编程,没有具体的反馈时,要更快地进步

0x9: 警惕老大哥效应(Big Brother Effect)

0x10: 评审一部分的代码,就算您不能全部完成,以从自我效能感(Ego Effect)中获益

0x11: 采用轻量级,工具支持的代码评审

2. 代码检视的内容

既然有了代码检视的指导原则,那么在代码检视中,我们究竟检视些什么内容呢

. 与详细设计方案的一致性
只要将检视的代码对照详细设计进行比较就很容易检查出代码是否和详细设计一致,采用逐行逐字阅读进行比较的方法进行。 . 头文件检查
头文件检查主要关注以下方面:
) 是否包含有多余的其他头文件
) 头文件是否内聚,即是否多个模块共用一个头文件
) 头文件内的内容是否清晰,是否分类排放好并给出了足够的注释
) 是否使用了象 #ifdef __LIST_H__ 之类的宏定义保证头文件不被重复引用 . 宏定义检查
) 宏定义中有参数和表达式时,参数和表达式是否都用括号括起来了。例如:
#define ADD(a, b) (a + b) //正确的应该是 ((a) + (b))
这个定义中就没有将参数a和b括起来,如果使用时a和b是表达式的话,就会因为运算符顺序问题而出问题。
) 续行符\是否使用正确 . 常量
常量方面主要检查的主要问题如下:
) 常量是否使用了宏来进行定义
) 程序中是否存在魔鬼数字
) 16进制数据是否在前面加上了0x
) 常量是否来自规格
) 不来自规格的常量的值是否合理 . 全局变量与共享变量需要检查的主要问题如下:
) 全局变量是否必须的,是否可以改成局部变量
) 是否有全局范围内变量和局部范围内变量重名情况
) 是否有多个任务访问共享变量,是否进行了有效的保护
) 当全局变量只限于本文件内使用时,是否定义成静态的
) 多个任务读写共享变量时,是否可以将读写操作封装成独立函数,而不是在每个模块里都进行加锁解锁操作 . 静态变量和函数
静态变量和函数检视时主要问题如下:
) 静态变量的使用是否正确
) 每次使用静态变量时是否需要重新初始化
) 对不需要重新初始化的静态变量在多次使用后是否有溢出的问题。
) 文件内部使用的函数是否要定义成静态的 . 数据结构
数据结构方面考虑的主要问题如下:
) 数据结构里的成员类型定义是否正确
) 结构体里面变量顺序安排是否合理,数据是否对齐
) 是否存在冗余未用的成员变量。
) 类里面是否有私有变量和私有函数放到了公有的定义里去了 . 初始化
初始化考虑的主要问题如下:
) 变量使用前是否需要初始化
) 类的构造函数中是否对需要初始化的成员都进行了初始化
) 数组的初始化是否正确
) 内存或数组在每次使用前是否需要初始化清零
) 多个变量初始化赋值时是否存在顺序问题
) 静态变量和全局变量的初始化是否存在初始化顺序问题 . 字符串
) 字符串是否以'\0'结尾
) 字符串是否会超长
) 字符串使用的空间大小是否存在差1问题
) 使用字符串指针时,指向的位置是否存在差1问题
) 字符串指针是否可以为空,为空时会有什么现象
) 字符串内容为空(即第一个字符为'\0')时会发生什么现象
) 字符串中如果有转义字符"\"字符时,是否正确地写成了"\\"
) 在对字符串进行拷贝或连接操作时,是否对空间大小进行校验,防止出现缓冲区溢出 . 输入检查
输入校验需要检视的主要问题如下:
) 函数参数是否需要进行检查
) 从文件读取的数据是否进行校验
) 使用全局数据时是否需要进行校验
) 通信收到的数据是否需要进行校验
) 从消息中接受到的数据是否需要进行校验 . 边界条件
凡是牵涉边界条件的地方都需要进行边界检查,以下的一些问题供参考:
) 循环变量上的边界是否正确
) 变量的取值是否有边界条件限制,边界是否给出并书写正确
) 空间边界,如内存大小,数组大小是否正确,是否存在差1和越界情况
) 数据结构边界,如链表的头一条记录和最后一条记录等边界情况
) 服务器连接数量最大是多少 . 内存分配和释放
内存分配方面需要检查的有以下几点:
) 分配的大小是否正确,是否分配了过大的内存或者分配的内存大小不足,分配的内存大小是否存在差1错误
) 内存分配是否经过判断或者进行异常处理
) 重新分配一块内存时,是否将原有内存释放
) 分配的内存是否需要初始化清零
) 是否有在大循环中不断分配内存导致可能出现系统内存不足情况
释放方面需要检查的有以下几点:
) 所有的分支路径上是否将分配的内存进行了释放
) 是否将已经释放的内存重复释放
) 释放的是否是空指针
) 释放多块内存时是否存在释放的先后顺序问题
使用realloc()时要考虑以下几点:
) 新增空间是否需要初始化清零
) 是否还有指针指向老的内存块,并在realloc()后使用指向老的内存块的指针。 . 类型转换
类型转换的检查有以下问题供参考:
) 类型转换是否采用安全的转换机制
) 当采用强制转换时是否会出问题
) signed 和 unsigned 转换是否存在问题
) 是否将小空间的类型转换成了大空间的类型
) 类型转换是否会造成截断、溢出或越界 . 数组使用
数组的使用也是很容易出错的一种,不幸的是现在还没有足够好的方法能保证数组越界一类的问题得到完美的解决,所以通过对数组的检视来保证质量就很重要了,下面给出检视数组的一些建议:
) 类型是否正确
) 多维数组是否数据存放顺序正确
) 数组使用时是否会越界,空间大小是否存在差1错误
) 作用域是否正确
) 数组大小是否太大导致浪费 . 指针使用
指针在C/C++中是使用最广泛的一种语法,指针使得语言的功能强大起来,但也给程序质量带来了很大麻烦,使用指针时是极易出错的,可以说C/C++代码中的缺陷大部分都与指针有关,下面给出检视指针的一些问题参考:
) 指针是否初始化
) 指针类型定义是否正确
) 使用前是否申请了内存
) 引用是否正确,是否引用了释放掉的空间
) 指向的空间是否正确
) 是否存在使用野指针现象
) 释放后再使用时是否需要重新初始化
) 是否使用了空指针,函数指针是否为空就被调用
) 指针是否需要校验
) 指针进行类型转换时是否会引起问题
) 指针地址运算是否有误,在地址相加时是否考虑了相加的数字要乘以指针类型所占空间的大小。比如int *p; p+1相比p的大小不是大于1,而是大于一个整数所占空间的字节数 . 函数
函数方面的一些检视建议如下:
) 函数调用的参数传递是否正确
) 是否有形参和实参使用错误的问题
) 函数的返回值和输出是否需要校验
) 调用的函数是否对全局数据产生影响
) 函数功能是否单一,是否在函数里处理了多个不同的功能
) 函数参数是否需要定义为const
) 函数是否过长(一般以不超过200行为宜) . 系统和标准库调用
调用系统函数和库函数时,以下一些检视建议供参考:
) 系统调用是否正确,调用参数设置是否正确
) 是否按照标准文档中的要求和注意事项进行了调用
) 对于存在BUG的系统函数是否采取了规避措施进行调用
) 对调用系统函数是否需要在调用前进行了输入校验
) 调用后是否需要对输出进行校验 . 判断循环条件
在程序中的判断和循环条件中,也存在着一些有时通过测试难以发现的问题,主要的检视建议如下:
) 逻辑运算符是否正确,如| 和||,&和&&运算符是否搞混淆掉或键盘失误写错,
) 逻辑等号==是否误写成等号=
) 运算符顺序是否正确,运算符| 、&,||、&&,=、== 的运算顺序需要特别注意
) 循环判断中的表达式是否正确地使用了括号将运算顺序区分开,并增加可读性
) 表达式运算是否存在逻辑上的错误
) 对浮点数是否误用了精确相等进行比较
) 循环变量是否进行了初始化
) 循环的中止条件是否在某些情况下无法到达而造成死循环
) 循环的边界上是否会造成问题
) 判断条件是否会恒真或恒假 . 计算
计算错误也是程序中经常遇到的一个问题,大部分计算错误可以经过测试发现,但并不是所有的计算错误都可以很容易通过测试来发现,以下的一些问题供检视时参考:
) 计算表达式或公式是否书写正确,需要逐字符地进行确认没有输入错误
) 表达式中运算符顺序是否书写正确,同优先级运算符运算时是否存在自左至右结合或自右至左结合运算结果不同的问题
) 是否需要使用括号来保证运算顺序的正确性和增加程序的可读性
) 是否存在计算溢出情况,如两个整数相乘结果超出整数最大范围等情况
) 截断误差和舍入误差是否会引起问题,误差是否会累积下去导致误差越来越大
) 是否存在除零问题(零做分母的问题),或者两个整数相除结果得到零然后再和其他整数相乘
) 是否存在某个变量会累积增加导致长时间运行后的溢出 . 资源释放
资源释放方面的一些检视建议如下:
) 所有的资源是否都进行了释放
) 要检查是否存在某条路径遗漏了释放
) 句柄释放
3.1) 打开文件是否关闭了
3.2) 信号量是否释放
3.3) 句柄是否关闭
3.4) 锁资源是否释放
3.5) 是否存在死锁问题
) 全局的资源是否存在随时间累积增加不减少的问题
) 其他各种资源如网络socket等是否在各条对应路径上进行了关闭
) 类的析构函数中是否对类中需要释放的成员进行了释放 、效率
从空间和时间效率方面一些检视建议如下:
) 多个if判断是否可以改为switch或if…else if结构
) 多个相同处理的case语句是否归到一起
) 在多重循环中,最忙的循环是否在最内层
) 循环体内的判断语句是否可以移到循环体外

3. 回归测试

回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误
自动回归测试将大幅降低系统测试、维护升级等阶段的成本
回归测试作为软件生命周期的一个组成部分,在整个软件测试过程中占有很大的工作量比重,软件开发的各个阶段都会进行多次回归测试。在渐进和快速迭代开发中,新版本的连续发布使回归测试进行的更加频繁,而在极端编程方法中,更是要求每天都进行若干次回归测试。因此,通过选择正确的回归测试策略来改进回归测试的效率和有效性是非常有意义的

在实际的产品研发和回归测试中,可以选择一个时间段之后集中进行一次回归测试,针对和上次相比变化的功能点进行回归重点回测测试

Relevant Link:

http://baike.baidu.com/view/106720.htm?fr=aladdin

Copyright (c) 2014 LittleHann All rights reserved