《编写可读代码的艺术》
标签: 代码能力 读书笔记
第零部分 基本原则
第1章 代码应当易于理解
可读性基本定理: 代码的写法应当使别人理解它所需的时间最小化
- 并不是代码行数越小越好
- 可以适当添加注释
第一部分 表面层次的改进
第2章 将信息装到名字里
- 选择专业的词,例如“get”可以换为“download”或者“fetch”,树中的高度使用“height”等避免使用“size”,选择词语更有表现力,清晰和精确
- 避免使用像“tmp”和“retval”这样泛泛的名字
- ”tmp”只应用于短期存在且临时性为其主要存在因素的变量。
- 要使用这些名字,必须有好的理由。
- 用具体的名字替代抽象的名字(例:DISALLOW_EVIL_CONSTRUCTORS)
- 为名字附带更多信息
- 带单位的值,start_ms/size_mb/max_kbps
- 常见其他附加属性,字符编码/是否加密/是否安全
- 决定名字的长度
- 小作用域中可以使用较短的名字,其他情况使用长名字,对于当代IDE,名字补全很智能
- 省略词和缩写词需要考虑对于新员工是否理解
- 去掉没有用的词语,如coverToString -> toString
- 利用名字的格式来表达含义,不同实体(类/函数/全局变量/局部变量/常量)可以使用不同的实体
第3章 不会误解的名字
这个名字会被别人解读为其他的含义么,filter是指的挑出还是去掉,length是指的字节数还是字符数
- 推荐使用min和max来表示包含极限
- 推荐使用first和last来表示包含范围
- 推荐使用begin和end来表示包含/排除范围
- 给布尔命名需要明确,如read_passwd->need_passwd/is_authenticated,disable_ssl->use_ssl
- 与使用者的期望相匹配,如get表示轻量级访问,size默认复杂度o(1)
第4章 审美
三条原则:
- 使用一致的布局,让读者很快就习惯这种风格
- 让相似的代码看上去相似
- 把相关的代码行分组,形成代码块
使用从审美角度讲让人愉悦的代码更容易
- 重新安排换行来保持一致和紧凑
- 将长代码行替换为方法调用来去掉不规则
- 在需要时使用列对齐,使相似的代码看上去相似
- 使用有意义的顺序,始终如一地使用它,如对象属性处理顺序/构造函数参数顺序等
- 把声明按块组织起来,按照分组和层次结构思考
- 把代码分成“段落”,相似想法物理上接近,提供了可见的“脚印”,利于段落间导航
一致的风格比“正确”的风格更重要
第5章 该写什么样的注释
关键思想: 注释的目的是尽量帮助读者了解得和作者一样多
- 什么不需要注释
- 不要为了那些可以从代码本身就能快速推断的事实写注释
- 不要为了注释而注释
- 不要给不好的名字加注释,应该改变名字
- 记录你的思想
- 加入导演评论
- 为代码中的暇庇写注释,TODO/FIXME/HACK(不得不采用的粗糙解决方案)/XXX(危险,这里有重要问题)
- 给常量加注释,解释为什么是这个值
- 站在读者的角度
- 想象你的代码在其他人看来是什么样子的
- 意料之中的提问
- 公布可能的陷阱
- 全局观注释,类之间的交互/数据的流动/入口点
- 总结性注释,段落和多层循环可以通过总结注释表明其做的事情
- 克服作者心理阻滞
第6章 写出言简意赅的注释
- 注释应该由很高的信息/空间率,让注释保持紧凑,采用信息含量高的词
- 避免使用不明确的代词,润色粗糙的句子
- 准确描述函数的行为,如行数->“\n”的个数?
- 用输入输出例子来说明特殊的情况
- 声明代码的高层次意图而非明显的细节
- “具名函数参数”的注释,如connect(/*timeout=*/5)
第二部分 简化循环和逻辑
第7章 把控制流变得易读
把条件/循环以及其他对控制流的改变做得越“自然”越好,运用一种方式使读者不用停下来重读你的代码
- 条件语句中参数的顺序
- 左侧:被问询的表达式,其值更倾向于不断变化
- 右侧:用来做比较的表达式,它的值更倾向于常量
- if/else语句块的顺序
- 首先处理正逻辑而不是负逻辑的情况
- 先处理:简单的情况/有趣或者可疑的情况/具体分析
- ?:条件表达式,最小化理解时间而不是最小化行数,只在简单情况下使用
- 避免do/while循环
- 从函数中提前返回时,注意清理代码结构化:析构函数/try…finally
- 臭名昭著的goto,在linux中大量使用,用于出口exit作为清理代码
- 最小化嵌套
- 嵌套太多,读者不容易搞清楚当前的分支情况
- 对代码进行修改时,从全新的角度审视它,把它作为一个整体来看待
- 通过提前返回来减少嵌套
- 减少循环内的嵌套 - continue
你能理解执行的流程么(复杂编程结果)
- 线程/信号量/中断处理程序
- 异常,多个函数调用中向上冒泡一样执行
- 函数指针/匿名函数,编译时无法知道到底会执行什么代码
- 虚方法,可能调用未知子类的代码
第8章 拆分超长的表达式
常用方法
- 用作中间解释的变量
- 总结变量
- 使用德摩根定理
- 滥用短路逻辑
- 必要时可以使用宏来简化长表达式
第9章 变量与可读性
- 变量问题
- 变量越多,读者越难全部跟踪它们的动向
- 变量的作用域越大,需要跟踪它的动向就越久
- 变量改动得越频繁,越难以跟踪它的当前值
- 减少变量
- 没有价值的临时变量
- 减少中间结果
- 减少控制流变量(flag)
- 缩小变量的作用域
- 让你的变量对尽量少的代码行可见
- C++中if语句的作用域
- js中创建私有变量(闭包)/全局变量
- Python/JS中没有嵌套的作用域,本地变量会溢出到整个函数
- 把定义移动到使用的时候
- 只写一次的变量更好,易于确定当前值,const/final修饰常量更容易理解
第三部分 重新组织代码
第10章 抽取不相干的子问题
工程学:把大问题拆分成小问题再把这些问题的解决方案放回一起
- 提取步骤
- 对于函数或者代码块,确认这段代码的高层次目标是什么?
- 对于每行代码,它是直接为目标而工作的么?这段代码的高层次目标是什么?
- 如果足够的行数在解决不相关的子问题,抽取代码到独立的函数中
- 常见的类型
- 纯工具代码
- 其他多用途代码,如调试输出美化代码
- 简化已有接口
- 按需重塑接口
- 过犹不及
第11章 一次只做一件事
步骤
- 列出代码所做的所有任务
- 尽量将这些任务拆分到不同的函数中,或者至少是代码中不同的段落中
第12章 把想法变成代码
How:
- 像对待一个同事一样使用自然语言描述代码要做什么
- 注意描述中所用的关键词和短语
- 写出与描述所匹配的代码
Example:
t = max(t1, t2, t3)
if t1 < t: t1.next()
if t2 < t: t2.next()
if t3 < t: t3.next()
第13章 少写代码
- 最好的代码就是没有代码,代码越多,越不易于维护和测试
- 别费神实现那个功能,你不会需要它
- 实践中,往往高估功能的重要性,低估功能的实现代价
- 质疑和拆分你的需求
- 保持小代码库
- 熟悉你周边的库
- 每隔一段时间,花15分钟来阅读标准库中的所有函数/模块/类型的名字
第四部分 精选话题
第14章 测试与可读性
测试应当具有可读性,以便其他程序员可以舒服地改变或者增加测试
- 使测试更好读
- 对使用者隐去不重要的细节,以便有更重要的细节会更突出
- 创建最小的测试声明
- 对于这样的输入情形,期望有这样的行为/输出
- 用自然语言描述,然后实现定制的“微语言”
- 例如:CheckScoreSortFilterBeforeAfter(“-5,1,4,3,-99”,”4,3,1”)
- 让错误消息具有可读性,更好版本的assert/手工打造错误消息
- 选择好的测试输入
- 应当选择一组最简单的输入,它能完整地使用被测代码
- 简化输入值,又简单又能完成工作的测试值更好
- 一个功能的多个测试,多种场景,各种临界情况
- 为测试函数命名
- 考虑被测试的类/函数/场景/Bug
- 测试函数并不会被工作代码调用,长度无关紧要
- 对测试较好的开发方式
- 利于测试的理想代码
- 明确定义的接口
- 没有过多状态和其他的设置
- 没有很多需要审查的隐藏数据
- 测试驱动开发
- 优秀代码
- 类中只有很少或者没有内部状态
- 类/函数只做一件事
- 每个类对别的类依赖较少,低耦合
- 函数的接口简单,定义明确
- 走得太远
- 牺牲真实代码的可读性
- 着迷于100%的代码覆盖率
- 让测试成为产品开发的阻碍
- 利于测试的理想代码
第15章 设计并改进“分钟/小时计数器”
- 方案1 传送带方法
- 每次添加事件时移动hour/minute事件组合且计数
- bug:事件精度为秒,实际记录的事件段是少于1分钟/1秒时,平均少0.5秒
- 方案2 时间桶设计
- 使用时间桶来统计相应时刻内的事件,如1分钟用60个桶表示,可通过桶数目来扩大精度
- 具体实现(略)