编写可读代码的艺术

编写可读代码的艺术

1.可读性基本原理:代码的写法应当使别人理解它所需的时间最小化。

一.表面层次的改进

2.把信息装入名字中:

  • 选择专业的词; 不用Get,而用Fetch或者Download可能会更好,由上下文决定。
  • 找到更有表现力的词;
  • 避免使用tmp和retval等泛泛的词(tmp只用于短期存在且临时性是其主要存在因素的变量,retval则应该用一个描述该变量的值的名字来代替)。
  • 循环迭代器;建议不要使用i、j、iter和it等常用名做索引和循环迭代器,而是使用user_i、member_j等名字,或者更简化一些,ui、mj等,这种命名会帮助把代码中的缺陷变的更明显。

  • 用具体的名字代替抽象的名字:
    ServerCanStart()这个名字就不如CanListenOnPort()清楚。

  • 为名字附带更多细节:

  1. 带单位的变量值:给变量加上下划线加单位,包括带单位的参数。

  2. 附带其他重要属性:
情形变量名更好的名字
已转化为UTF-8格式的html字节 html html_utf8
一个纯文本格式的密码,需要加密后才能进一步使用 password plaintext_password
  • 为作用域大的变量采用更长的名字: 在小的作用域里可以使用短的名字。
  • 首字母缩略词和缩写,不建议使用;经验原则是一个该项目团队的新成员能否理解这个名词的含义作为标准。
  • 利用名字的格式来传递含义:使用PascalCasing和camelCasing。

3.不会误解的名字

决定一个名字之前,想象一下你的命名会被误解成什么。

  • 定义一个值的上下限,max和min是很好的前缀;
  • 对于包含的范围,first和last是最好的选择。
  • 对于包含/排除范围,begin和end是最好的选择。
  • 当为bool命名时,使用is、has、should这样的词来明确表示它是bool值。
  • 小心对特定词的期望。例如,会期望get()或者size()是轻量的方法。

4.审美

审美三原则:

  1. 使用一致的布局,让读者很快就习惯这种风格。
  2. 让相似的代码看上去相似。
  3. 把相关的代码行分组,形成代码块。

建议:

  • 重新安排换行来保持一致和紧凑;
  • 用方法来整理不规则的东西;(提取成一个方法)
  • 把代码按“列”对齐可以让代码更容易浏览。
    例如:很容易区分第几个参数
  1. CheckFullName("Doug Adams","Mr.Douglas Adams","");
  2. CheckFullName("Jake Brown","Mr.Jake Brown III","");
  3. CheckFullName("No Such Guy","","no match fount");
  4. CheckFullName("John","","more than one result");

再比如:很容易区分拼写错误, 第三行拼写错误。

  1. details = request.Post.get('details');
  2. localation = request.Post.get('location');
  3. phone = equest.Post.get('phone');
  4. email = request.Post.get('email');
  • 选一个有意义的顺序,始终一致地使用它;
    • 让变量的顺序和对应的表单顺序相匹配;
    • 从“最重要”到“最不重要”排序;
    • 按字母顺序排序。
  • 把代码分成“段落”;c#的#region、#endregion。
  • 关键思想:一致的风格比“正确”的风格更重要。

5.注释

什么不需要注释

关键思想:不要为那些从代码本身就能快速推断的事实写注释。

  • 不要为了注释而注释
  • 不要给不好的命名加注释——应该把命名改好

记录你编程时的思想

  • 加入“导演评论”:
    比如:
    1. 防止继任者为无谓的优化而浪费时间:

      1. //出乎意料的是,对于这些数据用二叉树比用哈希表快40%
      2. //哈希运算的代价比左右比较大的多
    2. 防止继任者花费时间来修复没必要的bug

      1. //这没有问题,但是要100%的通过测试用例太难了。
    3. 解释代码为什么写的这么乱,鼓励下一个人修改它,还给出了具体建议

      1. //这个类变得越来越乱
      2. //也许我们应该建立一个子类来帮助整理
  • 为代码中的瑕疵写注释
    流行的注释标记:
标记同常的意义
TODO: 我还没有处理的事情
FIXME: 已知的无法运行的代码
HACK: 对一个问题不得不采用比较粗糙的解决方案
XXX: 危险!这里有重要的问题

可以用todo:或者maybe-later:来表示相对次要的问题

  • 给常量加注释
    提示常量是什么,或者为什么是这个值。

站在读者的角度

  • 意料之中的提问
    如果需要向别人解释代码,就加上注释。
  • 公布可能的陷阱
    为普通读者意料之外的行为加上注释,比如超时,可能的异常等等。
  • “全局观”注释
    在文件/类级别上使用注释来解释所有部分是如何一起工作的。
  • 总结性注释
    用注释来总结代码块,使读者不致于迷失在细节之中。

6.写出言简意赅的注释

关键思想:注释应当具有高的信息/空间率。

  • 当像“it” 和“this”这样的代词可能指代多个事物时,避免使用它们
  • 尽量精确的描述函数的行为
  1. //返回文件的行数
  2. intCountLines(string filename)
  3. {
  4. ....
  5. }

这个注释不是很明确,因为有很多定义“行”的方式:

  • “”(空文件)—-0或1行?
  • “hello”—-0或1行?
  • “hello\n”—-1或2行?
  • “hello\n world”—-1或2行?
  • “hello\n world\r”—-2、3或4行?

最简单的实现方式是统计换行符(\n)的个数,所以下面的注释更好一些:

  1. //统计文件中有多少换行符
  2. intCountLines(string filename)
  3. {
  4. ....
  5. }
  • 在注释中用精心挑选的输入/输出例子进行说明
  • 声明代码的高层次意图,而非明显的细节
  • 用嵌套的注释来(如Fuction(/arg = /…))解释难以理解的函数参数
    个人认为C#用不到,可以使用标准函数注释方式。
  • 用含义丰富的词来使注释简洁

二.简化循环和逻辑

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

7.把控制流变得易读

条件语句中参数的顺序

指导原则:

比较的左侧比较的右侧
“被询问的”表达式,它的值更倾向于不断变化 用来做比较的表达式。它的值更倾向于常量

if/else语句块的顺序

建议:

  • 首先处理正逻辑而不是负逻辑的情况。用if(debug)而不是if(!debug)
  • 先处理简单的情况。
  • 先处理有趣或者可疑的情况。

?:条件表达式

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

  • 避免do/while循环
    do语句是错误和困惑的来源,应该把条件放在优先看到的地方。
  • 从函数中提前返回
    想要单一出口点的动机是保证调用函数结尾的清理代码,但是现在的编程语言提供了更好的方式:
语言清理代码的结构化术语
c++ 析构函数
Java、Python try finally
Python with
c# using
  • 臭名昭著的goto
    向前的goto会产生面条式代码,其实它们可以被结构化的循环替代,大多数时候应该避免使用goto。

最小化嵌套

每个嵌套都在读者的“思维栈”上又增加了一个条件。
关键思想:当你对代码进行改动时,从全新的角度审视它,把它作为一个整体来看待。

  • 通过提早返回来减少嵌套
  • 减少循环内的嵌套
    如果循环中的每个迭代是相互独立的(“for each”循环),可以使用continue跳过不必要的项。

理解执行的流程

理想的情况是从main()开始,然后一步一步执行代码,一个函数调用一个函数,直到程序结束,然而实践中,总有“幕后代码在运行”,让流程难以理解:

编程结构高层次程序流程是如何变得不清晰的
线程 不清楚什么时间执行什么代码
信号量/中断处理程序 有些代码随时可能执行
异常 可能从多个函数调用中向上冒泡一样地执行
函数指针和匿名函数 很难知道到底执行什么代码,因为编译时还没有决定
虚方法 可能会调用未知子类的代码

如果滥用这些结构,它会让跟踪代码像赌博游戏一样难以跟踪。

8.拆分超长的表达式

关键思想:把你的超长表达式拆分成更容易理解的小块。

  • 引入解释变量
    引入额外的变量来表示小一点的子表达式,这种方式有三个好处:
    • 把巨大的表达式拆成小段;
    • 通过简单的名字描述子表达式来使代码文档化;
    • 帮助读者识别代码中的主要概念。
  • 引入总结变量
    用一个短很多的名字来代替一大块代码。
  • 使用德摩根定理
    电路中的德摩根定理:
    1) not(a or b or c) <=> (not a) and (not b) and (not c)
    2) not(a and b and c) <=> (not a) or (not b) or (not c)
    定理总结:分别取反,转换与或,反向操作是:提出取反因子
  • 不要滥用短路逻辑
    在很多编程语言中,布尔操作会做短路计算(if(a || b)在a为true时不会计算b),这种行为很方便,但是不要滥用来实现复杂逻辑。
    关键思想:要小心“智能”的小代码段——它们往往以后会让别人读起来感动困惑。
    ps:在像Python、JavaScript以及Ruby这样的语言中,“or”操作符会返回其中一个参数(不会转换为bool值)。
  • 简化表达式的创意方法:
    • 尝试从“反方向”解决问题;
    • 如果是C++代码,可以尝试为大表达式中重复的部分使用宏。

9.变量与可读性

对于变量的草率运用会带来三个问题:

  1. 变量越多,越难以全部跟踪它们的动向;
  2. 变量的作用域越大,就需要跟踪它的动向越久;
  3. 变量改变得越频繁,就越难以跟踪它的当前值。

减少变量

  • 减少无价值的临时变量
    比如:
  1. now = datetime.datetime.now();
  2. root_message.last_view_time = now;

删除now,代码一样容易理解。

  1. root_message.last_view_time = datetime.datetime.now();
  • 减少存储临时结果的变量
    可以通过立即处理来消除这种变量。
  • 减少控制流变量
    比如:
  1. booldone=false;
  2. while(condition &&!done)
  3. {
  4. ...
  5. if(...)
  6. {
  7. done=true;
  8. continue;
  9. }
  10. }

解决方案通常是通过把代码挪到一个新的函数中(循环中的代码或者整个循环)。

缩小变量的作用域

关键思想:让你的变量对尽量少的代码行可见。
很多编程语言提供了多重作用域/访问级别,包括模块、类、函数以及语句块作用域。这样做的目的就是为了减少程序员同时考虑的变量个数。
比如:

  • C++中if语句的作用域:
  1. PaymentInfo* info = database.ReadPaymentInfo();
  2. if(info)
  3. {
  4. ...
  5. }
  6. //改为这样更好,程序员可以在info超出作用域后忘掉它
  7. if(PaymentInfo* info = database.ReadPaymentInfo())
  8. {
  9. ...
  10. }
  • JavaScript中创建“私有”变量
  1. //JavaScript中省略var的变量会声明为全局变量
  2. //所有的<script></script>和js文件都可以访问它
  3. submitted =false//Note:gobal variable
  4. var submit_from =(function(form_name){
  5. if(submitted){
  6. return;
  7. }
  8. ...
  9. submitted =true;
  10. };
  11. //submitted变量作用域是全局的。
  12. //读者需要考虑它是仅在submit_from()中使用,还是之后还会使用
  13. //另外调用的其他JavaScript文件也可能使用这个变量但是为了完全不同的目的。
  14. //使用“闭包”来缩小作用域
  15. var submit_form(function(){
  16. var submitted =false;//Note:只能被下面的函数访问
  17. returnfunction(form_name){
  18. if(submitted){
  19. return;
  20. }
  21. ...
  22. submitted =true;
  23. }
  24. });
  • 把定义向下移
    原来的C语言要求把所有的变量定义放在函数或者语句块的顶端,其实这个要求早在99年就已经被去掉了。其实读者在读到使用之前不需要知道所有的变量,所以应该简单的把每个变量定义移到它的使用之前。

只写一次的变量更好

关键思想:操作一个变量的地方越多,越难确定它的当前值。
C# 提供了readonly,java提供了final,C++提供了const关键字,因为常量往往不会引来麻烦。

三.重新组织代码

三种组织代码的方法:

  1. 抽取与程序主要目的无关的子问题;
  2. 重新组织代码使它只做一件事件;
  3. 先用自然语言描述代码,然后用这个描述帮助你找到更整洁的解决方案。

10.抽取不相关的子问题

积极地发现并抽取不相关的子逻辑:

  1. 看看某个函数或代码块,问问自己:这段代码的高层目标是什么?
  2. 对于每一行代码,问一下:它是直接为高层目标而工作的吗?这段代码高层次的目标是什么呢?
  3. 如果足够的行数在解决不相关的子问题,抽取代码到独立的函数中。

纯工具代码

通常“基本工具”是由编程语言中内置的库来实现的。但是有时需要你自己填充这中间的空白。如果你希望库中有一个XYZ()函数,那么就写一个吧。

创建大量的通用代码

通用代码完全地从项目的其他部分解耦出来。这样的代码容易开发、容易测试、并且容易理解。从项目中拆分出越多的独立库越好,这样你的项目代码会更小而且更容易理解。

项目专有的功能

在理想情况下,抽取的子问题对项目一无所知,但是就算它们不是这样,分离子问题依然可以带来好处。(比如将代码的抽象保持在同一抽象层次)

简化已有接口

每个程序员都爱提供整洁接口的库——参数少,不需要很多设置,通常只需一点时间就可以使用的库。但是如果你所用的接口不整洁,可以使用包装器来创建自己整洁的函数。

按需重塑接口

程序中很多代码在哪里只是为了支持其他代码——比如,对输入进行有效性判断,对输出进行后期处理,这些“粘附”代码常常与实际逻辑无关。这种代码应该抽取成独立的函数。

总结

把一般代码和项目专有的代码分开。结果是,对于这些子问题的解决方案倾向于更加完整和正确。可以在以后重用。
阅读参考:《重构:改善既有代码的设计》。

11.一次只做一件事

关键思想:应该把代码组织得一次只做一件事情。
有很多类似的建议和面向对象设计原则:“一个函数只做一件事”、“单一职责原则(描述类行为的)”

总结

重构步骤:
1.尝试把代码需要完成的任务列出来。
2.其中一些任务可以变成单独 的函数或者类。
3.其他的可以简单地成为一个函数中的逻辑“段落”。

12.把想法变成代码

如果你不能把一件事解释给你祖母听的话说明你还没有真正理解它。——阿尔伯特.爱因斯坦


  • 清楚地描述逻辑
    1.使用自然语言描述代码要做什么。
    2.注意描述中所用的关键词和短语。
    3.写出与描述所匹配的代码。
  • 了解函数库是有帮助的
    不要自己写不必要的代码,库里的代码经过了无数程序员的测试。
  • 把这个方法应用到更大的问题

     

  • 使用自然语言描述解决方案
  • 递归地使用这种方法

13.少写代码

关键思想:最好读的代码就是没有代码。

  • 别费神实现你不需要的功能。
    • 程序员倾向于高估有多少功能真的对于项目来说是必不可少的,很多功能结果没有完成、或者没有用到,只会让程序更复杂。
    • 程序员还倾向于低估实现一个功能所要花费的时间成本。我们乐观地估计了一下实现一个粗糙原型所花费的时间,但是忘记了在将来代码库的维护、文件以及后增的“重量”所带来的所有额外时间成本。
  • 质疑和拆分你的需求
    不是所有的程序都需要运行得快,100%准确,并且能处理所有的输入。
  • 保持小代码库
    宇宙的自然法则:随着坐标系统的增长,把它粘合在一起所需的复杂度增长得更快。
    最好的解决方案:让你的代码库越小、越轻量级越好。
    具体做法如下:
    • 创建越来越多的“工具”代码来减少重复代码;
    • 减少无用代码或没有用的功能;
    • 让你的项目保持分开的子项目状态;
    • 小心代码的“重量”,让它保持又轻又灵。
  • 熟悉你周边的库
    很多时候,程序员不知道现有的库可以解决他们的问题。在一个成熟的库中,每一行代码都代表相当大量的设计、调试、重写、文档、优化和测试。
  • 总结
    • 从项目中消除不必要的功能,不要过度设计。
    • 重新考虑需求,解决版本最简单的问题,只要能完成工作就行。
    • 经常性地通读标准库的整个API,保持对它们的熟悉程度。

四、精选话题

14.测试与可读性

关键思想:测试应当具有可读性,以便其他程序员可以舒服地改变或者增加测试。





posted @ 2016-08-24 21:12  qianzi  阅读(684)  评论(0编辑  收藏  举报