保证代码整洁的一般方法

《编写可读代码的艺术》读书笔记

Posted by John Mactavish on December 1, 2019

前言

保持代码的简洁,干净,清爽毫无疑问是重要的,同时也是不容易做到的。我们可以通过遵循一些编程规范来编写可读代码, 如通过《Google Java 编程规范》等来确保 Java 代码相对整洁。但是如果换一种语言,又会有不同的规范要求;同时, 规范一般只在局部的细节上通过参考,我们难以借此了解更广视角上保证程序设计整洁的方法。所以,有必要接触一些讲解 编写整洁代码的一般方法的书。比如,我这两天看完的《编写可读代码的艺术》,这是著名的 O’Reilly Media,Inc. 出品的 “动物书”系列之一。相比较于更加实用主义的计算机类书籍,这本书看起来很快,比如我只花了大概两天时间,还是在闲暇时看的; 干货还是有的,建议花一点时间读一读。以下就是我的读书笔记。

编写整洁代码的一般方法

总体上的哲学就是实事求是。尽量保证同时处于生命期的变量最少,变量越少,读代码时要考虑的东西就越少,同时变量命名也可以越简单; 否则就需要更加具体、详细的命名。注释则讲究言简意赅,不要在注释里说显而易见的东西。代码块以逻辑清晰为主,只要更整洁清晰了, 哪怕行数更多了也不要紧。

标识符命名

Pascal 命名法,下划线命名法,驼峰命名法是具体的命名规范,我们在这里不讨论。同时,介绍一个基于大数据帮助取名字的网站CodeIF,VS Code也有对应的插件。

  1. 使用多义词语时想一想会不会引起歧义,尤其是 record, count 这些即可当名词又可当动词的单词。
  2. 使用具体词语代替抽象词语,如表达 find 含义的还有 search, peek, locate, extract, scan; 同时注意它们含义上的区别。
  3. 给表示量的词带上它的单位,如使用 getTime_ms() 和 getTime_s() 代替单位不明的 getTime(),又如使用 hex_id 代替不显示存储内容格式的 id
  4. i,j 是约定俗成的循环索引,但是在迭代对象比较多时,可以带上迭代对象的缩写。如:
for (int ci=0; ci < clubs.size(); ci++)
    for (int mi=0; mi < clubs[ci].members.size(); mi++){
        // 可以有效防止索引用错
        // ......
    }
  1. 需要使用表示同一事物的多个变量时,加上限定修饰词,如 plaintext_passwd 和 hash_passwd, str_passwd(string) 和 int_passwd(int)
  2. tmp 可以在短生命期中使用,如交换两个变量的值。适时地加上限定词以使得代码更可读,如 tmp_file, tmp_address, tmp_name
  3. 程序员间或领域内约定俗成的缩写(如 str,passwd,err)可以使用,不要人为的造新的缩写。
  4. 布尔类型变量命名常用前缀 is,has,should 等。
  5. 确定区间排除范围,一般 first/last表示区间时包括首尾,begin/end表示区间时包括首不包括尾(也就是说 end 所指的对象不在考虑的区间内)
  6. 注意使用者的期望,用命名区分开会花费大量时间或空间开销的函数,如 computeRays() 代替 getRays(), countSize() 代替 size(); 使用 get 前缀的函数或 size(), length() 看起来好像是的O(1)操作。

注释

  1. 保持注释看起来舒服,一般注释符与注释之间空一格,汉字与英文之间加空格,英文标点符号前不空格,后空格。
// 与两个反斜杠间空一格
// 使用了 beego 的日志模块
// Check the stack, if empty, return true.
  1. 保持相似结构的代码列对齐,有格式化插件会自动清除多余空格时,引入注释来占位。
type NilExp /*   */ struct{ Line int }
type TrueExp /*  */ struct{ Line int }
type FalseExp /* */ struct{ Line int }
type VarargExp /**/ struct{ Line int }
  1. 在表格形式的代码上写上注释当作表头,避免逐行单独注释,引起注释重复。
var opcodes = []opcode{
	/*     T  A    B       C     mode         name       action */
	opcode{0, 1, OpArgR, OpArgN, IABC /* */, "MOVE    ", move},     // R(A) := R(B)
	opcode{0, 1, OpArgK, OpArgN, IABx /* */, "LOADK   ", loadK},    // R(A) := Kst(Bx)
	opcode{0, 1, OpArgN, OpArgN, IABx /* */, "LOADKX  ", loadKx},   // R(A) := Kst(extra arg)
	opcode{0, 1, OpArgU, OpArgU, IABC /* */, "LOADBOOL", loadBool}, // R(A) := (bool)B; if (C) pc++
    // ......
}
  1. 使用常见的注释声明记号,如 TODO 表示待实现部分,FIXME 表示有问题的代码,HACK 表示为了解决某一问题而不得不采用了较粗糙的方法。
  2. 在文件头写上注释,全局性解释我们为什么要这个东西,为什么要这样做;根目录 README 描述项目文件夹结构和框架。
  3. 串行或异步,可能过多的时间开销,可能抛出的异常,副作用等东西如果函数命名无法体现一定要通过注释表明。

代码块

  1. 把一堆函数声明,接口声明,常量声明,按逻辑分类,用空行隔开并在每一块上面加上注释。
  2. 代码块保持单一功能,比如不要在一个 if 代码块中处理多个情况。
  3. 让最外层的流程处理正常情况,if 代码块优先处理异常情况,简单情况,可疑情况。
  4. 可能的话 if 语句块中通过 return 退出逻辑以避免过多的else
func sendEmail(email string)bool{
    if !isValid(email){
        // ...
        return false
    }

    // ...
}
  1. 把变量放在比较符的左边。
for i=0; i < N; i++{
    if i > num{
        // rather than "if num < i"
    } 
}
  1. 使用德摩根定理保证最外层不是逻辑取反。

德摩根定理: not (a or b or c) <=> (not a) and (not b) and (not c) not (a and b and c) <=> (not a) or (not b) or (not c) 小结一下就是: 分别取反,与或反转

if( !file_exists || is_protected )
// rather than if( !(file_exists && !is_protected) )
  1. 把低一个层次的逻辑抽象成函数,如给用户发邮箱函数中抽象出:校验邮件内容,查找用户邮箱地址,发邮件;不要涉及具体实现。
  2. 测试用例只需要保证覆盖所有情况,尽量给一些简单值,不要给一大堆很嚣张的用例,否则不容易看出用例特征。
var test_table={1,5,9,2,7,0,-3,1e9,2}
// 1e9 可以简洁地表示大数据情形
// rather than "var test_table={18904,5233,246459,2247712,7074,0,-39421}"
  1. 无所谓的东西如左花括号用不用换行建议遵循团队规范,一致性更加重要。
  2. 有时候你并不需要写数值型 for 循环,可以把迭代过程直接写在 for 语句中。
for(Node* p=head; p!=null; p=p.next){
    // rather than
    // Node* p= head;
    // while(p!=null){
    //    //...
    //    p=p.next;
    //}
}

最后附上GitHub:https://github.com/gonearewe