概述
第1章 代码应当易于理解
可读性基本定理:代码的写法应当使别人理解它所需的时间最小化。
尽管减少代码行数是一个好目标,但把理解代码所需的时间最小化是一个更好的目标。
第一部分 表面层次的改进
选择好的名字、写好的注释以及把代码整洁地写成更好的格式。这些改变很容易应用。你可以在“原位”做这些改变而不必重构代码或改变程序的运行方式。你还可以增量地做这些修改却不需要投入大量的时间。
第2章 把信息塞进名字里
读者仅通过读到名字就可以获得大量信息。
- 使用专业的单词,如不用Get而用Fetch或者Download可能会更好,这由上下文决定。
- 避免空泛的名字,像tmp和retval,除非使用它们有特殊的理由。
- 使用具体的名字来更细致地描述事物,ServerCanStart()这个名字就比CanListenOnPort更不清楚。
- 给变量名带上重要的细节,如在值为毫秒的变量后面加上_ms,或者在还需要转义的,未处理的变量前面加上raw。
- 为作用域大的名字采用更长的名字,不要用让人费解的一个或两个字母的名字来命名在几屏之间都可见的变量。对于只存在于几行之间的变量用短一点的名字更好。
- 有目的地使用大小写、下划线等,例如,可以在类成员和局部变量后面加上"_"来区分。
第三章 不会误解的名字
要多问自己几遍:这个名字会被别人解读成其他的含义吗?要仔细审视这个名字。
推荐使用min和max来表示(包含)极限。如购物车里最多不能超过10件物品:
MAX_ITEMS_IN_CART = 10
if shopping_cart.num_items() > MAX_ITEMS_IN_CART:
Error("Too many items in cart.")
推荐用first和last来表示包含的范围
推荐用begin和end来表示包含/排除范围
给布尔值命名,通常加上is, has, can或should这样的词可以把布尔值变量变得更明确。
与使用者的期望相匹配,很多程序员习惯了把以get开始的方法当做“轻量级访问器”这样的用法,它只是简单地返回一个内部成员变量。对于size()也是一样,如果违背这个习惯很可能会误导用户。
第4章 审美
如何用好的留白、对齐及顺序来让代码变得更易读。有三条原则:
- 使用一致的布局,让读者很快就习惯这种风格。
- 让相似的代码看上去相似。
- 把相关的代码行分组,形成代码块。
如用Java评估程序在不同的网路连接速度下的行为,有一个TcpConnectionSimulator,它的构造函数有4个参数:
1.网络连接的速度(Kbps)
2.平均延时(ms)
3.延时的“抖动”(ms)
4.丢包率(ms)
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.让变量的顺序与对应的HTML表单中<input>字段的顺序相匹配。
2.从“最重要”到“最不重要”排序。
3.按字母顺序排序。
把声明按块组织起来。不要把所有的方法都放到一个巨大的代码块中,应当按逻辑把它们分成组,像以下这样:
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 ReplayNotFound(HttpRequest* request, string error);
//Database Helpers
void OpenDatabase(string location, string user);
void CloseDatabase(string location);
}
把代码分成“段落”:
1.它是一种把相似的想法放在一起并与其他想法分开的方法。
2.它提供了可见的“脚印”,如果没有它,会很容易找不到你读到哪里了。
3.它便于段落之间的导航。
def suggest_new_friends(user, email_password):
#Get the user's friends' email addresses.
friends = user.friends()
friend_emails = set(f.email for f in friends)
#Import all email addresses from this user's email account.
contacts = import_contacts(user, email_password)
contact_emails = set(c.email for c in contacts)
#Find matching users that they aren't already frends with.
non_friend_emails = contact_emails - friend_emails
suggest_friends = User.objects.select(email_in = non_friend_emails)
#Display these lists on the page.
dispaly['user'] = user
dispaly['friend'] = friends
dispaly['suggested_friends'] = suggest_friends
return render("suggested_friends.html", dispaly)
一致的风格比“正确”的风格更重要。
第5章 该写什么样的注释
不要为那些从代码本身就能快速推断的事实写注释。不要为了注释而注释。
不要给不好的名字加注释——应该把名字改好。
很多好的注释仅通过“记录你的想法”就能得到,也就是那些你在写代码时有过的重要的想法。
标记 | 通常的意义 |
TODO | 我还没有处理的事情 |
FIXME | 已知的无法运行的代码 |
HACK | 对一个问题不得不采用比较粗糙的解决方案 |
XXX | 危险!这里有重要的问题 |
当定义常量时,通常在常量背后都有一个关于它是什么或者为什么它是这个值的“故事”,如
image_quality = 0.72; //users thought 0.72 gave the best size/quality tradeoff
你应该记录下来的想法包括:
- 对于为什么代码写成这样而不是那样的内在理由(“指导性批注”)。
- 代码中的缺陷,使用想TODO:或者XXX:这样的标记。
- 常量背后的故事,为什么是这个值。
站在读者的立场上思考:
- 预料到代码中哪些部分会让读者说:“啊?”并且给他们加上注释。
- 为普通读者意料之外的行为加上注释。
- 在文件/类的级别上使用“全局观”注释来解释所有的部分是如何一起工作的。
- 用注释来总结代码块,使读者不致迷失在细节中。
第6章 写出言简意赅的注释
本章是关于如何把更多的信息转入更小的空间里。下面是一些具体的提示:
- 当像it和this这样的代词可能指代多个事物时,避免使用它们。
- 尽量精确地描述函数的行为。
- 在注释中用精心挑选的输入/输出例子进行说明。
- 声明代码的高层次意图,而非明细的细节。
- 用嵌入的注释(如Founction(/*arg = */...))来解释难以理解的函数参数。
- 用含义丰富的词来使注释简洁。
第二部分 简化循环和逻辑
第7章 把控制流变得易读
比较的左侧 | 比较的右侧 |
“被询问的”表达式,它的值更倾向于不断变化 | 用来做比较的表达式,它的值更倾向于常量 |
相对于追求最小化代码行数,一个更好的度量方法是最小化人们理解它所需的时间。
默认情况下都用if/else。三目运算符?:只有在最简单的情况下使用。
在实践中,编程语言和库的结构让代码在“幕后”运行,或者让流程难以理解:
编程结构 | 高层次程序流程是如何变得不清晰的 |
线程 | 不清楚什么时间执行什么代码 |
信号量/中断处理程序 | 有些代码随时都有可能执行 |
异常 | 可能会从多个函数调用中向上冒泡一样地执行 |
函数指针和匿名函数 | 很难知道到底会执行什么代码,因为在编译时还没有决定 |
虚方法 | object.virtualMethod()可能会调用一个未知子类的代码 |
有几种方法可以让代码的控制流更易读:
- 在写一个比较时(while(byted_expected > bytes_received)),把改变的值写在左边并且把更稳定的值写在右边更好一些(while (bytes_received < bytes_expected))。
- 你也可以重新排列if/else语句中的语句块。通常来讲,先处理正确的/简单的/有趣的情况。有时这些准则会冲突,但是当不冲突时,这是要遵循的经验法则。
- 某些编程结构,像三目运算符(:?)、do/while循环,以及goto经常会导致代码的可读性变差。最好不要使用它们,因为总是有更整洁的代替方式。
- 嵌套的代码块需要更加集中精力去理解。每层新的嵌套都需要读者把更多的上下文“压入栈”。应该把它们改写成更加“线性”的代码来避免深嵌套。
- 通常来讲提早返回可以减少嵌套并让代码整洁。“保护语句”(在函数顶部处理简单的情况时)尤其有用。
第8章 拆分超长的表达式
很难思考巨大的表达式。一个简单的技术是引入“解释变量”来代表较长的子表达式。这种方式有三个好处:
- 它把巨大的表达式拆成小段。
- 它通过用简单的名字描述子表达式来让代码文档化。
- 它帮助读者识别代码中的主要概念。
另一个技术是用德摩根定理来操作逻辑表达式——这个技术有时可以把布尔表达式用更整洁的方式重写(例如if(!(a && !b))变成if(!a || b))。
第9章 变量与可读性
变量的草率运用会让程序更难理解:
- 变量越多,就越难全部跟踪它们的动向。
- 变量的作用域越大,就需要跟踪它的动向越久。
- 变量改变得越频繁,就越难以跟踪它的当前值。
你可以通过减少变量的数量和让它们尽量“轻量级”来让代码更有可读性。具体有:
- 减少变量,即那些妨碍的变量,如中间结果变量。
- 减小每个变量的作用域,越小越好。把变量移到一个有最少代码可以看到它的地方。眼不见,心不烦。
- 只写一次的变量更好。那些只设置一次值的变量(或者const、final、常量)使得代码更容易理解。
第三部分 重新组织代码
第10章 抽取不相关的子问题
把一般代码和项目专有的代码分开。其结果是,大部分代码都是一般代码,通过建立一大组库和辅助函数来解决一般问题,剩下的只是让你的程序与众不同的核心部分。
第11章 一次只做一件事
如果你有很难读的代码,尝试把它所做的所有任务列出来。其中一些任务可以很容易地变成单独的函数(或类)。其他的可以简单地变成一个函数中的逻辑“段落”。具体如何拆分这些任务没有它们已经分开这个事实那样重要。难的是要准确地描述你的程序所做的所有这些小事情。
第12章 把想法变成代码
用自然语言描述程序然后用这个描述来帮助你写出更自然的代码。这个技巧出人意料地简单,但很强大。看到你在描述中所用的词和短语还可以帮助你发现哪些子问题可以拆分出来。
第13章 少写代码
让你的代码库越小,越轻量级越好,就算你的项目在增长。那么你就要:
- 创建越多越好的“工具”代码来减少重复代码。
- 减少无用代码或没有用的功能。
- 让你的项目保持分开的子项目状态。
- 总的来说,要小心代码的“重量”。让它保持又轻又灵。
可以通过以下方法避免编写新代码:
- 从项目中消除不必要的功能,不要过度设计。
- 重新考虑需求,解决版本最简单的问题,只要能完成工作就行。
- 经常性地通读标准库的整个API,保持对他们的熟悉程度。
第四部分 精选话题
第14章 测试与可读性
在测试代码中,可读性仍然很重要。如果测试的可读性很好,其结果是它们也会变得很容易写,因此大家会写更多的测试。并且,如果你把事实代码设计得容易测试,代码的整个设计会变得更好。
以下是如何改进测试的几个具体要点:
- 每个测试的最高一层应该越简明越好。最好每个测试的输入/输出可以用一行代码来描述。
- 如果测试失败了,它所发出的错误信息应该能让你容易跟踪并修正这个bug。
- 使用最简单的并且能够完整运用代码的测试输入。
- 给测试函数取一个有完整描述性的名字,以使每个测试所测到的东西很明确。不要用Test1(),而用像Test_<FunctionName>_<Situation>这样的名字。
最重要的是,要使它易于改动和增加新的测试。
第15章 设计并改进“分钟/小时计数器”
回顾最后的MinuteHourCounter设计所走过的路。这是个典型的代码片段演进过程。
首先我们从编写一个幼稚的方案开始。这帮助我们意识到两个设计上的挑战:速度和内存使用情况。
接下来,我们尝试用“传送带”设计方案。这种设计改进了速度和内存使用情况,但对于高性能应用来讲还是不够好。并且,这个设计不是很灵活:让这段代码能处理其他的时间间隔需要很多工作。
我们最终的设计解决了前面的问题,通过把问题拆分成子问题。下面是创建的三个类,自下而上,以及每个类所解决的子问题:
ConveyorQueue
一个最大长度的队列,可以“移位”并且维护其总和。
TrailingBucketCounter
根据过去了多少时间来移动ConveyorQueue,并且按所给的精度来维护单一(最后)的时间间隔的计数。
MinuteHourCounter
简单地包含两个TrailingBucketCounters,一个用来统计分钟,一个用来统计小时。
最后
以上就是鲜艳猎豹为你收集整理的编写可读代码的艺术的全部内容,希望文章能够帮你解决编写可读代码的艺术所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复