第10章
代码规范与代码复审
在第9章中,同学们完成了WC程序,经过评比,九条的程序获得了第一名。这时,阿超说,现代软件产业经过几十年的发展,已经不可能出现一个人单枪匹马完成一个软件的事情了,软件都是在相互合作中完成的。阿超建议大家互相看看别人的代码,在TFS中每个人都把各自项目的权限放宽,允许别人访问,交流一下意见。
两个小时后,小飞来抱怨说,九条的代码都是一行到底,随意缩进,跟他提了意见,他还说“编译通过就行了”。
他们找到了九条。
九条:我打麻将的时候牌都是乱摆,赢的也不少呀。
阿超:为什么要乱摆?
九条:因为怕围观的人看清我的牌路,我自己清楚就行了。
上课了。阿超问大家,我们写的代码是给人看的,还是给机器看的?
杂曰:人也看,机器也看。
阿超:对,但是最终是人在看。而且和打麻将不同,我们的代码要让“旁观者”看得清清楚楚。请看下一段代码,如代码清单10-1所示,如果你接手这样的代码,有什么感想?
代码清单10-1 badly formatted code – big C[1]
#include "stdafx.h"
#include "stdio.h"
void test();
int _tmain
(int argc,
_TCHAR* argv[])
{ test(); return
0; }
char C[25][40];void d(int x,int y)
{C[x][y]=C[x][y+1]=32;}int f(int x){return (int)x*x*.08;}
void test(){int i,j; char s[5]="TEST";
for(i=0;i<25;i++)
for(j=0;j<40;j++)
C[i][j]=s[(i+j)%4];
for(i=1;i<=7;i++)
{d(18-i,12);
C[20-f(i)][i+19]=
C[20-f(i)][20-i]=32;
}d(10,13);d(9,13);
d(8,14);d(7,15);
d(6,16);d(5,18);d(5,20); d(5,22);d(5,26);
d(6,23);d(6,25);d(7,25);for(i=0;i<25;i++,printf("\n"))
for(j=0;j<40;printf("%c",C[i][j++]));}
同学们纷纷发言,基本上有如下的反应:
(1)Faint!!
(2)重写程序!!
(3)找到原作者,暴打一顿!!!
(4)让此人从公司辞职!!!!
计算机只关心编译后的机器码,你的程序是什么样的缩进风格,以及变量名是否有统一的规范等和机器码的执行无关。但是,做一个有商业价值的项目,或者在团队里工作,代码规范相当重要。
我们讲的“代码规范”可以分成两个部分。
(1)代码风格规范。主要是文字上的规定,看似表面文章,实际上非常重要。
(2)代码设计规范。牵涉到程序设计、模块之间的关系、设计模式等方方面面,这里有不少与具体程序设计语言息息相关的内容(如C/C++/Java/C#),但是也有通用的原则,这里主要讨论通用的原则。
10.1 代码风格规范
代码风格的原则是:简明,易读,无二义性。
提示:这是移山公司的一家之言,如果碰到争执,关键是要本着“保持简明,让代码更容易读”的原则,看看如下争执中的代码规范是否能够让程序员们更好地理解和维护程序。
10.1.1 缩进
是用Tab键好,还是2、4、8个空格?
结论:4个空格,在VS2005和其他的一些编辑工具中都可以定义Tab键扩展成为几个空格键。不用 Tab键的理由是Tab键在不同的情况下会显示不同的长度。4个空格的距离从可读性来说正好。
10.1.2 行宽
行宽必须限制,但是以前有些文档规定的80字符行宽太小了(以前的计算机/打字机显示行宽为80字符),现在时代不同了,可为100字符。
10.1.3 括号
在复杂的条件表达式中,用括号清楚地表示逻辑优先级。
10.1.4 断行与空白的{ }行
程序的结构是什么风格?下面有几种格式,我们一一讨论。
最精简的格式A:
if (condition) DoSomething();
else DoSomethingElse();
有人喜欢这样,因为可以节省几行,但是不同的语句(Statement)放在一行中,会使程序调试(DeBug)非常不方便,如果要一步一步观察condition(condition有可能是包含函数调用的复杂表达式)中各个变量的变化情况,单步执行就很难了。
因此,我们还是要有断行,这样可以得到如下的结构——格式B:
if (condition)
DoSomething();
else
DoSomethingElse();
这样的结构,由于没有明确的“{”和“}”来判断程序的结构,在有多层控制嵌套的时候,就不容易看清结构和对应关系。下面的改进(格式C)虽好,但是阿超认为还是不够清晰:
if ( condition) {
DoSomething();
} else {
DoSomethingElse();
}
于是我们最后做了这个选择,每个“{”和“}”都独占一行。就是格式D:
if ( condition)
{
DoSomething();
}
else
{
DoSomethingElse();
}
10.1.5 分行
不要把多行语句放在一行上。
a = 1; b = 2; // bogus
if (fFoo) Bar(); // bogus
更严格地说,不要把不同的变量定义在一行上。
Foo foo1, foo2; // bogus
10.1.6 命名
阿超:我在某个同学的程序中看到有些变量叫“lili”,“yunyun”,不知道这些变量在现实生活中有没有什么意义。
下面哄笑起来。
果冻:(红着脸问)那有些变量的确想不出名字,简单的变量像i、j、k都用完了,怎么办?
阿超:当我们的程序比“Hello World”复杂10倍以上的时候,像给变量命名这样简单的事看起来也不那么简单了。我们就来谈谈如何起名字这个问题。程序中的实体、变量是程序员昼思夜想的对象,要起一个好的名字才行。大家都知道用单个字母给有复杂语义的实体命名是不好的,目前最通用的,也是经过了实践检验的方法叫“匈牙利命名法”。例如:
fFileExist,表明是一个bool值,表示文件是否存在;
szPath,表明是一个以0结束的字符串,表示一个路径。
如此命名的目的是让程序员一眼就能看出变量的类型,避免在使用中出错。早期的计算机语言(如BCPL)不作类型检查,在C语言中,int、byte、char、bool大概都是一回事。下面这一句话:
if (i)
从语义来说,i可以是表示真/假的一个值,也可以表示长度是否为零,
还可以表示是否到了字符串的结束位置,或者可以表示两个字符串比较的结果不相等(strcmp()返回-1,0,1)。从程序的文字上,很难看出确切的语义。
同样是字符串类型,char *,BSTR的有些行为是很不一样的。
HRESULT的值也可以用来表示真假,但是HR_TRUE == 0,HR_FALSE ==1,这和通常的true/false刚好相反。
大部分的程序,错就错在这些地方!在变量面前加上有意义的前缀,就可以让程序员一眼看出变量的类型及相应的语义。这就是“匈牙利命名法”的用处。
匈牙利命名法的一些通用规定,见本书附录B(第337页)。
还有一些地方不适合用“匈牙利命名法”,比如,在一些强类型的语言(如C#)中,不同类型的值是不能做运算的,对类型有严格的要求,例如C# 中,if()语句只能接受bool值的表达式,这样就大大地防止了以上问题的发生。在这样的语言中,前缀就不是很必要的,匈牙利命名法则不适用了。Microsoft .Net Framework就不主张用这样的法则。
10.1.7 下划线问题
下划线用来分隔变量名字中的作用域标注和变量的语义,如:一个类型的成员变量通常用m_来表示。移山公司规定下划线一般不用在其他方面。
10.1.8 大小写问题
由多个单词组成的变量名,如果全部都是小写,很不易读,一个简单的解决方案就是用大小写区分它们。
Pascal——所有单词的第一个字母都大写;
Camel——第一个单词全部小写,随后单词随Pascal格式,这种方式也叫lowerCamel。
一个通用的做法是:所有的类型/类/函数名都用Pascal形式,所有的变量都用Camel形式。
类/类型/变量:名词或组合名词,如Member、ProductInfo等。
函数则用动词或动宾组合词来表示,如get/set; RenderPage()。
10.1.9 注释
谁不会写注释?但是,需要注释什么?
不要注释程序是怎么工作的(How),你的程序本身就应该能说明这一问题。
//this loop starts the i from 0 to len, in each step, it
// does SomeThing
for (i = 0; i<len; i++)
{
DoSomeThing();
}
以上的注释是多余的。
注释是用来解释程序做什么(What),为什么这样做(Why),以及要特别注意的地方的,如下:
//go thru the array, note the last element is at [len-1]
for (i = 0; i<len; i++)
{
DoSomeThing();
}
复杂的注释应该放在函数头,很多函数头的注释都是解释参数的类型等的,如果程序正文已经能够说明参数的类型in/out等,就不要重复!
注释也要随着程序的修改而不断更新,一个误导的(Misleading)注释往往比没有注释更糟糕。
另外,注释(包括所有源代码)应只用ASCII字符,不要用中文或其他特殊字符,它们会极大地影响程序的可移植性。
在现代编程环境中,程序编辑器可以设置各种好看的字体,我们可以使用不同的显示风格来表示程序的不同部分。
注意: 有些程序设计语言的教科书对于基本的语法有详细的注释, 那是为了教学的目的, 不宜在正式项目中也这么做。
下面的例子中,有很多注释, 但是那些注释是非常必要的呢? 哪些错误处理是多余的?
Protected Sub txtSSN_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtSSN.TextChanged
Try ‘Provides error trapping
'_______________________________________________________________________ '
' txtSSN_TextChanged
‘ Activated by entering SSN.
' Transfer form value to local class variable. '_______________________________________________________________________ '
' anOrder.SSN
' Holds the SSN for processing in all forms ' txtSSN ' Form object that holds user entered SSN ' '_______________________________________________________________________ '
anOrder.SSN = txtSSN.Text
Catch ex As Exception ' Error trapping. '_______________________________________________________________________ ‘
' Output system error message to user on form under form title and
' send details to database
'_______________________________________________________________________ ' subErrorReporting("txtSSN_TextChanged", ex.Message)
End Try
End Sub
10.2 代码设计规范
代码设计规范不光是程序书写的格式问题,而且牵涉到程序设计、模块之间的关系、设计模式等方方面面,这里有不少与具体程序设计语言息息相关的内容(如C、C++、Java、C#),但是也有通用的原则,这里主要讨论通用的原则。如果你只想为了“爽”而写程序,那么可以忽略下面的原则;如果你写的程序会被很多人使用,并且你自己会加班Debug 你的程序,那最好还是遵守下面的规定。
10.2.1 函数
现代程序设计语言中的绝大部分功能,都在程序的函数(Function, Method)中实现,关于函数最重要的原则是:只做一件事,但是要做好。
10.2.2 goto
问:我们能不能用goto?
答:函数最好有单一的出口,为了达到这一目的,可以使用goto。只要有助于程序逻辑的清晰体现,什么方法都可以使用,包括goto。
如代码清单10-2:
代码清单10-2
HRESULT HrDoSomething(int parameter)
{
//parameter check and initialization
//processing part 1
If (SomeCode() != ok)
{
//set HR value
Goto Error;
}
//processing part 1
If (SomeCode() != ok)
{
//set HR value
Goto Error;
}
Error:
//clean up
return hr;
}
10.2.3 错误处理
80%的程序代码,都是对各种已经发生和可能发生的错误的处理。
——阿超
如果错误会发生,那就让程序死的地方离错误产生的地方越近越好。
——阿超
1.参数处理
在DeBug版本中,所有的参数都要验证其正确性。在正式版本中,从外部(用户或别的模块)传递过来的参数要验证其正确性。
2.断言
如何验证正确性?那就要用Assert(断言)。断言和错误处理是什么关系?
当你觉得某事肯定如何,你可以用断言。
Assert (p != NULL);
然后可以直接使用变量p;
如果你认为某事可能会发生,这时就要用错误处理。
如:
……
p = AllocateNewSpace(); // could fail
if (p == NULL)
{ // error handling.
}
else
{ // use p to do something
}
10.2.4 如何处理C++中的类
注意,除了关于异常(Exception)的部分,大部分其他原则对C#也适用。
1.类
(1)使用类来封装面向对象的概念和多态(Polymorphism)。
(2)避免传递类型实体的值,应该用指针传递。换句话说,对于简单的数据类型,没有必要用类来实现。
(3)对于有显式的构造和析构函数,不要建立全局的实体,因为你不知道它们在何时创建和消除。
(4)只有在必要的时候,才使用“类”。
2.Class vs. Struct
如果只是数据的封装,用Struct即可。
3.公共/保护/私有成员Public、Private和Protected
按照这样的次序来说明类中的成员:public、protected、private
4.数据成员
(1)数据类型的成员用m_name说明。
(2)不要使用公共的数据成员,要用inline访问函数,这样可同时兼顾封装和效率。
5.虚函数Virtual Functions
(1)使用虚函数来实现多态(Polymorphism)。
(2)只有在非常必要的时候,才使用虚函数。
(3)如果一个类型要实现多态,在基类(Base Class)中的析构函数应该是虚函数。
6.构造函数Constructors
(1)不要在构造函数中做复杂的操作,简单初始化所有数据成员即可。
(2)构造函数不应该返回错误(事实上也无法返回)。把可能出错的操作放到HrInit()或FInit()中。
下面是一个例子(见代码清单10-3):
代码清单10-3
class Foo
{
public:
Foo(int cLines) { m_hwnd = NULL; m_cLines = cLines}
virtual ~Foo();
HRESULT HrInit();
void DoSomething();
private:
HWND m_hwnd;
int m_cLines;
};
7.析构函数
(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.异常(Exceptions)
(1)异常是在“异乎寻常”的情况下出现的,它的设置和处理都要花费“异乎寻常”的开销,所以不要用异常作为逻辑控制来处理程序的主要流程。
(2)了解异常及处理异常的花销,在C++语言中,这是不可忽视的开销。
(3)当使用异常时,要注意在什么地方清理数据。
(4)异常不能跨过DLL或进程的边界来传递信息,所以异常不是万能的。
11.类型继承(Class Inheritance)
(1)当有必要的时候,才使用类型继承。
(2)用Const标注只读的参数(参数指向的数据是只读的,而不是参数本身)。
(3)用Const标注不改变数据的函数。
10.3 代码复审
阿超:代码复审看什么?是不是把你的代码拿给别人看就行了?
杂曰:(1)别人根本就不懂,给他们讲也是白讲。
(2)我是菜鸟,别的大牛能看得上我的代码么?
(3)也就是形式而已,说归说,怎么做,还是我说了算。
代码复审的正确定义:看代码是否在“代码规范”的框架内正确地解决了问题(见表10-1)。
表10-1 复审的形式
名 称 |
形 式 |
目 的 |
自我复审 |
自己 vs. 自己 |
用同伴复审的标准来要求自己。不一定最有效,因为开发者对自己总是过于自信。如果能持之以恒,则对个人有很大好处 |
同伴复审 |
复审者 vs. 开发者 |
简便易行 |
团队复审 |
团队 vs. 开发者 |
有比较严格的规定和流程,用于关键的代码,以及复审后不再更新的代码。 覆盖率高——有很多双眼睛盯着程序。但是有可能效率不高(全体人员都要到会) |
软件工程中最基本的复审手段,就是同伴复审。
谁来做代码复审?即最有经验,熟悉这一部分代码的人。对于至关重要的代码,我们要请不止一个人来做代码复审。
复审的目的在于:
(1)找出代码的错误。如:
a. 编码错误,比如一些能碰巧骗过编译器的错误。
b. 不符合项目组的代码规范的地方。
(2)发现逻辑错误,程序可以编译通过,但是代码的逻辑是错的。
(3)发现算法错误,比如使用的算法不够优化。
(4)发现潜在的错误和回归性错误——当前的修改导致以前修复的缺陷又重新出现。
(5)发现可能改进的地方。
(6)教育(互相教育)开发人员,传授经验,让更多的成员熟悉项目各部分的代码,同时熟悉和应用领域相关的实际知识。
10.3.1 为啥要做代码复审
问:为什么非得做代码复审不可?难道开发人员没有能力写出合格的代码?既然你招我进了移山公司,就是相信我有这个能力,对不对?
答:首先,在代码复审中发现的问题,绝大多数都可以由开发者独立发现。从这一意义上说,复审者是在替开发者干开发者本应干的事情。
问:这么说如果开发者做到完美,复审者的时间和精力是一种浪费了?
答:不对,即使是完美,代码复审还有“教育”和“传播知识”的作用。更重要的是,不管多么厉害的开发者都会或多或少地犯一些错误,有欠考虑的地方,如果有问题的代码已签入到产品代码中,再要把所有的问题找出来就更困难了。大家学习软件工程都知道越是项目后期发现的问题,修复的代价越大。代码复审正是要在早期发现,并修复这些问题。
另外,在代码复审中的提问与回应能帮助团队成员互相了解,就像练武之人互相观摩点评一样。在一个新成员加入一个团队的时候,代码复审能非常有效地帮助新成员了解团队的开发策略、编程风格及工作流程。
问:新成员是否应该在完全掌握了这些方面之后再写代码?
答:理论上是如此。但是如果我们要“完全掌握”,可能需要比较长的时间,另外,如果不开发实际的软件,这样的“完全掌握”有意义么?还是在实际中学习吧。这也是“做中学”(Learning by Doing)思想的体现。
10.3.2 代码复审的步骤
在复审前——
(1)代码必须成功地编译,在所有要求的平台上,同时要编译DeBug| Retail版本。编译要用团队规定的最严格的编译警告等级(例如C/C++中的W4)。
(2)程序员必须测试过代码。什么叫测试过?最好的方法是在DeBugger中单步执行。
问:有些错误处理的分支我不能执行到怎么办?
答:如果你作为作者都不能让程序执行到那里,那谁能保证这些错误处理的正确性呢?
同时也可以加上OutputDeBugString等输出来监视程序的控制流。
(3)程序员必须提供新的代码,以及文件差异分析工具。Windiff或VSTS自带的工具都可以。VSTS中可以通过Shelveset来支持远程代码复审。
(4)复审者可以选择面对面的复审、独立复审或其他方式。
(5)在面对面的复审中,一般是开发者控制流程,讲述修改的前因后果。但是复审者有权在任何时候打断叙述,提出自己的意见。
(6)复审者必须把反馈意见逐一提出。注意,复审者有权提出很多看似吹毛求疵的问题,复审者不必每一件事都要亲自调查,开发者有义务给出详尽的回答。例如:
复审者:你在这里申请了这个资源,你是如何保证它在所有路径下都能正确释放的?
开发者:这个……我要再检查检查。
或者——
开发者:这个是这样保证的,我用了SmartPointer,然后这里有try/catch/ finally……
要记住复审者是通过问这些问题来确保软件质量的,而不是有意找碴。
(7)开发者必须负责让所有的问题都得到满意的解释或解答,或者在TFS中创建新的工作项以确保这些问题将来会得到处理。例如:
复审者:这一段代码可能会被多个线程调用,代码是thread-safe么,我怎么没有看到对共享资源的保护?
开发者:我一时得不出结论,让我在TFS中开一个“任务”来跟踪此事。
(8)对于复审的结果,双方必须达成一致的意见。
a. 打回去——复审发现致命问题,这些问题解决之前不能签入代码;
b. 有条件地同意——发现了一些小问题,在这些问题得到解决或记录之后,代码可以签入,不需要再次复审;
c. 放行——代码可以不加新的改动,签入源码控制服务器。
避免不必要的繁文缛节,我们做代码复审的目的是为了减少错误的发生,而不是找一个人来对着你的代码点头。一些简单的修改不是非得要一个复审者来走一遍形式。在项目开发的早期斤斤计较于一些细枝末节(例如:帮助文件里的拼写错误,数据文件格式不够最优化等)也是于大局无补的,但是,这些问题并不是不用处理了,我们必须建立一些优先级较低的工作项来跟踪这些事情。
10.3.3 在代码复审中还要做什么
好的复审者不光是要注意到程序员修改了什么,还要把眼光放远,问一些这样的问题:
“你这样修改了之后,有没有别的功能会受影响?”
“项目中还有别的地方需要类似的修改么?”
“有没有留下足够的说明,让将来维护代码时不会出现问题?”
“对于这样的修改,有没有别的成员需要告知?”
“导致问题的根本原因是什么?我们以后如何能自动避免这样的情况再次出现?”
有些修改看似聪明有效率,但是这样的修改有可能会让以后的开发和维护更困难。
10.3.4 在代码复审后要做什么
人不能两次踏入同一条河流,程序员不能两次犯同样的错误。在代码复审后,开发者应该把复审过程中的记录整理出来:
(1)更正明显的错误。
(2)对于无法很快更正的错误,要在TFS中创建Bug把它们记录下来。
(3)把所有的错误记在自己的一个“我常犯的错误”表中,作为以后自我复审的第一步。
有些人喜欢在程序中加一些特定的标记,来跟踪各种“要做的事情”,例如:
//$todo: make this function thread-safe
//$review: is this function thread-safe? Need to look at it again
//$bug: when input array is very large, this function might lead //to crash
这些标记最好是加上人名,以示负责。如:
//$bug (AChao): when input array is very large, this function will // become very slow due to O(N*N) algorithm。
在代码复审过程中,$review标记的问题要一一讨论,在代码复审过后,所有的$review标记要清除。在一个里程碑或正式版本发布之前,所有的$todo:$bug:标记都要清除。
做标记是不错的办法,但是如果开发者光记得做标记,最后却没有真正去研究和改正这些潜在的问题,这些todo、review、Bug就会被遗弃在代码中。过了一段时间后,后来的程序员也不敢碰它们——因为没有人能真正了解上一个版本的$todo是真的要马上做,还是已经做过了(done)了,只是没有更新$todo的注释,或者问题早已通过别的方式解决了。其根本原因在于团队没有用TFS(或者其他的管理软件)进行记录,没有人会跟踪这些事情。
10.3.5 代码复审的核查表
下面是移山公司总结的代码复审核查表,我们可以在复审中使用,也可以加上自己认为重要的注意事项。
1.概要部分
(1)代码能符合需求和规格说明么?
(2)代码设计是否有周全的考虑?
(3)代码可读性如何?
(4)代码容易维护么?
(5)代码的每一行都执行并检查过了吗?
2.设计规范部分
(1)设计是否遵从已知的设计模式或项目中常用的模式?
(2)有没有硬编码或字符串/数字等存在?
(3)代码有没有依赖于某一平台,是否会影响将来的移植(如Win32到Win64)?
(4)开发者新写的代码能否用已有的Library/SDK/Framework中的功能实现?在本项目中是否存在类似的功能可以调用而不用全部重新实现?
(5)有没有无用的代码可以清除?(很多人想保留尽可能多的代码,因为以后可能会用上,这样导致程序文件中有很多注释掉的代码,这些代码都可以删除,因为源代码控制已经保存了原来的老代码。)
3.代码规范部分
(1)修改的部分符合代码标准和风格么(详细条文略)?
4.具体代码部分
(1)有没有对错误进行处理?对于调用的外部函数,是否检查了返回值或处理了异常?
(2)参数传递有无错误,字符串的长度是字节的长度还是字符(可能是单/双字节)的长度,是以0开始计数还是以1开始计数?
(3)边界条件是如何处理的?Switch语句的Default是如何处理的?循环有没有可能出现死循环?
(4)有没有使用断言(Assert)来保证我们认为不变的条件真的满足?
(5)对资源的利用,是在哪里申请,在哪里释放的?有没有可能导致资源泄露(内存、文件、各种GUI资源、数据库访问的连接,等等)?有没有可能优化?
(6)数据结构中是否有无用的元素?
5.效能
(1)代码的效能(Performance)如何?最坏的情况是怎样的?
(2)代码中,特别是循环中是否有明显可优化的部分(C++中反复创建类,C#中 string 的操作是否能用StringBuilder 来优化)?
(3)对于系统和网络调用是否会超时?如何处理?
6.可读性
代码可读性如何?有没有足够的注释?
7.可测试性
代码是否需要更新或创建新的单元测试?
还可以有针对特定领域开发(如数据库、网页、多线程等)的核查表。
10.4 本章讨论
九条:哇,这么多酷的C++ 功能都不能用,那我们还学什么C++,为了迎接考试,我都把Operator Overload、Polymorphism背得滚瓜烂熟了,为什么不让我用?
阿超:我们写程序是为了解决问题,不是“为赋新词强说愁”,这些高级的语言特性,不是不让用,而是要用得慎重,不要动不动就写三五个类,一个套一个,要把注意力集中在能否用简洁的方法解决问题上来。
荔荔:这么多规范,我不知道怎么写第一行程序了。
二柱:自我复审也很重要——把代码摆在面前,当作是别的菜鸟写的。把你通常问别人的,以及别人会问你的问题都自己问一遍。这样就能发现不少问题。
九条:如果开发者很厉害,那么复审者就没有什么作用,也许这些复审都是走过场?
小飞:笑话,同理可以推论,如果开发者很厉害,那么测试人员也没什么作用,也是走过场,干脆把他们送回家得了。
荔荔给阿毛讲自己刚写好的代码,阿毛对于这个代码要解决的问题还没有什么了解,荔荔说什么,阿毛就“啊,啊”地附和。最后,阿毛也提不出什么意见,荔荔倒是在自己描述的过程中发现了一些问题。
阿毛:这样的代码复审有用么?
阿超:还是有一点用处,至少确保代码的作者把代码的逻辑和思想系统地表达了一遍,这样做本身就能发现不少问题。
问:数据库的代码有没有规范?
答:有,见本书附录B(第337页)。
问: 这些规范啊, 建议啊, 都是细枝末节的东西, 我们要做世界级的软件,搞这些东西是不是太小家子气了?
答: 首先世界级的软件也会因为小小的纰漏而导致世界级的问题 - 例如一段时间以来我们常常听到的安全漏洞和紧急补丁。 其次,软件的开发是一个社会性的活动, 有它的规律。 其中一个规律就是 “破窗效应” (参见 broken windows theory),如果团队成员看到同伴们连一些细小的规范都不遵守, 那自己还要严格执行单元测试么? 另一个成员看到这个模块连单元测试都没有, 那他自己也随意修改算了。。。 这样下去, 整个软件的质量可想而知。
[1] 来源于某次微软俱乐部程序竞赛,略有修改。