竹磬网-邵珠庆の日记 生命只有一次,你可以用它来做些更多伟大的事情–Make the world a little better and easier


273月/130

编写可读代码的艺术

发布在 邵珠庆

代码为什么要易于理解

“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

避免通用的词

tmpretval这样词,除了说明是临时变量和返回值之外,没有任何意义。但是给他加一些有意义的词,就会很明确:

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循环中,ij也有同样让人困惑的时候:

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年之前的呢还是不包含呢?

使用minmax代替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_LIMITMAX_ITEMS_IN_CART,想想哪个更好呢?

使用firstlast来表示闭区间

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")

firstlast含义明确,适宜表示闭区间。

使用beiginend表示前闭后开(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型变量添加ishascanshould可以让含义更清晰,比如:

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", "");
CheckFullName("No Such Guy", "", "no match found");
CheckFullName("John", "", "more than one result");