前言:人生有很多第一次,作为一只不谙世事的小程序媛,第一次踏出校门,开始实习,感触良多,其中,颇为震撼的一点是,原来一直给人印象邋里邋遢的程序猿们,竟然在写代码上有那么多讲究,原来科学看待程序媛的方法真的不是“以貌取人”,而是“以代码取人”。本文为《编写可读代码的艺术》一书的读书笔记,并加上一些个人理解,用于记录,也用于分享共勉。为了编写可读代码,本书主要从四个方面对代码进行提升改进,此文主要记录第一部分,即更多的是对代码表明的一些改进,如名字命名,注释的书写等,并建立代码审美意识。之后还会有同系列的文章用来继续记录后面三部分内容。
编写可读代码何以称作艺术?
相信不少道行尚浅的小程序猿们如我一样,在技术的海洋里混乱遨游之际,只追求写出能够运行的代码,然后窃喜以为已然顿悟,但过一段时间回来再看,即使是自己的亲儿子代码,依旧不知所云。为什么呢?这里就要说到代码对人的友好性和可读性,优秀的代码具备良好的可读性,编写的代码要使其他人(或者一段时间后的自己)能在最短的时间内理解才行。写代码不仅要在乎宏观架构和整体设计,同时也要注重细节,可以说每个程序员每天更多接触的是是代码的细枝末节,有时一个不小心整个工程就会在一个不起眼的小错误中纠结度过。因此,这样看来,编码,不仅仅是一种技术,也是一门艺术。
如何编写可读代码
- 简化命名、注释和格式的方法,使每行代码都言简意赅。
- 梳理程序中的循环、逻辑和变量来减小复杂度并理清思路。
- 在函数级别解决问题,例如重新组织代码块,使其一次只做一件事。
- 编写有效的测试代码,使其全面简洁,同时可读性更高。
起名字的学问
写代码如同写文章,每个函数每个变量都会有一个名字,以便需要时进行调用,好的名字能够让人一目了然,将信息装到名字里,把名字当做一条小小的注释,能够使得代码更加精简清晰。那么如何起名字,才能携带更多的信息呢?
1.选择专业的词
eg:GetPage(url)函数用于获取页面
Q:从本地缓存获取?从数据库获取?从互联网获取?
---> 从互联网获取:FetchPage()或者 DownloadPage()
2.找到更有表现力的词
可以从同义词中寻找,多思考。
单词 | 更多选择 |
---|---|
send | deliver, dispatch, announce, distribute, route |
find | search, extract, locate, recover |
start | launch, create, begin, open |
make | create, set up, build, generate, compose, add, new |
3.避免像tmp和retval这样泛泛的名字
- 好的名字应当描述变量的目的,或者它承载的值。
eg:sum 变量,代表累加平方值
Q: 谁的和?累加和?平方和?
---> sum_squares - 有时泛泛的名字也有意义,只在有特殊意义时使用,如 tmp 名字只应用于短期存在且临时性为其主要存在因素的变量。
- 循环迭代器的升级
eg: 像 i、j、k 这样的名字常被用于做索引或循环迭代器
Q: 很容易在使用时写错,如 i 写成 j
---> 变量名_循环迭代器 eg:club_i, member_j, user_k
4.用具体的名字代替抽象的名字
eg:ServerCanStart() 函数,检测服务器是否可以监听某个给定的TCP/IP端口
Q: 名字太抽象,范围大
---> CanListenOnPort() 直接描述方法做的事情
5.为名字附带更多信息
- 带单位的值
eg1:包含十六进制的字符串id ---> hex_id
eg2:如果变量是度量(时间长度或字节数)---> start_ms, delay_secs, size_mb, max_kbps, degrees_cw - 附带其他重要属性
情形 | 变量名 | better变量名 |
---|---|---|
一个“纯文本”格式的密码,需要加密后才能进一步使用 | password | plaintext_password |
一条用户提供的注释,需要转义后才能用于显示 | comment | unescaped_comment |
已转换为 UTF-8 格式的 html 字节 | html | html_utf8 |
以“uml方式编码”的输入数据 | data | data_urlenc |
6.利用名字的格式来传递含义
对下划线、连字符和大小写的使用方式可以把更多的信息装到名字中。对不同的实体使用不同格式就像语法高亮显示的形式一样,能够帮助更容易阅读代码。例如:
格式 | 意义 |
---|---|
CamelCase | 表示类名 |
lower_separated | 表示变量名 |
kConstantName | 表示常量 |
MACRO_NAME | 表示宏 |
offset_ | 表示类成员变量 |
名字应该有多长
- 在小的作用域里可以使用短的名字。
如果一个标识符有较大的作用域,那么它的名字就要包含足够的信息以便含义更清楚。 - 长名字也不用担心------编辑器上自动补全功能。
- 首字母缩略词和缩写的使用。
原则:团队新成员能够理解名字含义。 - 丢掉没用的词
eg: ConvertToString() ---> ToString()
准确命名(没有歧义)
使用 min 和 max 来表示(包含)极限
eg:CART_TOO_BIG_LIMIT 是个二义性名字导致“大小差一”问题
---> MAX_ITEMS_IN_CART 表示包含极限的最大值-
用 first 和last 来表示包含的范围
-
用 begin 和 end 来表示包含/排除范围
4、给布尔值命名
(1)通常来讲,加上像 is、has、can 或 should 这样的词,可以把布尔值变得更加明确
eg:bool read_password = ture;
Q: 需要读取密码?已经读取了密码?
---> need_password 或者 user_is_authenticated
(2)避免使用反义名字
eg:bool disable_ssl = false;
---> bool use_ssl = true;
代码审美-组织代码
三条原则:
(1)使用一致的布局,让读者很快就习惯这种风格;
(2)让相似的代码看上去相似;
(3)把相关的代码行分组,形成代码块;
整洁的代码能够提升浏览速度,和使用程度,相反如果代码结构凌乱,很影响代码的阅读和进一步重构。根据三条原则,可以总结出以下几条提高代码审美的Tips:
- 重新安排换行来保持一致和紧凑。
- 用方法来整理不规则的东西。如果多个代码块做相似的事情,尝试让他们有同样的剪影。
- 需要时使用列对齐。按列对齐可以让代码更加容易阅读。
- 选一个有意义的顺序,始终一直使用它。如:变量定义的顺序,如果一段代码中提到 A、B、C,那么另一段中使用同样的顺序。
- 把声明按块组织起来。不要把所有的方法都放到一个巨大的代码块中,应当用空行按逻辑把他们分组。
- 个人风格与一致性:一致的风格比“正确”的风格更重要。
注释的使用
什么地方需要注释?
注释的目的是尽量帮助读者了解的和作者一样多。当程序员写代码时,脑海中会有很多有价值的信息;当其他人阅读代码时,这些信息已经丢失,所见到的只是眼前的代码。
什么地方不需要注释?
(1)能从代码本身中迅速推断的事实。
(2)不要给不好的名字加注释-----应该把名字改好:一个好的名字比一个好的注释更重要,因为在任何用到这个函数的地方都能看到它。也就是好代码>坏代码+好注释
注释作用:记录你的思想
记录写代码时有过的重要想法。以下几种情况建议写注释:
- 对于为什么代码写成这样而不是那样的内在理由。
eg:防止做无谓的优化而浪费时间; 解释为什么代码不那么整洁; - 给常量加注释。是什么?为什么是这个值?通过阅读注释,有了调整这个值的指南。但有些常量不需要注释,名字本身已经很清楚了(eg: SECONDS_PER_DAY)
- 为代码中的瑕疵写注释。即代码需要改进时,或代码没有完成时。
标记 | 通常的作用 |
---|---|
TODO | 还没有处理的事情 |
MAYBE_LATER | 方法有次要的缺陷 |
FIXME | 已知的无法运行的代码 |
HACK | 对一个问题不得不采用的比较粗糙的解决方案 |
XXX | 危险!这里有重要的问题 |
加注释:站在读者的角度
想象代码对于外人来讲看起来是什么样子的,这个人并不熟悉项目,这对于发现什么地方需要注释尤其有用。
- 用注释总结代码块,不致迷失在细节中 ---> 在包含大块的长函数中使用。
- 在文件/类的级别上使用“全局观”注释 ---> 熟悉代码库。解释所有的部分是如何一起工作的,迅速了解代码,比自己读源代码快很多。
//这个文件包含一些辅助函数,为我们文件系统提供了更便利的接口
//它处理了文件权限及其他基本的细节。
写注释的学问
- 注释保持紧凑
- 避免使用不明确的代词,如 it,this
- 润色粗糙的句子
- 精确地描述函数的行为
eg:统计行数 ----> 统计换行符(/n) - 用输入/输出例子来说明特别的情况
- 声明代码的意图:表达写代码时的想法,不是描述字面上的意思
- 使用嵌入的注释:大多数函数不需要,这样可以方便紧凑地解释看上去难以理解的参数
eg:Connect(10, false); --->
Connect(/*timeout_ms = */ 10, /*use_encryption = */ false);
- 用含义丰富的词来使注释简洁:如果感觉一段注释太长了,可以使用一个典型的编程场景来描述。
eg1:
// This class contains a number of members that store the same information as in the
// database, but are stored here for speed. When this class is read from later, those
// members are checked first to see if they exist, and if so are returned; otherwise the
// database is read from and that data stored in these field for next time.
可以简单的说:
This class acts as a caching layer to the database.
eg2:
// Remove excess whitespace from the street address, and do lots of other cleanup
// like turn "Avenue" into "Ave." This way, if there are two different street addresses
// that are typed in slightly differently, they will have the same cleaned-up version and
// we can detect that these are equal.
可以简单的说:
Canonicalize the street address (remove extra spaces, "Avenue" -> "Ave.", etc.)