放假前在学校图书馆借了一本新书《The Art of Readable Code》,寒假回来看看,写写其中的Key Idea 、summary和一些读书笔记。
Preface
前言部分主要概况讲了本书的核心思想——Code shoule be easy to understand。接着探讨什么是好代码,是内容紧凑还是对每个过程都详细阐释?从而引发出核心概念:Code should be written to minimize the time it would take someone else to understand it.(代码应让人在尽可能短的时间内理解),这个人,很有可能就是以后的自己。
通俗来说,简短的代码总是比长代码好读懂,但是不能因为追求简短而使代码难于理解。因此,尽管要追求代码的短小精悍,更应该在最小化理解代码的时间。同 时,易于读懂的代码通常更加易于优化,有更好的架构,易于测试.etc。Easy to understand,对代码来说,是最核心的,因此,当遇到其他冲突要素时,不能忘记这一点。
Part One: Surface-Level Improvements
Packing information into names
1. 选用明确词汇。
比如,“get”就不是一个很明确的词汇,比如 def GetPage(url): 函数,我们将不能明确从哪里获取,是本地缓存还是数据库亦或是网络。比如从网络获取,我们可以选用fetch,download去替换。FetchPage,DownloadPage…….。一次类推,比如表达树的size,可以用NumNodes,进程stop可以用kill。另外,我们可以选用更加丰富的词汇来表达:
Word | Alternatives |
send | deliver,dispatch,announce,distribute,route |
find | search,extract,locate,recover |
start | launch,create,begin,open |
make | create,set up,build,generate,compose,add,new |
Key idea: It's better to be clear and precise than to be cute. 清晰准确,甚于灵巧可爱。
2. 避免使用泛化的名字,比如tmp,retval,foo等
比如retval(I'm a return value),我们应该选用描述变量值含义的词汇。比如tmp,它应仅仅用于短期存在或临时性是该变量的重要成分(此时tmp应该作为前缀或后缀,tmp_file),除此之外,我们都应选用能够描述含义的词汇。
另外,对于循环迭代中的 i,j,k,iter等,在多重循环时可以附加一些index的讯息。比如
for(int i=0; i<clubs.size(); i++)
for(int j=0; j<clubs[i].members.size();j++)
for(int k=0; k<users.size();k++)
这里,i可以改为 ci, j改为 mi, k改为 ui,这样,就能够很清晰的明白每个循环变量是什么意思了。类似的,选用 r -> row, c -> column 用在矩阵运算中。
3. 选用具体的名字而不是抽象的
比如,ServerCanStart()是个抽象的名称,可以改成CanListenOnPort()这个具体的名称。
为防止类拷贝,可以将拷贝构造函数和=运算私有化,谷歌中以前使用宏DISALLOW_EVIL_CONSTRUCTORS(ClassName),这里
#define DISALLOW_EVIL_CONSTRUCTORS(ClassName) \
ClassName(const ClassName&); \
void operator=(const ClassName&);
这里,宏名选用"evil"不太好,这里可以改为 DISALLOW_COPY_AND_ASSIGN(ClassName) …
4. 给名字添加额外讯息。
比如,string id; 这里的id没有附加更多的讯息,若该id是由十六进制数构成,可以改成 hex_id等。
比如,变量是一个表示度量值的数,比如时间,重量。我们可以加上单位信息,这样对于代码就能够更好的看懂。delay -> delay_ms, angle -> degree_cw (顺时针)。
同时,对于某些应用场合,需要给变量加上处理状态的讯息或编码的讯息,比如 password -> plaintext_password,表示未经处理的密码字符串。html -> html_utf8 表示
该网页是采用utf8编码等。
5.选用适合长度的名字
通过变量的使用范围跨度来度量。若是范围小,比如几行之间,可以选用短名字。
若是范围跨度大,可以选用长点的名字表达更加丰富的信息,但是不能太长了。另外,编程敲打长名字,可以使用编辑器的自动补全功能。 vim中可以用ctrl+p,甚至安装插件,按个tab就行。
另外,对于缩写,应该遵循通用的简称,比如string -> str, document-> doc, evaluation -> eval 等。
最后,可以抛弃无用的字眼,比如在类型转换, ConvertToString() -> ToString(),这里,To就有convert的含义,所以convert可以抛弃。
6. 选用不同名字格式表达不同的含义,可以参考google编程风格。
static const int kMaxOpenFiles = 100; // 常量使用前面加k表示,而不是全大 写,同宏名区别
class LogReader{ // 类名首字母大写的驼峰法
public:
void OpenFile(string local_file); // 局部变量名使用小写字母加下划线
private:
int offset_; // 类成员变量使用小写,并在最后添加下划线
DISALLOW_COPY_AND_ASSIGN(LogReader); // 宏名全大写
};
名字不能被误解
key idea: Actively scrutinize(细查) your names by asking yourself, "What other meanings could someone interpret from this name?"
比如: filter() 过滤。 这里,过滤有双重理解,"to pick out" or "to get rid of" 。 若是 to pick out ,则改为 select, 若是 to get rid of, 则改为 exclude。
比如: clip() 剪除 def clip(text, max_len): 有两种含义: 1、remove length from the end 2、 truncates to a maximum length。同时,这里,max_len也是容易让人歧义,是指 words还是bytes亦或是characters,应该改为 max_chars 等。
filter, length, limit是容易模棱两可的词汇。
1、涉及最大最小的限制时,添加max或min的前缀,比如 max_len, min_len。
2、前闭后闭范围[...],使用 first 和 last
3、前闭后开范围[...),使用 begin 和 end
4、布尔值,添加前缀 has, is, can, should 等表明,布尔值命名不要使用否定式。ex. bool disable_ssl = fasle; -> bool use_ssl = true;
5、不滥用约定熟成的变量名。 比如 get*(), 他应该是运算复杂度为 O(1)的方法,若是使用 getAverage(),它是一个复杂度为O(n)的运算,会误导人们去使用,导致增加时间消耗,应改为computeAverage(),这样,就告诉人们得到average是要compute的,而不是伸手既得的。
比如 list::size(),在stl中,为了统一命名,对list容器采用了size()方法,而list::size()方法是O(n)的,vector::size()是O(1)的,这样就会误导用户使用size()增加时间消耗。庆幸的是,最新的C++标准中,list::size()是O(1)的。
另外,在有多个候选名可以用时,要选择最能表达动机的词语。
Aesthetics 美感
Good source code should be just as "easy on the eyes"
1、重排换行位置得到更加一致和紧致的效果
public class PerformanceTester{
// TcpConnectionSimulator(Throughput, lantency, jitter, packet_loss)
// [kbps] [ms] [ms] [percent]
public static final TcpConnectionSimulation wifi =
new TcpConnectionSimulator( 500, 80, 200, 1);
public static final TcpConnectionSimulation t3_fiber=
new TcpConnectionSimulator( 45000, 10, 0, 0);
public static final TcpConnectionSimulation wifi =
new TcpConnectionSimulator( 100, 400, 250, 5);
}
当然,这里忽略了敲打空白和排列的时间,上述代码肯定很容易看懂。
2、对于重复形式的代码,编写函数,且函数命名有意义。
3、多形参采用列对齐排列。如上面的代码。
4、选择一个固定的排列顺序,比如按字母,按逻辑顺序,并在而后的调用中保持。
5、大段代码按逻辑组合,添加空白行和功能注释,使代码层次鲜明。
6、选用一致的代码风格,并保持。
Knowing what to Comment
key idea: The purpose of commenting is to help the reader know as much as the writer did.
1、什么不该被注释?
a. 不注释能从代码本身迅速传达含义的语句。
b. 不要为了注释而注释,避免这点,应该详细添加表达代码逻辑层次的注释。
c. 不为烂名字而注释,而应该改名字。 good code > bad code + good comments!
2、 记录你的想法
a. 包含“导演评论”,即洞察性的语句,能够避免重复劳动、无意义劳动。同时指出改进的方向。
b. 注释代码中的“瑕疵”
Marker | Typical meaning |
TODO: | 半成品 |
FIXME: | 此处代码有问题 |
HACK: | 诚然不雅的解决问题 |
XXX: | 危险,主要问题在这 |
c. 对常量注释()
d.站在代码阅读者的角度去看
猜测读者会在哪里产生疑惑。
ex.
struct Recoder{
vector<float> data;
...
void Clear(){
vector<float>().swap(data); // why not just data.clear()?
}
}
上述代码中,读者将不太了解为什么要调用空对象的swap方法释放data的空间。这里,是唯一的方式强行使vector将自己的内存归还内存池。所以,应补上注释如下:
// Force vector to relinquish its memory (Look up "STL swap trick")
vector<float>().swap(data);
另外,有一些函数的行为将占据大量的系统资源,需要注释说明其运行环境及限制条件。
e.大框架注释 对整个系统的每个关键环节,节点间联通关系等进行注释,让后来人能够理解代码的意图。
f. 对代码块进行注释,使读者免于陷入细节中。
Making Comments Precise and Compact 注释更加精确紧致
key idea: Comments should have a high information-to-space ratio。 注释应该言简意赅
1.保持代码紧凑。
// The int is the CategoryType
// The first float in the inner pair is the 'score'
// The second is the 'weight'
typedef hash_map<int, pair<float, float> > ScoreMap;
// 这里,三行的注释可以归结成一行
// CategoryType->(score, weight)
2.避免模棱两可的代词。
// Insert the data into the cache, but check if it's too big first
这里,it的指代不明,可能是data,也可能是cache。改进方法:
// 1.将代词补全
// but check if the data is too big first
// 2.将句型重新排列
// If the data is small enough, insert it into the cache
3. 雕琢粗糙的表达。 这个,看语文功底了.
4.精确描述函数行为。 比如一个函数要返回文件的行数,不能仅仅这样注释,应该要注释如何度量何为“行”的标准,如度量'\n'个数(unix),‘\n\r’(Windows)。
5. 采用一些输入输出的样例来阐释特殊情况。这一点,如同acm的题,sample input, sample out ...
6. 陈述代码的高层意图
7. 可以对神秘的函数参数进行内联注释, e.g. Functoin(/* arg = */ ...)
8. 选用信息密度强的词汇表达。也就是言简意赅。