编写可读代码的艺术

我们的经验是程序员的日常工作的大部分时间都花在一些“基本”的事情上,像是给变量命名、写循环以及在函数级别解决问题。并且这其中很大的一部分是阅读和编辑已有的代码。

关键思想是代码应该写得容易理解。确切地说,使别人用最短的时间理解你的代码。
是什么让代码变得“更好”

return exponent >= 0 ? mantissa * (1 << exponent) : mantissa / (1 << -exponent);

if (exponent >= 0) {
return mantissa * (1 << exponent); } else {
return mantissa / (1 << -exponent); }

第一个版本更紧凑,但第二个版本更直白。哪个标准更重要呢?一般情况下,在写代码时你如何来选择?

代码的写法应当使别人理解它所需的时间最小化。

尽管减少代码行数是一个好目标,但把理解代码所需的时间最小化是一个更好的目标。

要经常地想一想其他人是不是会觉得你的代码容易理解,这需要额外的时间。这样做就需要你打开大脑中从前在编码时可能没有打开的那部分功能。

第一部分 表面层次的改进

“表面层次”的改进开始:选择好的名字、写好的注释以及把代码整洁地写成更好的格式。
这些改变很容易应用。你可以在“原位”做这些改变而不必重构代码或者改变程序的运行方式。
第2章 把信息装到名字里

把信息装入名字中。
选择专业的词
Thread类:

class Thread { void Stop();
... };

Stop()这个名字还可以,但根据它到底做什么,可能会有更专业的名字。例如,你可以叫它Kill(),如果这是一个重量级操作,不能恢复。或者你可以叫它Pause()
send
deliver、dispatch、announce、distribute
find、search、extract、locate、recoverstart
launch、create、begin、open
make、create、set up、build、generate、compose、add、new
避免像tmp和retval这样泛泛的名字

var euclidean_norm = function (v) {
var retval = 0.0; for (var i = 0; i < v.length; i += 1)
retval += v[i] * v[i]; return Math.sqrt(retval);
};

好的名字应当描述变量的目的或者它所承载的值
sum_squares += v[i];

if (right < left) {
tmp = right; right = left;
left = tmp; }

在这种情况下,tmp这个名字很好。这个变量唯一的目的就是临时存储,它的整个生命周期只在几行代码之间。tmp这个名字向读者传递特定信息,也就是这个变量没有其他职责,它不会被传到其他函数中或者被重置以反复使用

tmp这个名字只应用于短期存在且临时性为其主要存在因素的变量

or (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;

用具体的名字代替抽象的名字
假设你有一个内部方法叫做ServerCanStart(),它检测服务是否可以监听某个给定的TCP/IP端口。然而ServerCanStart()有点抽象。CanListenOnPort()就更具体一些。这个名字直接地描述了这个方法要做什么事情。

为名字附带更多信息
但不管你在名中挤进任何额外的信息,每次有人看到这个变量名时都会同时看到这些信息
已转化为UTF-8格式的html字节
htmlhtml_utf8

如果这是一个需要理解的关键信息,那就把它放在名字里。

名字应该有多长
当选择好名字时,有一个隐含的约束是名字不能太长

在小的作用域里可以使用短的名字
如果一个标识符有较大的作用域,那么它的名字就要包含足够的信息以便含义更清楚

团队的新成员是否能理解这个名字的含义?如果能,那可能就没有问题
利用名字的格式来传递含义

对于下划线、连字符和大小写的使用方式也可以把更多信息装到名字中。
2021-01-08
当给一个HTML标记加id或者class属性时,下划线和连字符都是合法的值。一个可能的规范是用下划线来分开ID中的单词
总结
2021-01-08
使用专业的单词——例如,不用Get,而用Fetch或者Download可能会更好,这由上下文决定。
避免空泛的名字,像tmp和retval,除非使用它们有特殊的理由。使用具体的名字来更细致地描述事物——Server Can Start()这个名字就比CanListenOnPort更不清楚。
给变量名带上重要的细节——例如,在值为毫秒的变量后面加上_ms,或者在还需要转义的,未处理的变量前面加上raw_。为作用域大的名字采用更长的名字——不要用让人费解的一个或两个字母的名字来命名在几屏之间都可见的变量。对于只存在于几行之间的变量用短一点的名字更好。
有目的地使用大小写、下划线等——例如,你可以在类成员和局部变量后面加上"_"来区分它们。
给布尔值命名
2021-01-06
为布尔变量或者返回布尔值的函数选择名字时,要确保返回true和false的意义很明确
2021-01-08

通常来讲,加上像is、has、can或should这样的词,可以把布尔值变得更明确。
总结
2021-01-06
不会误解的名字是最好的名字——阅读你代码的人应该理解你的本意,并且不会有其他的理解
第4章 审美
2021-01-06
使用一致的布局,让读者很快就习惯这种风格。
让相似的代码看上去相似。把相关的代码行分组,形成代码块
重新安排换行来保持一致和紧凑
2021-01-08

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); }

选一个有意义的顺序,始终一致地使用它
2021-01-08
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表单中字段的顺序相匹配。
从“最重要”到“最不重要”排序。按字母顺序排序。

把声明按块组织起来
2021-01-06
不要把所有的方法都放到一个巨大的代码块中,应当按逻辑把它们分成组
2021-01-08


class FrontendServer {
public: FrontendServer();
~FrontendServer();
// 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); };

总结
2021-01-08
如果多个代码块做相似的事情,尝试让它们有同样的剪影。
把代码按“列”对齐可以让代码更容易浏览。如果在一段代码中提到A、B和C,那么不要在另一段中说B、C和A。选择一个有意义的顺序,并始终用这样的顺序。
用空行来把大块代码分成逻辑上的“段落”。

第5章 该写什么样的注释
2021-01-06
注释的目的是尽量帮助读者了解得和作者一样多。
什么不需要注释
2021-01-08
不要为那些从代码本身就能快速推断的事实写注释。
2021-01-08
不要给不好的名字加注释——应该把名字改好
2021-01-06
好代码>坏代码+好注释。
记录你的思想
2021-01-08
代码始终在演进,并且在这过程中肯定会有瑕疵。不要不好意思把这些瑕疵记录下来。
2021-01-08
标记
通常的意义TODO:
我还没有处理的事情FIXME:
已知的无法运行的代码HACK:
对一个问题不得不采用的比较粗糙的解决方案XXX:
危险!这里有重要的
2021-01-06
注释给读者带来对代码质量和当前状态的宝贵见解,甚至可能会给他们指出如何改进代码的方向。
2021-01-08
当定义常量时,通常在常量背后都有一个关于它是什么或者为什么它是这个值的“故事”。例如,你可能会在代码中看到如下常量:
NUM_THREADS = 8
站在读者的角度
2021-01-06
我们的建议是你可以做任何能帮助读者更容易理解代码的事。这可能也会包含对于“做什么”、“怎么做”或者“为什么”的注释(或者同时注释这三个方面)
最后的思考——克服“作者心理阻滞”
2021-01-08
很多程序员不喜欢写注释,因为要写出好的注释感觉好像要花很多工夫。当作者有了这种“作者心理阻滞”,最好的办法就是现在就开始写。
2021-01-07
当你经常写注释,你就会发现步骤1所产生的注释变得越来越好,最后可能不再需要做任何修改了。并且通过早写注释和常写注释,你可以避免在最后要写一大堆注释这种令人不快的状况

第7章 把控制流变得易读
2021-01-08
把条件、循环以及其他对控制流的改变做得越“自然”越好。运用一种方式使读者不用停下来重读你的代码。
条件语句中参数的顺序
2021-01-08
下面的两段代码哪个更易读?

if (length >= 10)还是
if (10 <= length)

2021-01-08
比较的左侧
比较的右侧“被问询的”表达式,它的值更倾向于不断变化
用来做比较的表达式,它的值更倾向于常量
?:条件表达式(又名“三目运算符”)
2021-01-08
return exponent >= 0 ? mantissa * (1 << exponent) : mantissa / (1 << -exponent);
2021-01-08
相对于追求最小化代码行数,一个更好的度量方法是最小化人们理解它所需的时间。
2021-01-08
if (exponent >= 0) {
return mantissa * (1 << exponent); } else {
return mantissa / (1 << -exponent); }

从函数中提前返回
2021-01-07
从函数中提前返回没有问题,而且常常很受欢迎
2021-01-08
public boolean Contains(String str, String substr) {
if (str == null || substr == null) return false; if (substr.equals("")) return true;
… }
最小化嵌套
2021-01-08

if (user_result == SUCCESS) {
if (permission_result != SUCCESS) { reply.WriteErrors("error reading permissions");
reply.Done(); return;
} reply.WriteErrors("");
} else { reply.WriteErrors(user_result);
}

2021-01-08
if (user_result != SUCCESS) {
reply.WriteErrors(user_result); reply.Done();
return; }
if (permission_result != SUCCESS) {
reply.WriteErrors(permission_result); reply.Done();
return; }
reply.WriteErrors("");
reply.Done();

2021-01-08
当你对代码做改动时,从全新的角度审视它,把它作为一个整体来看待。

用做解释的变量
2021-01-08
拆分表达式最简单的方法就是引入一个额外的变量,让它来表示一个小一点的子表达式。这个额外的变量有时叫做“解释变量”,因为它可以帮助解释子表达式的含义。

使用德摩根定理
2021-01-07
德摩根定理
2021-01-08
if (!(file_exists && !is_protected)) Error(“Sorry, could not read file.”);
那么可以把它改写成:if (!file_exists || is_protected) Error(“Sorry, could not read file.”);
拆分巨大的语句
2021-01-08
很多表达式是一样的,这意味着可以把它们提取出来作为函数开头的总结变量(这同时也是一个DRY——Don’t Repeat Yourself的例子):

第9章 变量与可读性
2021-01-08

  1. 变量越多,就越难全部跟踪它们的动向。
  2. 变量的作用域越大,就需要跟踪它的动向越久。
    1. 变量改变得越频繁,就越难以跟踪它的当前值。
      减少变量
      2021-01-08
var remove_one = function (array, value_to_remove) {
var index_to_remove = null; for (var i = 0; i < array.length; i += 1) {
if (array[i] === value_to_remove) { index_to_remove = i;
break; }
} if (index_to_remove !== null) {
array.splice(index_to_remove, 1); }
};
缩小变量的作用域
2021-01-08
submitted = false; // Note: global variable
var submit_form = function (form_name) {
if (submitted) { return; // don't double-submit the form
} ...
submitted = true; };
2021-01-08
var submit_form = (function () {
var submitted = false; // Note: can only be accessed by the function below
return function (form_name) { if (submitted) {
return; // don't double-submit the form }
... submitted = true;
}; }());

2021-01-07
因为读者在读到后面之前不需要知道所有变量,所以可以简单地把每个定义移到对它的使用之前
2021-01-08

def ViewFilteredReplies(original_id):
root_message = Messages.objects.get(original_id) root_message.view_count += 1
root_message.last_view_time = datetime.datetime.now() root_message.save()
all_replies = Messages.objects.select(root_id=original_id)
filtered_replies = [] for reply in all_replies:
if reply.spam_votes <= MAX_SPAM_VOTES: filtered_replies.append(reply)
return filtered_replies

第10章 抽取不相关的子问题
2021-01-08
你每天可能都会把代码抽取到单独的函数中。但在本章中,我们决定关注抽取的一个特别情形:不相关的子问题,在这种情形下抽取出的函数无忧无虑,并不关心为什么会调用它。
介绍性的例子:findClosestLocation()
2021-01-08

// Return which element of 'array' is closest to the given latitude/longitude.
// Models the Earth as a perfect sphere. var findClosestLocation = function (lat, lng, array) {
var closest; var closest_dist = Number.MAX_VALUE;
for (var i = 0; i < array.length; i += 1) { // Convert both points to radians.
var lat_rad = radians(lat); var lng_rad = radians(lng);
var lat2_rad = radians(array[i].latitude); var lng2_rad = radians(array[i].longitude);
// Use the "Spherical Law of Cosines" formula.
var dist = Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) + Math.cos(lat_rad) * Math.cos(lat2_rad) *
Math.cos(lng2_rad - lng_rad));
2021-01-08
if (dist < closest_dist) {
closest = array[i]; closest_dist = dist;
} }
return closest; };
2021-01-08
var findClosestLocation = function (lat, lng, array) {
var closest; var closest_dist = Number.MAX_VALUE;
for (var i = 0; i < array.length; i += 1) { var dist = spherical_distance(lat, lng, array[i].latitude, array[i].longitude);
if (dist < closest_dist) { closest = array[i];
closest_dist = dist; }
} return closest;
};

创建大量通用代码
2021-01-07
自顶向下编程是一种风格,先设计高层次模块和函数,然后根据支持它们的需要来实现低层次函数
2021-01-07
自底向上编程尝试首先预料和解决所有的子问题,然后用这些代码段来建立更高层次的组件
简化已有接口
2021-01-08
人人都爱提供整洁接口的库——那种参数少,不需要很多设置并且通常只需要花一点工夫就可以使用的库。
2021-01-08

var max_results;
var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) {
var c = cookies[i]; c = c.replace(/^[ ]+/, ''); // remove leading spaces
if (c.indexOf("max_results=") === 0) max_results = Number(c.substring(12, c.length));
}
2021-01-08
var max_results = Number(get_cookie("max_results"));

过犹不及
2021-01-08
像我们在本章的开头所说的那样,我们的目标是“积极地发现和抽取不相关的子问题”。我们说“积极地”是因为大多数程序员不够积极。但也可能会过于积极,导致过犹不及
总结
2021-01-08
把一般代码和项目专有的代码分开”。其结果是,大部分代码都是一般代码。通过建立一大组库和辅助函数来解决一般问题,剩下的只是让你的程序与众不同的核心部分。
第11章 一次只做一件事
2021-01-07
应该把代码组织得一次只做一件事情
任务可以很小
2021-01-08

var vote_changed = function (old_vote, new_vote) {
var score = 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);
};
这段代码好像是只做了一件事情(更新分数),但实际上是同时做了两件事:
4. 把old_vote和new_vote解析成数字值。
5. 更新分数。
我们可以分开解决每个任务来使代码变简单。下面的代码解决第一个任务,把投票解析成数字值:var vote_value = function (vote) {
if (vote === 'Up') {
return +1; }
if (vote === 'Down') { return -1;
} return 0;
};

总结
2021-01-08
如果你有很难读的代码,尝试把它所做的所有任务列出来。其中一些任务可以很容易地变成单独的函数(或类)。其他的可以简单地成为一个函数中的逻辑“段落”。
第12章 把想法变成代码
2021-01-08
如果你不能把一件事解释给你祖母听的话说明你还没有真正理解它。
阿尔伯特·爱因斯坦
2021-01-08
当把一件复杂的事向别人解释时,那些小细节很容易就会让他们迷惑。把一个想法用“自然语言”解释是个很有价值的能力,因为这样其他知识没有你这么渊博的人才可以理解它。
清楚地描述逻辑
2021-01-08

$is_admin = is_admin_request();
if ($document) { if (!$is_admin && ($document['username'] != $_SESSION['username'])) {
return not_authorized(); }
} else { if (!$is_admin) {
return not_authorized(); }
}

if (is_admin_request()) { // authorized
} elseif ($document && ($document['username'] == $_SESSION['username'])) { // authorized
} else { return not_authorized();
}

授权你有两种方式:
6. 你是管理员 2. 你拥有当前文档(如果有当前文档的话)
否则,无法授权你

第13章 少写代码
2021-01-08
知道什么时候不写代码可能对于一个程序员来讲是他所要学习的最重要的技巧。你所写的每一行代码都是要测试和维护的。
保持小代码库
2021-01-08
删除没用的代码
园丁经常修剪植物以让它们活着并且生长。同样地,修剪掉碍事和没用的代码也是个好主意。
2021-01-08
删除独立的函数很简单,但有时“无用代码”实际上交织在你的项目中,你并不知情
总结
2021-01-08
从项目中消除不必要的功能,不要过度设计。
重新考虑需求,解决版本最简单的问题,只要能完成工作就行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值