编写可读代码的艺术

观点:程序员之间的互相尊重体现在他所写的代码中。他们对工作的尊重也体现在那里。代码最重要的读者不是编译器,解释器或电脑,而是人。写出的代码能让人快速理解、轻松维护、容易扩展的程序员才是专业的程序员。

《编写可读代码的艺术》一书,专注于如何编写可读性更好的代码。

1.代码应当易于理解

1.1是什么让代码变得“更好”

示例1

[cpp]  view plain copy
  1. for(Node* node = list->head;node != NULL;node = node->next){  
  2.    print(node->data);  
  3. }  
  4.   
  5. Node* node = list->head;  
  6. if(node == null){  
  7.   return;  
  8. }  
  9. while(node->next != NULL){  
  10.  print(node->data);  
  11.  node=node->next;  
  12. }  
  13.   
  14. if(node != null){  
  15.    print(node->data);  
  16. }  


示例2

[cpp]  view plain copy
  1. return flag >0 ? "Fans":"fans";  
  2.   
  3. if(flag >0){  
  4.   return "Fans";  
  5. }else{  
  6.   return "fans;  
  7. }  


第1个版本更紧凑,第2个版本更直白。哪个标准更重要呢?

1.2可读性基本定理

代码的写法应当使别人理解它所需要的时间最小化。

"别人"应当指所有阅读你的代码的人,包括同事,也包括6个月后的你自己!


1.3总是越小越好吗

一般来讲,解决问题的代码越少就越好。很可能理解100行代码写成的类所需要的时间比1000行要短。
但少的代码并不总是最好。

[cpp]  view plain copy
  1. return flag >0 ? "Fans":"fans";  
  2.   
  3. if(flag >0){  
  4.   return "Fans";  
  5. }else{  
  6.   return "fans;  
  7. }  


比下面的代码花更多时间:

[cpp]  view plain copy
  1. value = findValue(key);  
  2. if( value != null && !value.isOccupied()){  
  3.  //do sth  
  4. }  



1.4理解代码所需的时间是否与其它目标有冲突
其它目标:更有效率,好的架构,容易测试?

1.5最难的部分
经常思考是否容易理解,需要额外的时间。(短期)
写出更少缺陷,更容易改进的代码。(长期)

第一部分 表面层次的改进

2.把信息装到名字里

2.1选择专业的词
getPage(url);
是从缓存获取页面,还是实时从互联网上获取呢?

更专业的词:fetchPage,downloadPage。

2.2避免象tmp这样泛泛的名字

[cpp]  view plain copy
  1. String tmp =user.name();  
  2. tmp += " "+user.email();  


用userInfo这样的名字更具有描述性。

建议:tmp这个名字只应用于短期存在且临时性为其主要存在因素的变量。

2.3用具体的名字代替抽象的名字
serverCanStart:检测服务是否可以监听某个给定的TCP/IP端口。
更好的名字:canLinstenOnPort。这个名字直接地描述了这个方法要做什么事情。


2.4为名字附带更多信息

[javascript]  view plain copy
  1. var start  = new Date().getTime();  
  2. //do sth  
  3. var end = new Date().getTime();  
  4.   
  5. var costTime = (start-end)/1000;  


(时间的单位是秒s,还是毫秒ms?)
costTimeMs?

2.5名字应该有多长
d,days,daysSinceLastUpdate
在小的作用域可以使用短的名字,大的作用域使用长的名字。
看看当前上下文是否有足够的信息。

2.6利用名字的格式来传递含义

[java]  view plain copy
  1. //常量名和类名的取名方式不一样  
  2. public static final int MAX_NUMBER= 100;  
  3.   
  4. public class Number{  
  5.   
  6. }  


3.不会误解的名字

3.1容易产生误解的例子
 

[java]  view plain copy
  1. allPersons.filter(“age>100”);  
  2.    


//挑出?减掉?

3.2推荐用first和last来表示包含的范围
   推荐用begin和end来表示包含/排除范围

 

[java]  view plain copy
  1. String str ="abcd";  
  2.  str.substring1(int first,int last);  
  3.  str.substring2(int bigin,int end);  


   
3.4给布尔值命名
  

[java]  view plain copy
  1. boolean addUser(){  
  2.   
  3.      boolean flag= true;  
  4.      return flag;  
  5.    }  
  6.        


把flag换成addSucceed
   

3.5与使用者的期望相匹配
 

[java]  view plain copy
  1. String name;  
  2.   //很多程序都习惯了把以get开始的方法当作“轻量级访问器”这样的用法,它只是简单地返回一个内部成员变量。  
  3.   private String getName(){  
  4.     return name;  
  5.   }  
  6.    
  7.   //bad  
  8.   private String getName(){  
  9.     return "My name is:"+Name+" !";  
  10.   }  



4.审美

4.1把声明按块组织起来

[java]  view plain copy
  1. //get/query/find/select 查询类方法  
  2.   
  3. //add  增加类方法  
  4.   
  5. //update 修改类方法  
  6.   
  7. //delete 删除类方法  


4.2把代码分成“段落”

[java]  view plain copy
  1. String name;  
  2. uddateName="";  
  3.   
  4. String email;  
  5. sendEmail();  
  6.   
  7. String address;  
  8. saveAddress();  


4.3个人风格与一致性

[java]  view plain copy
  1. class Name{  
  2.   
  3. }  
  4.   
  5. class Name  
  6. {  
  7.   
  8. }  


一致的风格比“正确”的风格更重要。

 

5.该写什么样的注释
5.1什么不需要注释

[java]  view plain copy
  1. //The class definition for Name.  
  2. class Name{  
  3.   
  4. }  


建议:不要为那些能从代码本身快速推断的事实写注释。

5.2记录你的思想
加入“导演评论”

[java]  view plain copy
  1. //准确率可以达到99%,没有必要达到100%  
  2. getValue();  
  3.   
  4. 为代码中的瑕疵写注意  
  5. //冒泡排序不够快  
  6. bubbleSort();  
  7.   
  8. 给常量加注释  
  9. //人的最大年龄  
  10. public static final int MAX_AGE=150;  


5.3站在读者的角度

[java]  view plain copy
  1. 公布可能的陷阱  
  2. //调用外部服务来发送邮件。(1分钟之后超时)  
  3. sendEmail();  
  4.   
  5. “全局观”注释  
  6. //这个类包含一些辅助函数,为我们的文件系统提供了更便利的接口  
  7.   
  8. 总结性注释  
  9. //求和  
  10. int[] array = {1,2,3};  
  11. for(int index=0;index<array.length;index++){  
  12.   sum += array[index];  
  13. }  


5.4最后的思考-克服“作者心理阻滞”
很多程序员不喜欢写注释,因为要写出好的注释感觉好像要花很多功夫。
当作者有了这种“作者心理阻滞”,最好的办法就是现在就开始写。
先写,再优化。

6.写出言简意赅的注释
建议:注释应该有很好的信息/空间率
6.1让注释保持紧凑

[java]  view plain copy
  1. //求和(计算第1个到最后1个元素的和)  
  2. int[] array = {1,2,3};  
  3. for(int index=0;index<array.length;index++){  
  4.   sum += array[index];  
  5. }  


6.2精确地描述函数的行为

[java]  view plain copy
  1. //返回文件的行数  
  2. //计算换行符(\n)的个数  
  3. int countLines(String fileName);  


6.3采用信息含量高的词
//这个类包含很多成员用来存储和数据库中相同的一些信息,为了提高速度。
//当这个类被读取的时候,检查这些成员是否存在,如果存在直接返回,不存在就存储。

简单地说:
//这个类作为数据库的“缓存层”(Caching Layer)。

第2部分 简化循环和逻辑

7.把控制流变得易读

关键思想:把条件、循环以及其它对控制流的改变做得越“自然”越好。运用一种方式使读者不用停下来重读你的代码。

7.1条件语句中参数的顺序

[html]  view plain copy
  1. <pre class="java" name="code">if(age >20){  
  2. }  
  3. 比if(20 < age){  
  4. }  
  5. 更易读。  
  6.   
  7. if(name == null){  
  8. }  
  9. 比  
  10. if(null == name){  
  11. }  
  12.   
  13. </pre><br>  


在中文和英文等自然语言中(“如果你的年龄大于20”)更常见,更符合一般用法。
即比较的左侧,它的值倾向于不断变化,比较的右侧,它的值倾向于稳定。

7.2if/else语句块的顺序

[java]  view plain copy
  1. if(a== b){  
  2.   //case one  
  3. }else{  
  4.   //case two  
  5. }  
  6. 也可以写成  
  7. if(a != b){  
  8.   //case one  
  9. }else{  
  10.   //case two  
  11. }  


之前你可能没想太多,但在有些情况下有理由相信其中一种顺序比另一种好:
a.首先处理正逻辑而不是负逻辑的情况。例如,if(debug)而不是if(!debug)。
b.先处理简单的情况。这种方式可能还会使得if和else在屏幕之内都可见,这很好。
c.先处理有趣的或者是可疑的情况。

下面所示是负逻辑更简单并且更有趣的一种情况,那么会先处理它

[java]  view plain copy
  1. if (not the same  username){  
  2.   //case one  
  3. }else{  
  4.   //case two  
  5. }  


7.3三目运算符
它对于可读性的影响是富有争议的。拥护者认为这种方式可以只写一行而不用写成多行,反对者则说
这可能会造成阅读的混乱而且很难用调试器来调试。

关键思想:相对于追求最小化代码行数,一个更好的度量方法是最小化人们理解它所需的时间。
建议:默认情况下都用if/else。三目运算符?:只有在最简单的情况下使用。

7.4避免do/while循环

[java]  view plain copy
  1. do{  
  2.   
  3. }while(condition);  


do/while循环的奇怪之处是一个代码块是否会执行是由其后的一个条件决定的。通常来讲,逻辑条件
应该出现在它们“保护”的代码之前,这是if,while和for语句的工作方式。
因为你通常会从前向后来读代码,这使得do/while循环有点不自然了。

7.5从函数中提前返回

[java]  view plain copy
  1. public boolean contains(String str,String substr){  
  2.   if(str==null || substr==null){  
  3.     return false;  
  4.   }  
  5.       
  6.    if(substr.equals("")){  
  7.     return true;  
  8.     }  
  9.       
  10.   ...  
  11. }  


7.6最小化嵌套

[java]  view plain copy
  1. if(userResult==SUCCESS){  
  2.   if(permissionResult != SUCCESS){  
  3.    reply.writeErrors("error reading permission");  
  4.    reply.done();  
  5.    return;  
  6.   }  
  7.   reply.writeErrors("");  
  8. else{  
  9.   reply.writeErrors(userResult);  
  10. }  
  11. reply.done();  


可以通过提前返回,来减少嵌套。


8.拆分超长的表达式
8.1用做解释的变量
 

[java]  view plain copy
  1. if(line.split(",")[0].name=="root"){  
  2.    
  3.  }  
  4.    
  5.  增加一个解释变量  
  6.  String username = line.split(",")[0].name;  
  7.  if(name=="root"){  
  8.  }  
  9.    



 
8.2总结变量
即使一个表达式不需要变量(因为你可以看出它的含义),把它装入一个新变量中仍然有用。
我们把它叫做总结变量,因为它的目的是用一个短很多的名字来代替一大块代码,这个名字会
更容易思管理和思考。

[java]  view plain copy
  1. if(request.user.id == document.user.id){  
  2.  //user can edit this document  
  3. }  
  4.   
  5. if(request.user.id != document.user.id){  
  6.  //document is read only  
  7. }  


这里的表达式“request.user.id==document.user.id”看上去可能并不长,但它包含
5个变量,所以需要多花点时间来想一想如何处理它。

这段代码中的主要概念是:“该用户拥有此文档吗?”这个概念可以通过增加一个总结变量来表达得更清楚。

[java]  view plain copy
  1. final boolean userOwnDocument = (request.user.id==document.user.id);  
  2. if(userOwnDocument){...  
[java]  view plain copy
  1. }  
  2. if(!userOwnDocument){...  
[java]  view plain copy
  1. }  



9.变量与可读性

关于变量的3个问题
a.变量越多,就越难全部跟踪它们的动向。
b.变量的作用域越大,就需要跟踪它的动向更久。
c.变量改变得越频繁,就越难以跟踪它的当前值。

9.1减少变量

[java]  view plain copy
  1. 没有价值的临时变量  
  2. now = datatime.time();  
  3. rootMessage.lastVisitTime=now;  
  4.   
  5. 减少控制流变量  
  6. boolean done=false;  
  7. if(condition && !done){  
  8.   if(...){  
  9.     done=true;  
  10.     continue;  
  11.   }  
  12. }  
  13.   
  14. 可以改为  
  15. if(condition){  
  16.   if(...){  
  17.     break;  
  18.   }  
  19. }  


9.2缩小变量的作用域

[java]  view plain copy
  1. 把定义向下移  
  2. int a=0;  
  3. int b=0;  
  4. int c=0;  
  5.   
  6. //handle a  
  7. //handle b  
  8. //handle c  
  9.   
  10. 改为  
  11. int a=0;  
  12. //handle a  
  13.   
  14. int b=0  
  15. //handle b  
  16.   
  17. 全局变量改为局部变量。  

9.3只写一次的变量更好

[java]  view plain copy
  1. public static final int MAX_AGE=14

重新组织代码

3种组织代码的方法

a.抽取出那些与程序主要目的“不相关的子问题”。

b.重新组织代码使它一次只做一件事情。

c.先用自然语言描述代码,然后用这个描述来帮助你找到更整洁的解决方案。

第10章 抽取不相关的子问题

本章的建议是“积极地发现并抽取不相关的自逻辑”,我们是指:

a.看看某个函数或代码块,问问你自己,这段代码高层次的目标是什么?

b.对于每一行代码,问一下:它是直接未来目标而工作吗?这段代码高层次的目标是什么呢?

c.如果足够的行数在解决不相关的子问题,抽象代码到独立的函数中。

介绍性的例子

[java]  view plain copy
  1. int[] array = {2,4,1,3};   
  2.   
  3. 求最大值和最小值。   
  4.   
  5. void method(){   
  6.   
  7.   //排序函数,这就是1个子问题   
  8.   
  9.   //去第1个和最后1个   
  10.   
  11. }   


纯工具代码

文件操作,邮件发送等。

创建大量通用代码

通用代码,它完全地从项目的其它部分解耦出来。这样的代码容易开发,容易测试,并且容易理解。SQL数据库、JavaScript库、XML库等。

项目专有的功能

把名字转换成1个URL,这类项目特有的功能,也是可以提取出来的。

其它话题:

简化已有接口;按需重塑接口;过犹不及。

总结

对本章一个简单总结就是:“把一般代码和项目专有的代码分开”。其结果是,大部分代码都是一般代码。通过建立一大组库和辅助函数来解决一般问题,剩下的只是让你的程序与众不同的核心部分。

这个技巧有帮助的原因是,它使程序员关注小而定义好的问题,这些问题已经同项目的其它部分脱离。其结果是,对于这些问题的解决方案倾向于更加完整和正确。你也可以在以后重用它们。

第11章 一次只做一件事情

同时在做几件事的代码很难理解。一个代码块可能初始化对象,清除数据,解析输入,然后应用业务逻辑,所有这些都同时进行。如果所有这些代码都纠缠在一起,对于每个"任务"都很难靠其自身来帮你理解它从哪里开始,到哪里结束。

你也许听说过这个建议:“一个函数只应该做一件事”。我们的建议和这差不多,但不是关于函数边界的。当然,把一个大函数拆分成多个小一些的函数是好的。但是就算你不这样做,你仍然可以在函数内组织代码,使得它感觉像是有分开的逻辑段。

下面是用于使代码“一次只做一件事情”所用到的流程:

a.列出代码所做的所有“任务”。这里的“任务”没有很严格的定义–它可以小得如“确保这个对象有效”,或者含糊得如“遍历树中所有的节点”。

b.尽量把这些任务拆分到不同的函数中,或者至少是代码中不同的段落中。

第12章 把想法变成代码

当你把一件复杂的事向别人解释时,那些小细节很容易就会让他们迷惑。把一个想法用“自然语言”解释是个很有价值的能力,因为这样其它知识没有你这么渊博的人才可以理解它。这需要把一个想法精炼成最重要的概念。这样做,不仅帮助他人理解,而且也帮助你自己把这个想法想得更清楚。

在你把代码“展示”给读者时,也应该使用同样的技巧。我们接受代码难点是你解释程序所做事情。

在本章中,我们会用一个简单的过程来使你编写更清晰的代码:

a.象对着一个同事一样用自然语言描述代码要做什么。

b.注意描述中所用的关键词和短语。

c.写出与描述所匹配的代码。

一个示例:

用户输入网站地址:如“http://FansUnion.cn”;

浏览器解析网址到IP,如42.96.184.84;

浏览器建立和该IP的Socket;

浏览器与该主机通信,取得网页;

显示网页内容。

第13章 少写代码

知道什么时候不写代码可能对于一个程序员来讲是他所要学习的最重要的技巧。你所写的每一行代码都是需要测试和维护的。通过重用库或者减少功能,你可以节省时间并且让你的代码保持精简节约。

最好读的代码就是没有代码。

a.别费神实现那个功能–你不会需要它。

很多功能没有完成,或者没有用,也可能是让程序更复杂。

一个功能,不是只有开发,还有测试,最后还有维护和升级。

b.质疑和拆分你的需求。

不是所有的程序都需要运行的快,100%准确,并且能处理所有的输入。如果你真的仔细检查你的需求,有时你可以把它削减成一个简单的问题。

c.保持小代码库。

创建“工具”代码减少重复代码;减少无用代码或者没有用的功能;让你的项目保持分开的子项目状态;注意“质量”,保持又轻巧又灵活。

d.熟悉你周边的库。

很多时候,程序员就是不知道现有的库可以解决他们的问题。或者有时,他们忘了库可以做什么。知道你的库能做什么,以便让你可以使用它,这一点很重要。

这里有一条比较中肯的建议:每隔一段时间,花15分钟来阅读标准库中的所有函数/模块类型的名字。这包括C++标准函数库(STL)、Java API等。

这样做的目的不是记住整个库。这只是为了了解有什么可以用的,以便下次你写新代码时会想:“等一下,这个听起来和我在API中见到的东西有点像…”我们相信提前做这种准备很快就会得到回报,起码因为你会更倾向于使用库了。

e.为什么重用库有这么大的好处?

 在一个成熟的库中,每一行代码都代表大量的设计、调试、重构、文档、优化和调试。

第14章 测试与可读性

a.使测试易于阅读和维护

测试代码的可读性和非测试代码是同样重要的。其他程序员会经常来把测试代码看作非正式的文档,它记录了真实代码如何工作和应该如何使用。因此如果测试很容易阅读,使用者对于真实代码的行为会有更好的理解。

对使用者隐藏不必要的细节,以便更重要的细节会更突出。

b.让错误消息具有可读性。

c.手工构造错误消息。

选择好多测试输入:你应该选择一组最简单的输入,它能完整地使用被测代码。

为测试函数取合适的名字,如testAddUser()。

总结

在测试代码中,可读性仍然很重要。如果测试的可读性很好,其结果是它们也会变得很容易写,因此大家会写更多的测试。并且,如果你把事实代码设计得容易测试,代码的整个设计会变得更好。

以下是如何改进测试的几个具体要点:

a.每个测试的最高一层应该越简明越好。最好每个测试的输入/输出可以用一行代码来描述。

b.如果测试失败了,它所发出的错误信息应该能让你容易跟踪并修正这个bug。

c.使用最简单的并且能够完整运用代码的测试输入。

d.给测试函数一个有完整描述性的名字,以使得每个测试所测试的东西都很明确。不要用Test1(),而用testAddUser这样的名字。

最重要的是,要使它易于改动和增加新的测试。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值