一本杂志的布局包含了很多的思想。例如,段落的长度,列的宽度,文章的顺序,还有封面故事等等。一本好的杂志,可以方便的跳着看,或顺着看。
好的源代码也应该“看得顺眼”。这一章,我们将展示如何更好的使用空格,对齐和排序使你的代码更容易阅读。
下面就是我们使用的3个原则:
- 使用风格一致的布局,最好是能让读者习惯的样式
- 让相似的代码看起来也相似
- 把相关的代码组成一个块
为什么美学重要?
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: int GetAvg(int k); private: double minimum; list<double> past_items ;double maximum; };
你理解上面代码的时间要比下面一个更干净的版本的时间要长:
// A class for keeping track of a series of doubles // and methods for quick statistics about them. class StatsKeeper { public: void Add(double d); int GetAvg(int k); private: list<double> past_items; int count; // how many so far double minimum; double maximum; };
很明显,理解那些有美学愉悦性的代码更容易。你想想看,你编程的大部分时间是花在读代码上!你的代码能被浏览得越快,就越容易被其他人使用。
重新布置换行,使代码一致和简洁
假设你在写一个Java的代码来评估你的程序在不同的网速下的表现。你有一个在构造器带四个参数的TcpConnectionSimulator方法:
1.网速(Kbps)
2.平均响应时间(ms)
3.不稳定响应时间(ms)
4、丢包率(percentage)
你的代码需要3个不同的TcpConnectionSimulator实例:
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 % */); };
这段代码需要很多换行来满足80字字母的限制(这是你公司的编码标准)。不幸的是,这使得t3_fiber的定义与旁边的定义看上去不一样了。这段代码的轮廓很奇怪,把人的注意力都无故的吸引到t3_fiber上了。这段代码没有遵循“相似的代码应看起来也相似”得原则。
为了使代码看起来一致,我们可以引进另外的换行符:
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 % */); };
这段代码就有一致的样式,更容易快速浏览。但不好的地方时,它占用了太多行。并且注释重复了3遍。
这里有一种够紧凑的方式:
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); };
我们把注释放到了最上面,然后把所有参数都放在同一行。现在,即使注释不在每个数字的旁边,但数据都排在一个紧凑的表里。
用方法清楚不规则
假设你有一个提供如下方法的个人数据库:
// 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");
这段代码看起来没有美学愉悦感。有些行太长不得不换行。而且代码的轮廓看上去很丑,也没有使用一致的样式。
这个情况下,换行只能做到那么多。一个更大的问题是有很多重复的字符串像“assert(ExpandFullName(database_connection...,”,和"error“等等。为了真正的能改进这段代码,我们需要一个方法,这样代码就能看起来如下:
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");
现在,就能很清晰的知道有四个测试,每个测试都有不同的参数。即使所有的”脏活“都在ChenkFullName()里面,这个方法看起来也不会很坏:
void CheckFullName(string partial_name, string expected_full_name, string expected_error) { // database_connection is now a class member string error; string full_name = ExpandFullName(database_connection, partial_name, &error); assert(error == expected_error); assert(full_name == expected_full_name); }
即使我们的目的是使代码看起来更具美感,这些改变还带来了很多额外额外的好处:
- 他去掉了很多重复的代码,是代码更紧凑
- 一眼就能看到每个测试用例的重要部分(名字和错误字符串)。之前,这些字符串与database_connection和error混在一起,很难一眼看出来。
- 添加新的测试时更容易了。
这个故事的意义在于,使代码“看起来更美”不只是表面上的改进——也肯能帮你更好的组织你的代码。
有用的话使用列对齐
读者更容易浏览对齐的边和列。
有时候,你可以引进“列对齐”使代码更易于阅读。例如,上个例子,ChecnkFullName的参数的空格和对齐可以如下:
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");
这个例子中,第二和第三个参数更容易被分辨出来。
以下例子有一大组的变量定义:
# Extract POST parameters to local variables details = request.POST.get('details') location = request.POST.get('location') phone = equest.POST.get('phone') email = request.POST.get('email') url = request.POST.get('url')
正如你所发现,第三个变量定义有个拼写错误。这样的错误在这样简洁的排列下很容易被发现
在wget代码库里,可选的命令行选项(超过100个)是这样列出来的:
commands[] = { ... { "timeout", NULL, cmd_spec_timeout }, { "timestamping", &opt.timestamping, cmd_boolean }, { "tries", &opt.ntry, cmd_number_inf }, { "useproxy", &opt.use_proxy, cmd_boolean }, { "useragent", NULL, cmd_spec_useragent }, ... }
这样使得列表极易地浏览,从一列跳到另一列。
你需要使用列对齐么?
列的边缘会提供“视觉扶手”让代码更容易被扫描。是个“相似的代码应看起来相似”的好例子“。
但有些程序员不喜欢。一个原因是对齐需要更多的时间来建立和维护。另一个原因是修改的时候会差生巨大的差异——一行修改会导致另外的5行都要修改。
我们的建议是你可以试试。我们的经验里,他没有需要程序员想象的那么多工作。如果真的花了太多精力,你只要不那么做就行了。
选一个有意义的顺序,并一直使用它
这是一个许多情况下,顺序并不影响代码正确性的例子。你可以以任意顺序书写下面5个变量的定义:
details = request.POST.get('details') location = request.POST.get('location') phone = request.POST.get('phone') email = request.POST.get('email') url = request.POST.get('url')
这种情况,把他们按一定顺序排列,而不是随机写,是有帮助的。下面是一些选择:
- 与HTML里<input>字段的顺序保持一致
- 把他们按重要性重大到小排列
- 根据字母顺序排列
不管怎样排列,你必须在你的所有代码里使用这个排序方法。如果你改名排序的方式,就会让人疑惑:
if details: rec.details = details if phone: rec.phone = phone # Hey, where did 'location' go? if email: rec.email = email if url: rec.url = url if location: rec.location = location # Why is it down here now?
把声明组织到一块
大脑天生是以分组和分级的方式思考的。所以,你可以用这个方法帮助你的读者更快理解的代码,
例如,这是个前端服务器的C++类,所以得方法定义如下:
class FrontendServer { public: FrontendServer(); void ViewProfile(HttpRequest* request); void OpenDatabase(string location, string user); void SaveProfile(HttpRequest* request); string ExtractQueryParam(HttpRequest* request, string param); void ReplyOK(HttpRequest* request, string html); void FindFriends(HttpRequest* request); void ReplyNotFound(HttpRequest* request, string error); void CloseDatabase(string location); ~FrontendServer(); };
这段代码没什么不好的地方。但是它的布局不会让读者知道这个类是干嘛的。所有的方法都放在一起,就像与同一件事相关一样。实际上,这些方法可以按逻辑组织成不同的组,像这样:
lass FrontendServer { public: FrontendServer(); ~FrontendServer(); // Handlers void ViewProfile(HttpRequest* request); void SaveProfile(HttpRequest* request); void FindFriends(HttpRequest* request); // Request/Reply Utilties string ExtractQueryParam(HttpRequest* request, string param); void ReplyOK(HttpRequest* request, string html); void ReplyNotFound(HttpRequest* request, string error); // Database Helpers void OpenDatabase(string location, string user); void CloseDatabase(string location); };
这个版面更易理解。也更容易阅读即使他增加了许多行,原因是你能很快弄清楚4个不同的部分,需要的话,可以再阅读每部分的细节。
把代码分段
文章被分成不同的段落有很多原因:
- 这样能把相似的东西放在一起,与其他不同的分开。
- 这样能提供一个视觉的”脚踏石“——没有段落,你一不小心就不知道看到哪里了。
- 它方便你能从一段跳到下一段。
由于以上原因,代码也应该分段。例如,没人会喜欢阅读下面那么一大段代码:
# Import the user's email contacts, and match them to users in our system. # Then display a list of those users that he/she isn't already friends with. def suggest_new_friends(user, email_password): friends = user.friends() friends.sort(key=lambda u: u.display_name) email_contacts = import_contacts(user.email, email_password) emails = [ec.email for ec in email_contacts] contact_users = User.objects.filter(email__in=emails).exclude(email=user.email) suggested_friends = [cu for cu in contact_users if cu not in friends] display['user'] = user display['friends'] = friends display['suggested_friends'] = suggested_friends return render("suggested_friends.html", display)
也许很不明显,但是这个方法有很多不同的独立的步骤。所以把他分成不同的段落是很有用的:
def suggest_new_friends(user, email_password): friends = user.friends() friends.sort(key=lambda u: u.display_name) email_contacts = import_contacts(user.email, email_password) emails = [ec.email for ec in email_contacts] contact_users = User.objects.filter(email__in=emails).exclude(email=user.email) suggested_friends = [cu for cu in contact_users if cu not in friends] display['user'] = user display['friends'] = friends display['suggested_friends'] = suggested_friends return render("suggested_friends.html", display)
就像文章一样,把这段代码分段有很多不同的方式,有些程序员会喜欢长一些的,有些喜欢短的。
对于做很多事的段,最好注释总结一下它在做什么:
def suggest_new_friends(user, email_password): friends = user.friends() friends.sort(key=lambda u: u.display_name) # Import all email addresses from this user's email account email_contacts = import_contacts(user.email, email_password) emails = [ec.email for ec in email_contacts] # Find matching users that they aren't already friends with contact_users = User.objects.filter(email__in=emails).exclude(email=user.email) suggested_friends = [cu for cu in contact_users if cu not in friends] display['user'] = user display['friends'] = friends display['suggested_friends'] = suggested_friends return render("suggested_friends.html", display)
当扫描这段代码的时候,这些注释就能代替下面的代码。
个人风格 VS. 一致性
很多美学选择完全取决于个人风格。例如,对于一个类的括号,可以:
class Logger { ... };
也可以:
class Logger { ... };
一旦选择其中一种,他不会影响代码的阅读性,但是,两种风格同时被使用,可读性是会受影响的。
我们参加的许多项目,感到代码使用了”错误“的风格,但我们仍然遵循原来的项目惯例因为我们知道一致性更重要
总结
所有人都喜欢阅读具体美学愉悦性的代码。通过一致,有意义的方式”格式化“你的代码,它就会更易快速的阅读。
如下是我们讨论的一些技巧:
- 如果一块代码做相似的事情,那么就给他们相似的样子。
- 如果”让你的代码好看“导致更有意义的改进,那很好!
- 把代码按”列“排成一线,能让你的代码更易浏览
- 如果在代码中一处提到A,B,C,就不要在另外地方说成B,C,A。选一种有意义的排序并坚持使用它。
- 使用空行把大段的代码分成不同的逻辑”段落“