您的位置:首页 > 其它

《构建之法》读书笔记——第4章 两人合作

2016-08-12 23:10 134 查看

第4章 两人合作

4.1 代码规范

计算机只关心编译生成的机器码,你的程序采用哪种缩进风格,变量名有无统一规范等,与机器码的执行无关。但是,做一个有商业价值的项目,或者在团队里工作,代码规范相当重要。

 

“代码规范”可以分成两部分:

         1.代码风格规范。主要是文字上的规定,看似表面文章,实际上非常重要。

         2.代码设计规范。牵涉到程序设计、模块之间的关系、设计模式等方方面面的通则。

4.2 代码风格规范

代码风格的原则是:简明,易读,无二义性。

4.2.1 缩进

最好用4个空格。

4.2.2 行宽

行宽必须限制,可以限定为100字符。

4.2.3 括号

在复杂的条件表达式中,用括号清除地表示逻辑优先级。

4.2.4 断行与空白的{}行

这里作者推荐每个“{”和“}”都独占一行。

在这里,我还是采用Linux中的风格,除了函数和类。

if ( condition ) {

   DoSomething();

} else {

DoSomethingElse();

}

4.2.5 分行

不要把多条语句放在一行上。并且,不要把多个变量定义在一行上。

4.2.6 命名

详见下面大小写。

4.2.7 下划线

一般不用。

4.2.8 大小写

Pascal —— 所有单词的第一个字母都大写。

Camel —— 第一个单词全部小写,随后单词随Pascal形式。

所有的类型/类/函数名都用Pascal形式,所有的变量都用Camel形式。

函数中get/set中例外。

4.2.9 注释

不要注释程序是怎么工作的(How),程序本身就应该能说明这一问题。

注释是为了解释程序做什么(What),为什么这样做(Why),以及要特别注意的地方。

复杂的注释应该放在函数头,很多函数头的注释都用来解释参数的类型等,如果程序正文已经能够说明参数的类型in/out,就不要重复!

注释要随着程序的修改而不断更新,一个误导(Misleading)注释往往比没有注释更糟糕。

4.3 代码设计规范

代码设计规范不光是程序书写的格式问题,而且牵涉到程序设计、模块之间的关系、设计模式等方方面面。

4.3.1 函数

现代程序设计语言中的绝大部分功能,都在程序的函数(Function、Method)中实现。关于函数,最重要的原则是:只做一件事,并且要做好。

4.3.2 goto

函数最好有单一的出口,为了达到这一目的,可以使用goto。只要有助于程序逻辑的清晰体现,什么方法都可以使用,包括goto。

4.3.3 错误处理

当程序的主要功能实现后,一些程序员会乐观地估计只需要另外20%的时间,给代码加一些错误处理就大功告成了,但是这20%的工作往往需要全部项目80%的时间。

1. 参数处理

在Debug版本中,所有的参数都要验证其正确性。在正式版本中,对从外部(用户或别的模块)传递过来的参数,要验证其正确性。

2. 断言

如何验证正确性?那就要用断言(Assert)。

 

当你觉得某事肯定如何时,就可以用断言。

如果你认为某事可能会发生,这时就要写代码来处理可能发生的错误情况。

4.3.4 如何处理C++中的类

 

1. 类

         1)使用类来封装面向对象的概念和多态(Polymorphism)

         2)避免传递类型实体的值,应该用指针传递。换句话说,对于简单的数据类型,没有

必要用类来实现。

         3)对于有显式的构造和析构函数的类,不要建立全局的实体,因为你不知道它们在何

时创建和消除。

         4)仅在必要时,才使用类。

 

2.class vs. struct

如果只是数据的封装,用struct即可。

 

3. 公共/保护/私有成员(public、protected和private)

按照这样的次序来说明类中的成员:public、protected、private。

 

4. 数据成员

不要使用公共的数据成员,要用get/set函数。

 

5. 虚函数(Virtual Function)

         1)使用虚函数来实现多态(Polymorphism)。

         2)仅在很有必要时,才使用虚函数。

         3)如果一个类型要实现多态,在基类(Base Class)中的析构函数应该是虚函数。

 

6. 构造函数(Constructors)

         1)不要在构造函数中做复杂的操作,简单初始化所有数据成员即可。

         2)构造函数不应该返回错误(事实上也无法返回)。把可能出错的操作放到Initialize()

中。

 

7. 析构函数(Destructor)

         1)把所有的清理工作都放在析构函数中。如果有些资源在析构函数之前就释放了,记

住要重置这些成员为0或NULL。

         2)析构函数也不应该出错。

 

8. new和delete

         1)如果可能,实现自己的new/delete,这样可以方便地加上自己的跟踪和管理机制。

                   自己的new/delete可以包装系统提供的new/delete。

         2)检查new的返回值。new不一定都成功。(一般不会吧?)

         3)释放指针时不用检查NULL。(应该是必须检查吧?要不会出错。)

 

9. 运算符(Operators)

         1)在理想状态下,我们定义的类不需要自定义操作符。却有必要时,才会自定义操作

                   符。

         2)运算符不要做标准语义之外的任何动作。例如,“==”的判断不能改变被比较实体的

                   状态。

         3)运算符的实现必须非常有效率,如果有复杂的操作,应定义一个单独的函数。

         4)当你拿不定主意的时候,用成员函数,不要用运算符。

 

10. 异常(Exception)

         1)异常是在“异乎寻常”的情况下出现的,它的设置和处理都要花费“异乎寻常”的开销,

                   所以不要用异常作为逻辑控制来处理程序的主要流程。

         2)了解异常及处理异常的花销,在C++语言中,这是不可忽视的开销。

         3)当使用异常时,要注意在什么地方清理数据。

         4)异常不能跨过DLL或进程的边界来传递信息,所以异常不是万能的。

 

11. 类型继承(Class Inheritance)

         1)仅在必要时,才使用类型继承。

         2)用const标注只读的参数(参数指向的数据是只读的,而不是参数本身)

         3)用const标注不改变数据的函数。

4.4 代码复审

代码复审的形式

名称

形式

目的

自我复审

自己vs.自己

用同伴复审的标准来要求自己。不一定最有效,因为开发者对自己总是过于自信。如果能持之以恒,则对个人有很大好处。

同伴复审

复审者vs.开发者

简便易行。

团队复审

团队vs.开发者

有比较严格的规定和流程,适用于关键代码,以及复审后不再更新的代码。覆盖率高——有很多双眼睛盯着程序,但效率可能不高(全体人员都要到会)

 

代码复审的目的在于:

1. 找出代码的错误,比如:

         1)编码错误,比如一些碰巧骗过了编译器的错误

         2)不符合团队代码规范的地方

2. 发现逻辑错误,程序可以编译通过,但是代码的逻辑是错的

3. 发现算法错误,比如使用的算法不够优化,边界条件没有处理好等

4. 发现潜在的错误和回归性错误——当前的修改导致以前修复的缺陷又重新出现

5. 发现可能需要改进的地方

6. 教育(互相教育)开发人员,传授经验,让更多的成员熟悉项目各部分的代码,同时熟悉

         和应用领域相关的实际知识。

4.4.1 为什么要做代码复审

1. 人不可能不犯错误

2. 让新员工快速融入

4.4.2 代码复审的步骤

1. 代码必须成功地编译,在所有要求的平台上,同时要编译Debug|Retail版本。编译要用团

         队规定的最严格的编译警告等级(例如C/C++中的W4)

2. 程序员必须测试过代码。

3. 程序员必须提供新的代码,以及文件差异分析工具。

4. 复审者可以选择面对面的复审、独立复审或其他方式。

5. 在面对面的复审中,一般是开发者控制流程,讲述修改的前因后果。但是复审者有权在任

         何时候打断叙述,提出自己的意见。

6. 复审者必须逐一提供反馈意见。注意,复审者有权提出很多看似吹毛求疵的问题,复审者

         不必亲自调查每一件事,开发者有义务给出详尽的回答。

7. 开发者必须负责让所有的问题都得到满意的解释或解答,或者在TFS中创建新的工作项

         以确保这些问题会得到处理。

8. 对于复审的结果,双方必须达成一致的意见。

         1)打回去——复审发现致命问题,这些问题在解决之前不能签入代码。

         2)有条件地同意——发现了一些小问题,在这些问题得到解决或记录之后,代码可以

                   签入,不需要再次审核。

         3)放行——代码可以不加新的改动,签入源码控制服务器。

 

4.4.3 在代码复审中还要做什么

好的复审者不光是要注意到程序员修改了什么,还要把眼光放远,问一些这样的问题:

“这么修改之后,有没有别的功能会受影响”

“项目中还有别的地方需要类似的修改吗”

“有没有留下足够的说明,让将来维护代码时不会出现问题?”

“对于这样的修改,有没有别的成员需要告知?”

“导致问题的根本原因是什么?我们以后如何能自动避免这样的情况再次出现?”

有些修改看似聪明有效率,实则可能会加大以后的开发和维护的难度。

4.4.4 在代码复审后要做什么

人不能两次踏入同一条河流,程序员不能两次犯同样的错误。在代码审核后,开发者应该把复审过程中的记录整理出来:

         1.更正明显的错误。

         2.对于无法很快更正的错误,要在项目管理软件中创建Bug把它们记录下来。

         3.把所有的错误记录在自己的一个“我常犯的错误”表中,作为以后自我复审的第一步。

 

可以在代码中做标记:

//$todo(name) :

//$review(name) :

//$bug(name) :

在代码复审过程中,$review标记的问题要一一讨论,在代码复审过后,所有的$review标记要清除。在一个里程碑或正式颁布发布之前,所有的$todo和$bug标记都要清除。

4.4.5 代码复审的核查表

1. 概要部分

         1)代码符合需求和规格说明吗?

         2)代码设计是否考虑周全?

         3)代码可读性如何?

         4)代码容易维护么?

         5)代码的每一行都执行并检查过了吗?

 

2. 设计规范部分

         1)设计是否遵从已知的设计模式或项目中常用的模式?

         2)有没有硬编码或字符串/数字等存在?

         3)代码有没有依赖于某一平台,是否会影响将来的移植(如Win32到Win64)

         4)开发者新写的代码能否用已有的Library/SDK/Framework中的功能实现?在本项目中

                   是否存在类似的功能可以调用而不用全部重新实现?

         5)有没有无用的代码可以清楚?

 

3. 代码规范部分

         修改的部分符合代码标准和风格吗?

 

4. 具体代码部分

         1)有没有对错误进行处理?对于调用的外部函数,是否检查了返回值或处理了异常?

         2)参数传递有无错误,字符串的长度是字节的长度还是字符的长度,是以0开始计数

                   还是以1开始计数?

         3)边界条件是如何处理的?switch语句的default分支是如何处理的?循环有没有可能

                   出现死循环?

         4)有没有使用断言(Assert)来保证我们认为不变的条件真的得到满足?

         5)对资源的利用,是在哪里申请,在哪里释放的?有无可能存在资源泄露?有没有优

                   化的空间?

         6)数据结构中有没有用不到的元素?

 

5. 效能

         1)代码的效能(Performance)如何?最坏的情况是怎样的?

         2)代码中,特别是循环中是否有明显可优化的部分?

         3)对于系统和网络的调用是否会超时?如何处理?

 

6. 可读性

         代码可读性如何?有没有足够的注释?

 

7. 可测试性

         代码是否需要更新或创建新的单元测试?

4.5 结对编程

略。

4.5.1 最早有记录的结对编程

4.5.2 为什么要结对编程

4.5.3 不间断地复审

4.5.4 如何结对编程

4.6 两人合作的不同阶段和技巧

1. 萌芽阶段(Forming)

2. 磨合阶段(Storming)

3. 规范阶段(Norming)

4. 创造阶段(Performing)

5. 解体阶段(Deforming)

4.6.1 两人的合作——如何影响对方

4.6.2 如何正确地给予反馈

1. 最外层:行为和后果

2. 中间层:习惯和动机

3. 最内层:本质和固有属性

4.7 练习与讨论

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: