读薄《代码整洁之道》—— Clean Code

December 18, 2013
作者:星爷
出处:http://lxWei.github.io/posts/cleanCode.html
声明:转载请注明作者及出处。

每个想写出优美代码的程序员都应该好好读读《Clean Code》。

注:因为读的是英文版Clean Code,本文有些翻译可能与中文版不一致,我会在文中适当注明英文。

目录

1. 综述

糟糕的代码总让人深恶痛绝,使团队生产力下降,延缓工作进度,在竞争激烈的互联网时代,甚至能毁掉公司。

需求变化背离初期设计,紧张的进度,愚蠢的经理,苛求的用户……我们找了太多理由,究竟为什么好代码会这么快变质成糟糕的代码?其实,不怨别人,都是我们自己,自作自受,我们自己太不专业!

什么是整洁的代码?不同的程序员有不同的理解,但可以归纳出一些共同点。比如优雅、高效、容错、简单、直接、没有重复……那么,怎么才能写出整洁的代码呢?写整洁的代码,需要遵循大量的小技巧,还需要贯彻刻苦实践,习得“整洁感”。下面,对作者提到的技巧做一些总结。

2. 有意义的命名

2.1 使用名副其实的名字

标识符名字必须能够反映作者意图,如果一个标识符需要注释,那么,这个标识符命名就没能反映作者意图,需要进行修改。

2.2 避免误导信息

需要避免多意词,比如,缩略词常常会带来误导;单词拼写差异很小的情况也应该避免,如ControllerForEfficientHandlingOfString与ControllerForEfficientStorageOfString,这给阅读代码带来多大干扰;最后,大写字母O、数字0,小写字母l和数字1应该尽量避免,它们实在太像了。

2.3 使用有意义的区分

添加数字编号或干扰词来区分标识符可不算明智的做法。如果标识符不同,那么,他们就得有不同的含义,举例来说:

char * strcpy(char a1[], char a2[])

char * strcpy(char destination[], char source[])

二者相比,第二种写法显然更clean。

再说干扰词(noise words),干扰词是另一个无意义的区分,是多余的。举例来说,有个class叫product,然而,可能还有另外两个类,分别叫productInfo和productData,这里,Info和Date即是干扰词,这种命名会在后期带来不必要的麻烦。

2.4 使用便于读的名字

编程是一项社会化的活动,编程要便于交流。举例来说,有个变量名叫“BCR3cnt”,这会给沟通带来多大不便啊

2.5 使用便于搜索的名字

单个字母、数字常量之类的都有一个问题:它们不便于搜索。想象一下,如果你在当前目录下使用命令进行搜索,简直就是灾难

grep -rn “e” .

grep -rn “7” .

所以,单个字母仅可以用来在小作用域做局部变量,名字长度应该和作用域的大小相关。

2.6 不要编码(Avoid Encoding)

匈牙利标记法、成员前缀并不会带来便利,接口前不要加前缀“I”表示是接口,接口的实现前也不用加前缀“Imp”。最后,对于单个字母变量,需要避免心理映射(Mental Mapping),单个字母作为循环计数器并没有不妥,但用作它用时,程序员很容易将其映射到某具体的概念,如果因为变量”a”和变量”b”已经用了,就给新定义变量命名为”c”,这简直太难以理解了。

2.7 类名与方法名

类名和对象名应该用名词或者名词短语,而方法名应用动词或动词短语

2.8 其它

如果标识符名字很诙谐,那么,只有明白程序员的幽默的人在记得这个玩笑的时间内能看懂;另外,不要使用方言、俚语等。总之,Choose clarity over entertainment value. And Say what you mean. Mean what you say.

为每个概念选一个词并一直使用,名字应该唯一,并在使用中保持一直。举例来说,如果有fetch、get和retrieve三个方法,那么,调用方法时,自会增加许多不必要的麻烦。

要避免一词多意,代码要易读,而且很多时候是快速略读(quick skim),如果一次多意,那么在读代码过程中就需要去核实究竟用的是哪个意思。

阅读程序的基本都是程序员,他们不一定会问题领域很了解,所以,尽可能的使用CS的专业词汇。只有当没有程序员的说法时,才使用问题域的名字。最后,上下文语境也会有所帮助。

2.9 总结

好的命名需要好的描述技巧和一些文化背景,这不是技巧、商业和管理问题,所以,大部分程序员并不愿学习如何做好命名工作。但应该果断的做这件事,虽然短期内会付出一些精力,但收获会很大。

3. 函数

这章主要是函数编写方面的技巧,它能让函数更短小、命名合理、更友好的组织。

3.1 小

使用小函数。80年代,程序员常说函数不应该超过一屏,而且,那时候的屏幕仅能显示24行,每行80字符,虽然现在的屏幕更大,编辑器也更智能,但函数也应该尽可能的小。如果需要上下移动眼珠才能扫视完一个函数,函数就可能太长;如果需要移动头部才能扫视完一个函数,那么,这个函数一定太长;如果需要滚动屏幕才能才能完整显示函数,那么,赶紧重构。

3.2 空格与缩进

正确的使用空格和良好的缩进可以使代码更易读。其次,if、else、while等语句应该只占一行,如果太长,可以在这些语句里使用函数调用,一是使函数变短,二是函数调用能很好的说明作者意图。最后,这条规则也说明,函数里层次不应太多,也就是说缩进最多两层。

3.3 只做一件事

Functions should do one thing. They should do it well. They should do it only.

函数只做一件事,上升一个层级,那么,类也应该封装一种事物,若函数做了不只一件事,或类封装了不只一种事物,就是权责不清。不过,对于“一件事”的理解,需要大量实践经验。

那么,如何定义”One thing”?可以这样约定:如果一个函数下所有步骤都是相同等级的抽象,那么,就认为该函数只做一件事。如果一个函数里混合了各种层次的抽象,那么,很容易造成混乱,使人迷惑。就像破窗户理论,一旦细节和基本概念混淆,函数中的细节就会变得越来越多。

其次,需要自顶向下阅读代码。说的是整个文件的布局。每个函数下,应该是函数的下一层抽象函数,这样,阅读代码时可以自顶向下,而不用跳来跳去。

最后,是switch语句。switch语句通常都较长,而且,switch语句总是做多件事情。这可以采用抽象工厂的方式进行解决。

不过,对于“一件事”的理解,需要大量实践经验。

3.4 使用描述性强的名字

不要害怕使用长名字,也不要怕花太多时间在选择名字上,选择描述性强的名字可以极大简化脑海中的模块设计并有助于改进。

3.5 函数参数

函数参数应该尽可能的少,理想情况下是没有参数,参数越多,越不易读;参数越多,越不容易测试;参数不应该用来做输出,输出应该使用函数返回值。

其次,标记参数,比如,给函数传递一个boolean参数要尽量不用(ugly)。首先,它使问题复杂化了;其次,这明显是做了两件事,违背了一个函数只做一件事的原则。可以在上一抽象级使用if语句来进行判断,或者根据一个全局变量来进行判断。

当函数参数较多时,可以把概念相关的变量打包为类或者结构。不仅减少参数数量,也使得参数的描述性增强。

为了使函数描述性更强,可以使函数名与参数形成“动词\/名词对”的形式,如write(name)。

3.6 执行与查询分离(command query separation)

一个函数只应该做一件事,要么执行一个操作,要么回答一个问题,不要两个都做。比如下面的函数:

public boolean set(String attribute, String value)

函数表示,如果没有attribute属性,返回false;如果有,将其值设置为value,并返回true。那么,在函数调用时,如:

if (set(“username”,”lx”))…

读者可能感到迷惑,”username”是以前就被设置为”lx”了还是表示设置是否成功呢?如果将查询和执行分开,就不会有这种疑惑,把上述代码改成:

if (attributeExists("username")) {
    setAttribute("username","lx");

3.7 使用异常,不要用错误码

当使用错误码时,调用函数必须马上处理;当使用异常时,异常处理与正常代码分离。但是,try/catch模块打乱了代码结构,将异常处理代码与正常代码混合,所以,最好将try/catch模块抽取出来,抽取成他们自己的函数调用。最好,错误/异常处理函数也应该只做一件事,所以,函数里应该只有try/catch块。一个常见异常处理如下所示:

    public void delete (Page page) {
        try {
            deletePageAndAllReferences(page);
        }
        catch (Exception e){
            logError(e);
        }
    }

3.8 拒绝重复

Duplication may be the root of all evil in software.

许多原则和方法都是为了消除重复。引用的话已经说明了一切。

3.9 总结

编程就像写作,一开始写得很糟糕,结构混乱,然后,需要修改、重构,直到满意。编程也一样,程序开始也很长很复杂,有太多缩进与嵌套,很长的参数列表,乱七八糟的命名,重复冗余的代码。然后,需要修改和重构代码,拆分函数,改变命名,消除冗余代码,重新编排文件,当然,还得通过所有单元测试。最后,按照前面的规则进行规范。没有人能从一开始就严格遵守规范,但一定能循序渐进地修改。

4. 注释

最近看CPABE的开源代码,痛苦……

但是,Robert大叔并没有强调注释的重要性,而是说“comments are, at best, a necessary evil”,如果代码表述得足够清楚,那么,就不需要注释,注释是对表达性不足的代码的补充。 为什么Robert大叔这么说?因为注释会撒谎,人们经常会修改代码,但经常不会一并修改相应的注释,而错误的注释比没有注释坏处更大,所以,尽管注释有时是必须的,但本书更多的会讨论减少注释。