前言
保持代码的简洁,干净,清爽毫无疑问是重要的,同时也是不容易做到的。我们可以通过遵循一些编程规范来编写可读代码, 如通过《Google Java 编程规范》等来确保 Java 代码相对整洁。但是如果换一种语言,又会有不同的规范要求;同时, 规范一般只在局部的细节上通过参考,我们难以借此了解更广视角上保证程序设计整洁的方法。所以,有必要接触一些讲解 编写整洁代码的一般方法的书。比如,我这两天看完的《编写可读代码的艺术》,这是著名的 O’Reilly Media,Inc. 出品的 “动物书”系列之一。相比较于更加实用主义的计算机类书籍,这本书看起来很快,比如我只花了大概两天时间,还是在闲暇时看的; 干货还是有的,建议花一点时间读一读。以下就是我的读书笔记。
编写整洁代码的一般方法
总体上的哲学就是实事求是。尽量保证同时处于生命期的变量最少,变量越少,读代码时要考虑的东西就越少,同时变量命名也可以越简单; 否则就需要更加具体、详细的命名。注释则讲究言简意赅,不要在注释里说显而易见的东西。代码块以逻辑清晰为主,只要更整洁清晰了, 哪怕行数更多了也不要紧。
标识符命名
Pascal 命名法,下划线命名法,驼峰命名法是具体的命名规范,我们在这里不讨论。同时,介绍一个基于大数据帮助取名字的网站CodeIF,VS Code也有对应的插件。
- 使用多义词语时想一想会不会引起歧义,尤其是 record, count 这些即可当名词又可当动词的单词。
- 使用具体词语代替抽象词语,如表达 find 含义的还有 search, peek, locate, extract, scan; 同时注意它们含义上的区别。
- 给表示量的词带上它的单位,如使用 getTime_ms() 和 getTime_s() 代替单位不明的 getTime(),又如使用 hex_id 代替不显示存储内容格式的 id
- i,j 是约定俗成的循环索引,但是在迭代对象比较多时,可以带上迭代对象的缩写。如:
for (int ci=0; ci < clubs.size(); ci++) for (int mi=0; mi < clubs[ci].members.size(); mi++){ // 可以有效防止索引用错 // ...... }
- 需要使用表示同一事物的多个变量时,加上限定修饰词,如 plaintext_passwd 和 hash_passwd, str_passwd(string) 和 int_passwd(int)
- tmp 可以在短生命期中使用,如交换两个变量的值。适时地加上限定词以使得代码更可读,如 tmp_file, tmp_address, tmp_name
- 程序员间或领域内约定俗成的缩写(如 str,passwd,err)可以使用,不要人为的造新的缩写。
- 布尔类型变量命名常用前缀 is,has,should 等。
- 确定区间排除范围,一般 first/last表示区间时包括首尾,begin/end表示区间时包括首不包括尾(也就是说 end 所指的对象不在考虑的区间内)
- 注意使用者的期望,用命名区分开会花费大量时间或空间开销的函数,如 computeRays() 代替 getRays(), countSize() 代替 size(); 使用 get 前缀的函数或 size(), length() 看起来好像是的O(1)操作。
注释
- 保持注释看起来舒服,一般注释符与注释之间空一格,汉字与英文之间加空格,英文标点符号前不空格,后空格。
// 与两个反斜杠间空一格 // 使用了 beego 的日志模块 // Check the stack, if empty, return true.
- 保持相似结构的代码列对齐,有格式化插件会自动清除多余空格时,引入注释来占位。
type NilExp /* */ struct{ Line int } type TrueExp /* */ struct{ Line int } type FalseExp /* */ struct{ Line int } type VarargExp /**/ struct{ Line int }
- 在表格形式的代码上写上注释当作表头,避免逐行单独注释,引起注释重复。
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++ // ...... }
- 使用常见的注释声明记号,如 TODO 表示待实现部分,FIXME 表示有问题的代码,HACK 表示为了解决某一问题而不得不采用了较粗糙的方法。
- 在文件头写上注释,全局性解释我们为什么要这个东西,为什么要这样做;根目录 README 描述项目文件夹结构和框架。
- 串行或异步,可能过多的时间开销,可能抛出的异常,副作用等东西如果函数命名无法体现一定要通过注释表明。
代码块
- 把一堆函数声明,接口声明,常量声明,按逻辑分类,用空行隔开并在每一块上面加上注释。
- 代码块保持单一功能,比如不要在一个 if 代码块中处理多个情况。
- 让最外层的流程处理正常情况,if 代码块优先处理异常情况,简单情况,可疑情况。
- 可能的话 if 语句块中通过 return 退出逻辑以避免过多的else
func sendEmail(email string)bool{ if !isValid(email){ // ... return false } // ... }
- 把变量放在比较符的左边。
for i=0; i < N; i++{ if i > num{ // rather than "if num < i" } }
- 使用德摩根定理保证最外层不是逻辑取反。
德摩根定理: 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) )
- 把低一个层次的逻辑抽象成函数,如给用户发邮箱函数中抽象出:校验邮件内容,查找用户邮箱地址,发邮件;不要涉及具体实现。
- 测试用例只需要保证覆盖所有情况,尽量给一些简单值,不要给一大堆很嚣张的用例,否则不容易看出用例特征。
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}"
- 无所谓的东西如左花括号用不用换行建议遵循团队规范,一致性更加重要。
- 有时候你并不需要写数值型 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