概述
1、本文档的内容主要来源于书籍《代码整洁之道》作者Robert C.Martin,属于读书笔记。
2、软件质量,不仅依赖于架构和项目管理,而且与代码质量紧密相关,本书提出一种,代码质量与整洁成正比的观点,并给出了一系列行之有效的整洁代码操作实践,只要遵循这些规则,就可以编写出整洁的代码,从而提升代码质量。
3、该书介绍的规则均来自于作者多年的实践经验,涵盖从命名到重构的多个编程方面,具有很好的学习和借鉴价值。
4、习艺要有二:知和行。你应当学习有关规则、模式和实践的知识,穷尽应知之事,并且对其了如指掌,通过刻苦实践掌握它!
前言
学习整洁代码很难,它不止于要求你掌握原则和模式,你还得在上面下功夫,并自行实践,体验失败。你须观察他人如何实践与失败,怎样蹒跚学步,再转头学习他们的路数,。
本书要求你多用信息,多用功,而且非常用功。如何用功?-大量阅读代码,并琢磨代码好在什么地方,坏在什么地方。
本书大概分为三部分:原则、模式和实践。
一、 使用有意义的命名
1、名副其实
注意命名,一旦发现有更好的名称就换掉旧的,这么做阅读的人会更开心
名称本身应该能解释其含义,无需注释就能看懂是最佳。比如
int d;//消失时间,以日计
int elapsedTimeInDays;
前者名称没有任何含义,在程序中使用时看不出这个变量的实际作用,需要对应注释才能看懂,因此远不如后者的名称好!
2、避免误导
程序员必须避免留下掩藏代码本意的错误线索,避免使用与本意相悖的词。
提防使用细节之处差别较小的名称
使用相同的拼写方式,前后拼写不一致(大小写不同),就是误导。在使用编辑器名称自动补全功能时,拼写相近的变量容易引起误选。
避免使用小写字母l和大写字母O作为变量名称,易与1和0混淆
3、做有意义的区分
避免以数字系列命名,其无法提供正确的信息和导向作者意图的线索。
不要使用意义相近的名称,比如ProductInfo和ProductData变量不同,意思一样,容易引起意义混淆
不要使用冗余信息,比如NameString,难道Name会是一个浮点数吗?如果是,就不该使用Name命名。
4、使用读的出来的名称
人类善于记忆和使用单词,如果名称无法阅读或者发音,就不是一个好名称,讨论和交流时也难以表达。
比如函数名称为:genymdhms()//生成日期,年月日时分秒。
不要使用傻乎乎的自造词,而要使用恰当的英语单词
5、使用可搜索的名称
比如字母e,f等就不是一个好的变量名,其是英文常用字母,不方便搜索,
单字母名称仅限于本地局部变量使用,名称长短应该与作用域大小相对应
如果程序中多出使用相同数字,实现相同功能,则需要使用宏定义变量代替。
比如WORK_DAYS_PER_WEEK就比数字5好搜索,也更能体现作者意图
6、避免使用编码
无需把类型和作用域编进名称,这样只会自找麻烦,既不便发音,也容易拼错,对解决问题毫无帮助。
匈牙利标记法,破坏了不编码的规则,不应该采用。
也不必使用成员前缀,应该把类和函数做的足够小,同时使用可以高亮和颜色标出成员的编辑环境。(Keil,notepad++都支持)。
7、避免思维映射
不应当让读者把你脑中的名称翻译成他们熟知的名称,这个问题常见于选择使用问题领域的术语还是解决方案领域的术语时。
在作为局部变量时,并且名称没有冲突时,可以采用i,j,k作为循环变量。
专业程序员善用其能,编写能让他人理解的代码
8、类名
类名应该是名称或者名词短语,例如Customer、Account,避免使用Manager、Data、Info这样的类名,其不应该是动词。
9、方法名
方法名应该是动词或者动词短语,比如postPayment、deletePage或s**e,属性访问应该加上set、get、is前缀
10、每个概念对应一个词
给每个抽象概念选用一个词,并且一以贯之。比如使用fetch、retrieve、get在多个类的中同种方法命名,就容易引起混淆。
11、别用双关语
避免将以此用于不同目的,同一术语用于不同概念就是双关语了。
比如多个类中都有add方法,该方法通过增加或者链接两个现存值来获得新值,如果一个新类的含义是,把单个参数放到群集(collection)中,使用add名称,虽然保持了名称一致,你是含义却不同,应该使用insert才对。
12、使用解决方案领域的名称
因为只有程序员才会读取你的代码,因此名称应该选择解决方案领域的名称,而不是问题设计领域的名称。比如名称AccountVisitor就比JobQueue富有意义。
13、使用源自所涉及问题领域的名称
当不能使用程序员所熟悉的术语命名时,就应该采用所涉及问题领域的名称
与所涉问题领域更加贴近的代码,应当采用源自问题领域的名称
14、添加有意义的语境
很少有名称能够自我说明-多数都不能,因此需要使用良好命名的类、函数来放置名称,给读者提供语境。
比如添加前缀addrFirstName、addrLastName、addrState,就可以提供语境,这些变量属于地址范围。更好的做法是,创建一个名称为Address的类,来存放这些相关变量。
语境的增强也让算法能够通过分解为更小的函数而变得干净利索。
15、不要添加没有意义的语境
比如应用(Gas Station Deluxe)简称为GSD,因此为每个函数、类、变量增加同样的前缀就GSD命名就不是一个好点子。
Address是个好名称,但是如果需要与MAC地址、端口地址或Web地址区分,应当使用PostalAddress、MAC、URI,这样的名称更为精确。
16、总结
取名字最难地方在于需要良好的描述技巧和共有的文化背景
试试上面的规则,看你的代码的可读性是否有所提升。如果维护别人的代码,使用重构工具来解决问题,效果也好立竿见影,而且会持续下去。
二、关于函数的一些规则
1、短小
函数的第一规则就是短小。
代码长度以20行封顶为最佳。
if,else,while语句的代码块应该只有一行,这样不但能保持函数短小,还能增加可读性。
函数的层级不应该多于两层,这样才容易理解和阅读。
2、只做一件事
函数应该只做一件事,做好这一件事。
判断函数是否只做了一件事的方法就是看其是否能够拆出一个函数,该函数不是单纯的重新诠释其实现。
3、每个函数一个抽象层级
要确保函数做做一件事情,函数中的语句都要在同一抽象层级上。这是保持函数短小的要诀。
4、函数参数
最好少于3个,0个和1个最佳,3个以上请使用结构体代替。
输出参数比输入参数更加难以理解,读函数时,习惯于认为信息通过参数输入函数,通过返回值输出。
给函数起个好名字,使其能够很好的解释函数的意图、以及参数的顺序。比如assertExpectedEqualsActual(expected,actual)就容易理解和记忆。
5、无副作用
副作用指的是被其隐藏起来的事情,比如开机检查密码函数,如果调用前密码未经初始化,就会出错,就隐藏了其时序性耦合的要求。应该在其函数中增加初始化密码的功能,尽管违反了只做一件事的。
6、分割指令与查询
函数修改某对象的状态或者获取该对象的参数,应该分为两个函数实现。
比如函数设置某个指定属性,如果成功就返回true,失败返回false,用例如下:
if(set("username","unclebob"))...
这样的语句理解上面有歧义,set是动词,但是在上下文中感觉像形容词,从读者角度看,这句语句的意思是问username的属性是否已经设置为unclebob?还是询问username的属性是否成功设置为unclebob?因此最好改为如下所示:
if(attributeExists("username"))
{
setAttribute("username","unclebob");
}
7、使用异常代替返回错误值
直接返回错误代码鼓励了在if语句中把指令当做表达式使用,会导致多层次嵌套结构。如果返回异常代替错误码,错误处理代码就可以从主路径代码中分离出来,得到简化
8、不要重复
重复的代码可维护性差,如果算法修改,所涉及的地方都要更新,代码臃肿,还会增加放过错误的可能!
如果一个算法在不同函数中重复出现,则需要使用一个独立的函数修复它。
面向对象和面向组件编程也多多少少都是消除重复的策略。
9、结构化编程
只要保持函数短小,偶尔出现return、break、continue语句没有什么坏处,甚至比单入单出更具表达力。
goto语句只在大函数中才能用到,因此尽量避免使用。
10、如何写出短小的函数
写代码和写文章一样,先写出初稿、再细细打磨,直至达到心中的样子。
初写的函数,一般都是冗长而复杂,有太多缩进和嵌套循环,名称随意性大,代码有重复。通过分解函数、修改名称、消除重复,然后遵循本章的规则,重新组装函数,就可以写出短小精炼的函数。
三 关于注释的规则
1、注释不能美化糟糕的代码
带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样的多!
与其花时间写注释,不如花时间清理早搞定糟糕的代码。
代码在变动、演化,而很不幸,注释不是总随之变动,时间越久,注释离代码的本意就越久。只有代码是唯一真正准确的信息来源。。
2、用代码阐述
使用代码本身解释其行为。
比如//Check to see if the employee is eligible for full benefits
if((employee.flags &HOURLY_FLAG)&&(employee.age > 65))
通过创建与注释所言同一事物的函数即可。
if(employee.isEligibleForFullBenefits())
3、好注释
法律信息,比如公司代码规范要求写的有关法律的注释,版权声明等。
提供信息的注释,更好的方法是使用函数名称传达信息。
对意图的解释:注释不仅提供了有关实现的有用信息,而且还提供了某个决定后面的意图。
阐释:把晦涩难懂的参数或者返回值翻译为可读形式的注释是有用的。
警示:用于警告其它程序员会出现某种后果的注释也是有用的。
4、坏的注释
喃喃自语式:只是觉得因为过程需要就增加的注释。
多余的注释:本身并不能比代码提供更多的信息,就是多余。
误导性注释:含义不精确或者容易引起误导的注释。
循规性注释:每个函数或者每个变量都要有注释的规矩是全然可笑的,只会搞乱代码。
日志式注释:在有源码控制系统的今天,这种冗长的记录只会让模块变得凌乱不堪,应该删除
废话注释:对显然之事喋喋不休,毫无新意。看代码的人员,也几乎对其视而不见。因此用整理代码的决心替代废话的冲动,会使你成为更优秀的程序员。
可怕的废话:在知名开源库的J**adoc中,也可以看到复制黏贴错误,意思就是变量不一样,注释却一样。
位置标记:少用标记栏,比如//Action//////////////////////////////,特定函数放在这个下面,多数时候是在不必要。
括号后面的注释:尽管对于多层嵌套有帮助,但是更应该使用短小、封装的函数,如果你想标记右括号,其实应该做的是缩短函数。
归属与署名:源码控制系统很善于记录是谁在何时改动了什么,没有必要用小小的签名搞脏代码。比如注释/*Added by Rick*/
注释掉的代码:直接把代码注释掉是讨厌的做法,别人可能认为其一定有原因才留在这里不敢删除,最后注释掉的代码像渣滓一样堆在那里,别这么干!
信息过多的注释:别再注释中增加历史性话题或者无关的细节描述。
不明显的联系:注释的作用是解释未能自我解释的代码,如果本身还需要解释就没有必要了。
函数头:短函数不需要太多注释,只需要写一个好名字就比注释强得多!
四 关于代码格式的规则
你应该保持良好的代码格式,选用一套管理代码格式的简单规则,然后贯彻实施。如果在团队中工作,则团队应该一致同意采用一套简单的格式规则,所有成员都要遵从。使用能帮你应用这些格式规则的自动化工具会很有帮助。
1、格式的目的
原始代码修改很久之后,其代码风格和可读性仍会影响到可维护性和扩展性。
代码格式关乎沟通,而沟通时专业开发者的头等大事!
哪些代码格式方面能帮助我们沟通呢?
2、垂直格式
垂直尺寸
单个文件的长度尺寸与可理解性相关,统计数据表明,用大多数为200行到500行的单个文件可以构造出出色的系统,短文件通常比长文件容易理解!
函数布局
函数名称应该足够告诉我们是否在正确的模块中,源文件顶部应该给出高层次的概念和算法,细节应该向下逐次展开,直至源文件中最低层的函数和细节。就像报纸一样,标题统领提纲概要,内容逐次展开细节。
空行
在包声明、函数之间、代码块之间应该使用空行隔开,这条简单的规则极大的影响到代码的视觉外观。空白行作为一条线索,标示出独立的概念。
靠近 如果说空白行隔开了概念,靠近的代码则暗示了他们之间的紧密联系。因此关系紧密的应该互相靠近.
垂直距离
全局变量声明应该放到文件第一个函数声明前,局部变量声明放到函数顶部。相关函数应该放在一起,调用者放在被调用者上面,这样就能轻易找到被调用函数,极大增强模块的可读性
3、横向格式
行宽
一行代码应该多宽?统计数据表明,70%的代码行少于60个字符,代码行应该尽量短小,死守80字节有点僵化,但是不要超过100字符或者120字符。简单的规则是无需向右拖动滚动条,就可以看到全部代码。
水平区隔与靠近
在运算符号两端(乘号除外,因其优先级较高,多数格式化工具都忽视优先级)、函数名和右括号之间之间,函数参数之间增加空格!
水平对齐
经验表明一组类声明中的变量名、或者赋值语句的右值对齐没有什么实际作用,因此左端对齐,使用同样的空格区分规则即可。
缩进
有缩进的代码结构,阅读时可以很容易查找新的声明、变量、类和函数块,否则就需要折腾一番才能明白,程序员应该依赖缩进模式(在python中强制使用缩进表示for循环的范围,C语言中使用大括号,但是也要依赖缩进增强可读性)。
空范围
有时while或for语句的语句体为空,如下所示.右端的分号很难看见,容易让人迷惑。
while(dis.read(buf,0,readBufferSize) != 1);
因此最好改为如下格式
while(dis.read(buf,0,readBufferSize) != 1)
{
}
团队规则
在团队中,各成员应该采用一致的代码格式,如果各自风格不同,会增加项目代码的复杂度。
五 单元测试
过去10年来,编程专业领域进步很大,特别是敏捷和TDD(测试驱动开发)运动鼓舞了很多程序员编写单元测试。TDD要我们在编写生产代码之前先编写测试,其还有三大定律:
定律一:再编写不能测试通过的测试驱动前,不可编写生产代码
定律二:只可编写刚好无法通过的单元测试,不能编译也不算通过
定律三:只可编写刚好足以通过当前失败测试的生产代码。
这些定律要求测试与生产代码一起编写,测试代码先写,这样写程序,测试将覆盖所有生产代码。
1、保持测试的整洁
测试代码和生产代码一样,需要被思考、设计和照料,像生产代码一样整洁
正是单元测试让你的代码可扩展、可维护、可复用。如果没有测试,每次修改都可能带来缺陷,无论架构如何有扩展性,设计划分如何好,如果没有单元测试,你的改动都可能带来不可预知的缺陷。
2、整洁的测试
整洁的唯一要素的就是可读性,在单元测试中,可读性比生产代码还重要。如何做到可读性?就是要明确、简洁和具有足够的表达力。
测试要采用构造-操作-检验模式,每个测试均要可以清晰的拆分为三个环节,第一个环节构造测试数据,第二个环节操作测试数据,第三个环节检验操作是否得到期望的结果。
3、每个测试一个断言
单个测试函数有且仅有一个断言是个好准则。
每个测试做测试一个概念,如果一个测试测量三件事情,可能会导致遗漏。
4、整洁的5条规则
快速(fast),测试应该快速运行,你才能想要频繁运行它,如果不频繁运行,就不能尽早发现问题。
独立(Independent),测试应该互相独立,从而可以单独运行每个测试。
可重复(Repeatable),测试应该在任何环境中重复通过,否则当前环境具备时,你也会无法运行测试。
自足验证(Self-Validating),测试应该有布尔值输出去,无论如何不应该查看日志文件来确认测试是否通过。
及时(Timely),测试应该及时编写,如果在生产代码之后编写测试,你会发现代码难以测试。