编写可读代码的艺术
代码为什么要易于理解
“Code should be written to minimize the time it would take for someone else to understand it.”
日常工作的事实是:
- 写代码前的思考和看代码的时间远大于真正写的时间
- 读代码是很平常的事情,不论是别人的,还是自己的,半年前写的可认为是别人的代码
- 代码可读性高,很快就可以理解程序的逻辑,进入工作状态
- 行数少的代码不一定就容易理解
- 代码的可读性与程序的效率、架构、易于测试一点也不冲突
整本书都围绕“如何让代码的可读性更高”这个目标来写。这也是好代码的重要标准之一。
如何命名
变量名中应包含更多信息
使用含义明确的词,比如用download
而不是get
,参考以下替换方案:
1
2
3
4
|
send -> deliver, dispatch, announce, distribute, route find -> search, extract, locate, recover start -> lanuch, create, begin, open make -> create,set up, build, generate, compose, add, new |
避免通用的词
像tmp
和retval
这样词,除了说明是临时变量和返回值之外,没有任何意义。但是给他加一些有意义的词,就会很明确:
1
2
3
|
tmp_file = tempfile.NamedTemporaryFile() ... SaveData(tmp_file, ...) |
不使用retval而使用变量真正代表的意义:
1
|
sum_squares += v[i]; // Where's the "square" that we're summing? Bug! |
嵌套的for循环中,i
、j
也有同样让人困惑的时候:
1
2
3
4
|
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++) if (clubs[i].members[k] == users[j]) cout << "user[" << j << "] is in club[" << i << "]" << endl; |
换一种写法就会清晰很多:
1
|
if (clubs[ci].members[mi] == users[ui]) # OK. First letters match. |
所以,当使用一些通用的词,要有充分的理由才可以。
使用具体的名字
CanListenOnPort
就比ServerCanStart
好,can start比较含糊,而listen on port确切的说明了这个方法将要做什么。
--run_locally
就不如--extra_logging
来的明确。
增加重要的细节,比如变量的单位_ms
,对原始字符串加_raw
如果一个变量很重要,那么在名字上多加一些额外的字就会更加易读,比如将string id; // Example: "af84ef845cd8"
换成string hex_id;
。
1
2
3
4
|
Start( int delay) --> delay → delay_secs CreateCache( int size) --> size → size_mb ThrottleDownload( float limit) --> limit → max_kbps Rotate( float angle) --> angle → degrees_cw |
更多例子:
1
2
3
4
|
password -> plaintext_password comment -> unescaped_comment html -> html_utf8 data -> data_urlenc |
对于作用域大的变量使用较长的名字
在比较小的作用域内,可以使用较短的变量名,在较大的作用域内使用的变量,最好用长一点的名字,编辑器的自动补全都可以很好的减少键盘输入。对于一些缩写前缀,尽量选择众所周知的(如str),一个判断标准是,当新成员加入时,是否可以无需他人帮助而明白前缀代表什么。
合理使用_
、-
等符号,比如对私有变量加_
前缀。
1
2
3
4
5
6
7
8
|
var x = new DatePicker(); // DatePicker() 是类的"构造"函数,大写开始 var y = pageHeight(); // pageHeight() 是一个普通函数 var $all_images = $( "img" ); // $all_images 是jQuery对象 var height = 250 ; // height不是 //id和class的写法分开 <div id= "middle_column" class = "main-content" > ... |
命名不能有歧义
命名的时候可以先想一下,我要用的这个词是否有别的含义。举个例子:
1
|
results = Database.all_objects.filter( "year <= 2011" ) |
现在的结果到底是包含2011年之前的呢还是不包含呢?
使用min
、max
代替limit
1
2
3
4
5
6
7
|
CART_TOO_BIG_LIMIT = 10 if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT: Error( "Too many items in cart." ) MAX_ITEMS_IN_CART = 10 if shopping_cart.num_items() > MAX_ITEMS_IN_CART: Error( "Too many items in cart." ) |
对比上例中CART_TOO_BIG_LIMIT
和MAX_ITEMS_IN_CART
,想想哪个更好呢?
使用first
和last
来表示闭区间
1
2
3
4
|
print integer_range(start= 2 , stop= 4 ) # Does this print [ 2 , 3 ] or [ 2 , 3 , 4 ] (or something else )? set.PrintKeys(first= "Bart" , last= "Maggie" ) |
first
和last
含义明确,适宜表示闭区间。
使用beigin
和end
表示前闭后开(2,9))区间
1
2
3
|
PrintEventsInRange( "OCT 16 12:00am" , "OCT 17 12:00am" ) PrintEventsInRange( "OCT 16 12:00am" , "OCT 16 11:59:59.9999pm" ) |
上面一种写法就比下面的舒服多了。
Boolean型变量命名
1
|
bool read_password = true ; |
这是一个很危险的命名,到底是需要读取密码呢,还是密码已经被读取呢,不知道,所以这个变量可以使用user_is_authenticated
代替。通常,给Boolean型变量添加is
、has
、can
、should
可以让含义更清晰,比如:
1
2
|
SpaceLeft() --> hasSpaceLeft() bool disable_ssl = false --> bool use_ssl = true |
符合预期
1
2
3
4
5
6
7
|
public class StatisticsCollector { public void addSample( double x) { ... } public double getMean() { // Iterate through all samples and return total / num_samples } ... } |
在这个例子中,getMean
方法遍历了所有的样本,返回总额,所以并不是普通意义上轻量的get
方法,所以应该取名computeMean
比较合适。
漂亮的格式
写出来漂亮的格式,充满美感,读起来自然也会舒服很多,对比下面两个例子:
1
2
3
4
5
6
7
8
9
10
11
12
|
class StatsKeeper { public : // A class for keeping track of a series of doubles void Add( double d); // and methods for quick statistics about them private : int count; /* how many so far */ public : double Average(); private : double minimum; list< double > past_items ; double maximum; }; |
什么是充满美感的呢:
1
2
3
4
5
6
7
8
9
10
11
12
|
// A class for keeping track of a series of doubles // and methods for quick statistics about them. class StatsKeeper { public : void Add( double d); double Average(); private : list< double > past_items; int count; // how many so far double minimum; double maximum; }; |
考虑断行的连续性和简洁
这段代码需要断行,来满足不超过一行80个字符的要求,参数也需要注释说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class PerformanceTester { public static final TcpConnectionSimulator wifi = new TcpConnectionSimulator( 500 , /* Kbps */ 80, /* millisecs latency */ 200, /* jitter */ 1 /* packet loss % */); public static final TcpConnectionSimulator t3_fiber = new TcpConnectionSimulator( 45000, /* Kbps */ 10, /* millisecs latency */ 0, /* jitter */ 0 /* packet loss % */); public static final TcpConnectionSimulator cell = new TcpConnectionSimulator( 100, /* Kbps */ 400, /* millisecs latency */ 250, /* jitter */ 5 /* packet loss % */ ); } |
考虑到代码的连贯性,先优化成这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class PerformanceTester { public static final TcpConnectionSimulator wifi = new TcpConnectionSimulator( 500 , /* Kbps */ 80, /* millisecs latency */ 200, /* jitter */ 1 /* packet loss % */); public static final TcpConnectionSimulator t3_fiber = new TcpConnectionSimulator( 45000, /* Kbps */ 10, /* millisecs latency */ 0, /* jitter */ 0 /* packet loss % */); public static final TcpConnectionSimulator cell = new TcpConnectionSimulator( 100, /* Kbps */ 400, /* millisecs latency */ 250, /* jitter */ 5 /* packet loss % */ ); } |
连贯性好一点,但还是太罗嗦,额外占用很多空间:
1
2
3
4
5
6
7
8
9
10
11
12
|
public class PerformanceTester { // TcpConnectionSimulator(throughput, latency, jitter, packet_loss) // [Kbps] [ms] [ms] [percent] public static final TcpConnectionSimulator wifi = new TcpConnectionSimulator( 500 , 80 , 200 , 1 ); public static final TcpConnectionSimulator t3_fiber = new TcpConnectionSimulator( 45000 , 10 , 0 , 0 ); public static final TcpConnectionSimulator cell = new TcpConnectionSimulator( 100 , 400 , 250 , 5 ); } |
用函数封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// Turn a partial_name like "Doug Adams" into "Mr. Douglas Adams". // If not possible, 'error' is filled with an explanation. string ExpandFullName(DatabaseConnection dc, string partial_name, string* error); DatabaseConnection database_connection; string error; assert (ExpandFullName(database_connection, "Doug Adams" , &error) == "Mr. Douglas Adams" ); assert (error == "" ); assert (ExpandFullName(database_connection, " Jake Brown " , &error) == "Mr. Jacob Brown III" ); assert (error == "" ); assert (ExpandFullName(database_connection, "No Such Guy" , &error) == "" ); assert (error == "no match found" ); assert (ExpandFullName(database_connection, "John" , &error) == "" ); assert (error == "more than one result" ); |
上面这段代码看起来很脏乱,很多重复性的东西,可以用函数封装:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
CheckFullName( "Doug Adams" , "Mr. Douglas Adams" , "" ); CheckFullName( " Jake Brown " , "Mr. Jake Brown III" ,
|