[读书笔记]: 可读代码的艺术
本文地址:https://www.cnblogs.com/hchengmx/p/14826739.html
本文为 《编写可读代码的艺术》
英译《The Art of Readable Code》的读书笔记。
整本书的关键思想是:
让别人理解 你这段代码 所需的时间 “最小化”
Code should be written to minimize the time it would take for someone else to understand it.
第一部分 表面层次的改进
从“表面层次”改进代码,这一部分不涉及 代码的重构。主要包括:
- 选择好的 方法名、函数名;
- 写好的注释
- 代码的整洁性。
1. 选择准确的变量名
1.1 准确的动词
案例1: def getPage(url) 这个函数名
用get就不明显,是从 数据库中得到一个网页呢?还是从 互联网上得到一个网页?要是从互联网中得到一个页面,可以考虑用 fetchPage() 或者 downloadPage;
案例2: 一段操作数据库结果的代码
results = Database.all_objects.filter("year <= 2011")
这里的filter指代的是 “挑出”,还是减少呢,从代码中其实看不出来的。
一些常用词及其替代词汇
send: deliver、dispatch、announce、distribute、route
find: search、extract、locate、discover
start: launch、create、begin、open
make:create、set up、build、generate、compose、add、new
1.2 准确的变量
案例1:类 BinaryTree 定义如下:
class BinaryTree{
int size;
}
这里的 Size就很不明显,是期望返回的是 树的高度,节点数,还是树在内存中所占的空间? 对应应该用更专业的词,Height, NumNodes 或者 MemoryBytes。
几点变量命名建议:
- 推荐用 min 和 max来表示(包含)极限
- 推荐用 first 和 last 来表示包含的范围
- 表示 布尔值的时候,加上 is、has、can、should
2. 避免使用 temp / retval / result
不要用 temp 或者 retval 这样的词。因为这种包括不了更多的信息。(temp这个名字只应用于 短期存在 且 临时性 为其存在因素的变量)
3. 避免在迭代器中使用 i/j/k
因为要是用了多重循环,就很难发现错误。
if(clubs[i].members[k] == users[j])
-> if(clubs[ci].members[mi] == users[ui])
4. 为度量变量加上单位
如果变量是一个度量的话(比如 时间长度、字节数),那么最好把名字带上它的单位
var start = (new Date()).getTime()
-> var start_ms = (new Date()).getTime();
before | after |
---|---|
satrt(int delay) | delay -> delay_desc |
createCache(int six) | size -> size_mb |
throttleDownload(float limit) | limit -> max_kbps |
truncate(text, max_length) | maxLength -> max_chars |
5. 代码美学
选用列对齐,可以比较整洁,也可以更直接的发现拼写错误
details = request.POST.get('detail')
location = request.POST.get('location')
phone = equest.POST.get('phone')
email = request.POST.get('email')
url = request.POST.get('url')
把代码分成 "段落"。
class FrontendServer {
public:
FrontServer();
~FrontServer();
//Handlers
void ViewProfile(HttpRequest* request);
void SaveProfile(HttpRequest* request);
void FindFriends(HttpRequest* request);
//Request/Reply Utilities
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);
}
6. 注释
KEY IDEA
Comments should have a high information-to-space ratio
- 不要添加不必要的注释(从代码就能读出来的含义)
6.1 不要为了注释而注释
// Find the Node in the givin subtree, with the given name, using the given depth
Node* FindNodeInSubtree(Node* subtree, string name, int depth)
添加细节:
// Find a Node with given 'name' or return NULL.
// If depth <=0, only 'subtree' is inspected.
// If depth == N, only 'subtree' and N levels below are inspected.
Node* FindNodeInSubtree(Node* subtree, string name, int depth)
6.2 用注释记录你的想法
标注代码瑕疵
TODO: Stuff I haven’t gotten around to yet
FIXME: Known-broken code here
HACK: Admittedly inelegant solution to a problem
XXX: Danger! major problem here
每个团队都有自己的风格,比如 TODO:只用于重要的问题,也可以用 todo: 或者 mayber-later: 来表示重要的缺陷
6.3 给常量添加注释
// Impose a reasonable limit - no human can read that much anyway.
const int MAX_RSS_SUBSCRIPTION = 1000;
6.4 写出言简意赅的注释
让注释保持紧凑
// The int is the CategoryType
// The first float in the inner pari in the 'score',
// the second is the 'weight'
typedef hash_map<int, pair<float, float>> ScoreMap;
6.5 不要用代词
会导致含义不明。
案例1;
// Insert the data into the cache, but check if it's too big first.
那这里的it指的是数据呢?还是缓存呢?
6.6 举例来说明特别的情况
//...
// Example: Stripe("abba/a/ba", "ab") returns "/a/"
String Stripe(String src, String chars) { ... }
第二部分 简化逻辑
关键思想:把条件、循环以及其他对控制流的改变做得越“自然”越好。运用一种方式使读者不用停下来重读你的代码。
Key Idea: Make all your conditionals, loops, and other changes to control flow as "natural" as possible - written in a way that doesn't make the reader stop and reread your code.
7. 把控制流变得易读
7.1 if
比如我们常用的习惯是 if(length >= 10)
, while(bytes_actual > bytes_expected)
建议:
比较式的左侧:不断变化的
比较式的右侧:常量
7.2 if else
原则
- 先处理 正逻辑,再处理负逻辑。比如 用
if(debug)
而不是if(!debug)
用 三目运算符,替代简单的 if else
time_str += (hour >=12) ? "pm" :"am";
7.3 return
public boolean Contains(String str, String subStr){
if(str == null || substr == null) return false;
if(substr.equals("")) return true;
}
7.4 减少嵌套
嵌套会 大大增加读者的思维负担。
if(user_result == SUCCESS) {
if(premission_result != SUCESS){
reply.WriteErrors("error reading permissions");
reply.Done();
return;
}
reply.WriteErrors("");
else{
reply.WriteErrors(user_result);
}
reply.Done();
}
以上的代码很有可能是 以前的需求是只需要判断一下 "user_result",现在增加了新的需求,还要判断 "premission_result"。
可以改为
if(user_result != SUCCESS){
reply.WriteError(user_result);
reply.Done();
return;
}
if(permission_result != SUCCESS){
reply.WriteError(permission_result);
reply.Done();
return;
}
reply.WriteErrors("");
reply.Done();
8. 拆分超长的表达式
8.1 抽离变量
用变量代替 表达式 里面的东西
final boolean users_owns_document = (request.user.id == document.owner.id);
if(user_owns_document) {
// user can edit this document...
}
if(!user_owns_document) {
// document is read-only...
}
8.2 德摩根定律
!(a||b) 等于 !a&&!b,!(a&&b) 等于 !a||!b
后者比前者的可读性更好。
9. 变量与可读性
9.1 减少变量
可以考虑删除的变量:
- 临时变量,也就是 tmp 变量;
- 控制流变量,也就是
boolean done = false;
while(!done){
if(...){
done = true;
continue;
}
}
应该考虑改成
while( /* condition*/){
...
if(...){
break;
}
}
9.2 减少变量的作用域
要是一个变量,只有一个方法可以用到,就应该把这个变量变为局部变量。
class LargeClass{
string str_;
void Method1(){
str_ = ...;
Method2();
}
void Method1(){
// Uses str_
}
}
就可以改成
class LargeClass{
void Method1(){
string str_ = ...;
Method2();
}
void Method1(){
// Uses str_
}
}
要是一个变量用在if里面,可以适当进行简练。
if(Payment* info = database.ReadPaymentInfo()){
cout << "Use pard: " << info ->amount() <<endl;
}
第三部分 重新组织代码
10. 抽取不相关的子问题
基本原则:
- 看看某个函数或代码块,问问你自己:这段代码高层次的目标是什么?
- 对于每一行代码,问一下:它是直接为了目标而工作吗?这段代码高层次的目标是什么?
- 如果足够的行数在解决不相关的子问题,就应该抽取代码到独立的函数中。
11. 一次只做一件事
假如有一个博客上的投票插件,用户可以给一条评论 按“上” 或 “下”。每条评论的总分为所有投票的和:“up” 代表 +1,“Down”代表分数 -1。
当用户按了一个按钮,会调用以下的JS代码。
vote_changed(old_vote, new_bote);
下面这个函数实现了这个功能,其中用了大量的代码来判断 old_value 和 new_value 的组合情况。
var vote_changed = function(old_vote, new vote){
vat scroe = get_score();
if(new_vote != old_vote){
if(new_vote === 'up'){
score += (old_vote === 'Down' ? 2:1);
} else if(new_vote === 'Down'){
score -= (old_vote === 'Up'? 2:1);
} else if(new_vote === ''){
score += (old_vote === 'Up'?-1:1);
}
}
set_score(score);
}
我们可以考虑把这个问题分成两个任务
- 把投票解析为 数字值;
- 更新分数;
var vote_value = function(vote){
if(vote === 'Up'){
return +1;
}
if(vote === 'Down'){
return -1;
}
return 0;
}
var vote_changed = function(old_vote, new_vote){
var score = get_score();
score -= vote_value(old_vote); //remove the old vote
score += vote_value(new_vote); //add the new vote
set_score(score);
}
12. 把想法变成代码
想象对着一个同事,用“自然语言”来描述代码要做什么。
以下代码用来决定 是否授权用户看到这个页面,要是没有,就告诉用户没有授权。
$is_admin = is_admin_request();
if($document){
if(!$is_admin && ($document['username'] != $_SESSION['username']){
return not_authorized();
})else{
if(!$is_admin){
return not_authorized();
}
}
//continue rendering the page ...
}
首先从自然语言描述着逻辑:
授权你有两种方式:
- 你是管理员;
- 你拥有当前文档;
否则,无法授权给你
if(is_admin_request()){
//authorized
}else if($document && ($(document['username'] == $_SESSION['username']))){
//authorized
}else{
return not_authorized();
}
13. 少写代码
- 不要费力做不需要的feature
- 创建更多更好的“工具代码”,来减少重复代码;
- 让你的项目保持分开的子项目状态;
- 熟悉你周边的库;