读《代码整洁之道》
date
Aug 21, 2022
slug
read-clean-code
status
Published
tags
阅读
type
Post
lang
summary
工作后就很久没有完整的阅读过技术类型的书,最近抽时间读完了早该阅读的《代码整洁之道》,本文简单总结。
工作后就很久没有完整的阅读过技术类型的书,最近抽时间读完了早该阅读的《代码整洁之道》,本文简单总结。
简评
本书讲解整洁代码的意义,如何辨别整洁的代码,如何写好的代码等,对编码中的命名、函数、注释、类、测试等都做了总结,非常贴合日常编程中的发生的种种问题。是一本适合所有程序员认真读一读的好书。
整洁代码的重要性
作为专业的程序员自然要需要追求好代码,也需要知道糟糕代码带来的灾难,同时知道如何将糟糕代码改成好代码。
糟糕的代码毁了公司
业务迅速迭代,所有程序员疯狂糊屎。短期看起来可能没什么问题,但是总有一天会发现无法继续维护。
混乱的代价
随看混乱的增加,团队生产力也持续下降,趋向于零。当生产力下降时,管理层就只有体事可做了:增加更多人手到项目中,期望提升生产力。可是新人并不熟悉系统的设计(不想维护屎山)。
最终导致重构成新的系统,新的系统又需要实现所有老系统的所有功能。而在新系统能够完全和老系统媲美之前,业务老板压根不想让新系统代替老系统(鬼知道会有多少 Bug)。
同时重构之后的新系统一定会更好吗?如果他们还是没有遵守整洁代码的规范,那么将又会是一坐新的屎山,新的轮回。
读与写花费时间的比例超过 10 : 1
当在写新代码时,我们一直在读旧代码!
读代码是 Coding 时,最常发生的事情,尤其是维护他人的模块,或者自己长期没有维护的模块。
相信你一定有看了 1 个小时代码,最终只改了 1 行的经历(不管你有没有,反正我肯定有,而且不只 1 次,是很多次)。
例如我之前维护的一个比较复杂的的模块,因为初期设计的不合理,导致自己过了一段时间没有维护之后,再上手维护的阅读成本就非常高(也就是常说的自己看不懂自己写的什么东西)。
稍后等于永不
重构应该在日常开发的时候持续的去做,而不是 “等有时间再说”
这句话说的真的很好,且不光是写代码,生活着很多事情都是这样的。例如准备和谁吃个饭,如果不立即开约,那么过一段时间可能就忘了。“下次一定“ 这个梗就是这么来的吧~
这点真的应该牢记。
必须时时保持代码整洁
让营地比你来时更干净。
就像每次都做一点小的改造,解决所有小问题,自然整体就会变好。
如果每次签入时,代码都比签出时干净,那么代码就不会腐坏。清理并不一定要花多少功夫,也许只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复代码,清理一个嵌套 if 语句。
随时随地重构垃圾代码是我立即学会的事情并进行实践的事情,推荐大家也立即行动起来。 稍后等于永不,其实和《搞定Ⅰ 无压工作的艺术》中讲的一样,一件小事如果能立即完成,就立刻完成它,而不是稍后在做。喝完牛奶就洗杯子,吃完饭就洗碗,别让这些小事拖垮自己。
有意义的命名
在代码中取名真的是一件很困难,但同时又很重要的一件事,书中讲了几点需要重点注意的事。
也可以看看这个:忍者代码。
名副其实
变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在, 它做什么事,应该怎么用。
如果名称需要注释来补充,那就不算是名副其实。比如这样:

如果团队有 Lint 规则的话,就不要限制命名的长度了。相信我,太短的命名没有办法在很大的作用范围内描述清楚自身的意义。
做有意义的区分
- 别用什么 a1, a2 作为变量命名(当心被砍)。
- 不要用什么 ArrayList 作为命名,既然有 List 了,为什么还要用 Array 呢?
// 他们有什么区别吗? getActiveAccount(); getActiveAccounts(); getActiveAccountInfo(); // 直接用这个就可以 getActiveAccount();
使用读得出来的名称
例如
getAccount
肯定好过 getAct
,还有人喜欢这么写 list
→ lst
???也就是说别用缩写,或者说别用别人看不懂的缩写,没必要,别坑了以后的自己。况且有 IDE 的补全,所以不要怕名称太长。
方法名用动词
方法名应当是动词或动词短语,如 postPayment、deletePage 或 save。
添加有意义的语境
通过添加语境的方式让命名更易读懂。
当描述地址相关的变量时,可以添加前缀
addrFirstName
、addrLastName
、addrState
等,以此提供语境。至少,读者会明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为 Address
的类。不要添加没用的语境
例如要写一个商品发布(GoodsPublish)的页面,这时候给所有的名称前面都加上 GoodsPublish 就是一件很蠢的事情,例如
GoodsPublishApi
, GoodsPublishStore
。因为编码时 IDE 提示的备选项会多出一堆选项。命名是一件很重要的事情,好的命名能让代码可读性提升 100%! 能用命名解决的事情,就不要再加注释了。
函数
系统由函数组成,我们应该学习如何写好每一个函数。
短小
短小的函数更利于阅读和理解。
书中指出不成超过 20 行,但我感觉太难了。只能尽量让函数尽可能的短吧。总之编码的时候要牢记函数要短小。
只做一件事
一个函数做很多事情的时候,可读性就大大降低,测试难度也很高。
只做一件事的函数更便于理解和测试。
单一权责原则
例如在调用服务端接口的时候,以前可能会说,你给这个接口加个 type 然后根据 type 不同,做不同的事情。其实这个时候应该是让服务端再开一个接口,做单独的事情。
使用描述性的名称
简单的说就是,函数的名称要能看懂,能讲清楚这个函数是做什么的,同时不要太概括,例如渲染 HTML,renderDOMToPage 就比 render 强。
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。
函数参数
- 最理想的参数数量是零:过多的参数测试用例就很难写,因为把所有参数的组合搞出来就是一件痛苦的事情。一般来说参数数量越少的代码越容易理解;
- 标识参数:标识参数指的就是 Bool 类型的参数,使用 Bool 参数就明显代表了这个 fun 在做两种事情,所以这个时候我们应该把他们拆开!
- 参数对象:如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了;
- 看起来像作弊,但其实并不是;
- 因为参数对象有着自己的定义,例如我传的是 goods 商品信息,那么它的定义就是商品信息,就是一个整体,我在使用的时候也知道 goods 是个什么东西;
无副作用
也就是我们常说的纯函数,这种函数通常来说更容易理解 & 不容易坑人!
谁也不想某个函数偷偷的就把我的变量给改了吧?
使用异常替代返回错误码
这个说的就是多用 try catch,而不是抛出看不懂的状态码。
如何写出这样的函数
先按照感觉写,然后不停的优化你的函数,打磨这些代码,分解函数、修改名称、消除重复。
我(作者)并不从一开始就按照规则写函数。我想没人做得到。大师级程序员把系统当作故事来讲,而不是当作程序来写。
这一点很重要,先把要实现的功能写出来,再去思考如何优化函数。使用 IDE 的重构功能可以很方便的完成这件事。
注释
注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。
当我们无法用程序语言去描述代码意图时,我们才会使用注释进行弥补。
注释不能美化糟糕的代码
带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多。与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。
用代码来阐述
能用代码结束清楚的东西,就不要用注释了。
好注释
- 提供信息的注释:例如:这个函数被用在什么地方等等
- 对意图的解释:例如:为什么这部分代码一定要放在这个位置
- 警示:这串代码别乱动,因为 xxx 原因,这个在代码库里挺常见的,哈哈~
- 其他的还有:法律信息、TODO、API 注释
坏注释
- 喃喃自语的废话、多余的注释(还不如看代码,扰乱读者)
- 误导性的注释:通常是代码逻辑发生变更,而注释并没有一起变动
- 这就是为什么能不用注释尽量不要用注释,代码永远是有效的,而注释不是
- 日志式注释、归属与署名:
- 很久以前,在模块开始处创建并维护这些记录还算有道理。那时,我们还没有源代码控制系统可用。如今,这种冗长的记录只会让模块变得凌乱不堪,应当全部删除。
- 可怕的废话:
interface Person { // 名称 name: string // 年龄 age: number // 性别 male: boolean }
- 能用函数或变量名称描述时时就别用注释
- 注释掉的代码:没用的代码就删了吧,有 Git 帮你存呢,再说留着真的有参考意义吗?
- 信息过多:注释里面加了太多的废话或者无关的细节描述
如果你发现自己需要写注释,再想想看是否有办法翻盘,用代码来表达。每次用代码表达,你都该夸奖一下自己。每次写注释,你都该做个鬼脸,感受自己在表达能力上的失败。
代码格式
格式关乎沟通,而沟通是专业开发者的头等大事。
垂直格式
短文件通常比长文件易于理解
向报纸学习,报纸由许多篇文章组成;多数短小精悍。有些稍微长点儿。很少有占满一整页的。这样做,报纸才可用。假若一份报纸只登载一篇长故事,其中充斥毫无组织的事实、日期、名字等,没人会去读它。
概念间垂直方向上的区隔和靠近
用空行来分隔不同的代码思路;紧密相关的代码需要互相靠近,不要乱放,中间不要插奇奇怪怪的东西。
垂直顺序
被调用的函数应该放在执行调用的函数下面。这样就建立了一种自顶向下贯穿源代码模块的良好信息流。
横向格式
每行代码的字符过多时实在不便于阅读,所以尽量小一点,不超过 120 字符;字符少一点也便于我们分屏查看代码
团队规则
每个程序员都有自己喜欢的格式规则,但如果在一个团队中工作,就是团队说了算。
一定要用格式化工具 & 校验工具来保证代码格式,不会有人真的想让团队靠自觉性统一代码格式吧?给 CI 加上格式校验吧,务必让所有团队开发者遵守团队规则。
错误处理
使用异常而非返回码
使用 try catch finally 的方式来编写你的代码,而不是抛出难以看懂的错误码。
finally 保证了在发错发生后代码能够正常运行。
给出异常发生的环境说明
简单来说就是 catch 的错误需要上报上来,事实也是如此,业务中的 catch 上报非常重要。
代码中 catch 之后不做任何事的代码就是毒瘤中的毒瘤!请务必干掉这些代码。我被这种代码肯了不知道多少次。
场景复现:程序表现不符合预期 → 完全没有错误日志 → debug 半天发现被 .catch 住了,但是 catch 里面并没有任何处理
整洁代码是可读的,但也要强固。可读与强固并不冲突。如果将错误处理隔离看待,独立于主要逻辑修就能得出强国面整洁代码。
单元测试
测试很重要,保障代码可扩展、可维护、可复用。国内的现状是很难在业务代码中增加测试用例,原因我之前也分析过,如果实在没法给业务代码加上测试,请工具库请务必加上测试,为了自己!
TDD 三定律
- 在编写不能通过的单元测试前,不可编写生产代码
- 只可编写刚好无法通过的单元测试,不能编译也算不过
- 只可编写刚好足以通过当前失败测试的生产代码
保持测试整洁
测试代码和生产代码一样重要
测试代码质量低会造成难以修改、阻碍开发的问题。
测试带来一切好处
代码可扩展、可维护、可复用。
没有测试个工具库代码,真的改不动(不敢改),因为你担忧改动会引入不可预知的缺陷。
整洁的测试
- 可读性:看不懂的测试用例和看不懂的代码一样恐怖;
- 构造-操作-检验(BUILD-OPERATE-CHECK)模式:就按照这个流程写测试用例,让读测试的人可以快速明白测试 case 的做什么;
- 打造了一套包装这些API的函数和工具代码:通过封装工具代码,让编写测试变得简单;
每个测试一个断言
目的是让每个 case 都便于理解(有点难…)。
单个测试中的断言数量应该最小化
这个感觉现实一点,也好实现一点,断言尽量的少。
每个测试一个概念
测试多个概念会导致理解成本陡增,一个测试用例不要做多个事情,这个也是便于理解 case。
F.I.R.S.T 原则
- 快速(Fast)
- 测试应该够快。测试应该能快速运行。测试运行缓慢,你就不会想要频繁地运行它。如果你不频繁运行测试,就不能尽早发现问题,也无法轻易修正,从而也不能轻而易举地清理代码。最终,代码就会腐坏。
- 独立(Independent)
- 测试应该相互独立。某个测试不应为下一个测试设定条件。你应该可以单独运行每个测试,及以任何顺序运行测试。当测试互相依赖时,头一个没通过就会导致一连串的测试失败,使问题诊断变得困难,隐藏了下级错误。
- 可重复(Repeatable)
- 测试应当可在任何环境中重复通过。你应该能够在生产环境、质检环境中运行测试,也能够在无网络的列车上用笔记本电脑运行测试。如果测试不能在任意环境中重复,你就总会有个解释其失败的接口。当环境条件不具备时,你也会无法运行测试。
- 自足验证(Self-Validating)
- 测试应该有布尔值输出。无论是通过或失败,你不应该查看日志文件来确认测试是否通过。你不应该手工对比两个不同文本文件来确认测试是否通过。如果测试不能自足验证,对失败的判断就会变得依赖主观,而运行测试也需要更长的手工操作时间。
- 及时(Timely)
- 测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。如果在编写生产代码之后编写测试,你会发现生产代码难以测试。你可能会认为某些生产代码本身难以测试。你可能不会去设计可测试的代码。
味道与启发
代码什么时候让人感觉不舒服?书中列举了一些例子。
注释
- 不恰当的信息:别传达和代码无关的多余信息,例如修改历史记录(Git 会帮你记录)
- 废弃的注释:尽快删除
- 冗余注释:看注释不如看代码
- 糟糕的注释:要写注释就好好写,别写看不懂、难读的注释
- 注释掉的代码
- 污染模块、分散读者注意力
- 注释掉基本上就是废弃代码了,直接删除就可以,如果真的需要,看之前版本就可以
环境
- 需要多步才能实现的构建
- 系统应该能够使用一个命令进行构建
- 需要多步才能做到的测试
- 一行命令运行所有测试
一般性问题
- 明显的行为未被实现:指的是函数名称所描述的行为,函数并没有实现,这就导致其他程序员不再信任函数名称
- 不正确的边界行为:每种边界条件在我们编码的时候都需要考虑到,而不是依赖直觉,“感觉没问题” 不代表没问题
// ❌错误 async function f() { const a = await getA(); const b = await getB(); return c; } // ✅正确 async function f() { try { const a = await getA(); const b = await getB(); return a + b; } catch (e) { // do something } }
- 忽视安全:请务必遵守各种 SOP
- 重复:
- 每次看到重复代码,都代表遗漏了抽象
- 尽量不要复制粘贴代码,能做好抽象就做好抽象
- 信息过多:设计良好的模块有着非常小的接口,让你能事半功倍
- 死代码:请务必删除废弃代码
- 人为耦合:不互相依赖的东西不该耦合
- 晦涩的意图:这点我认为很重要,代码如果为了追求简短写的晦涩难懂,不如写长一些,可读性高一些
- 使用解释性变量:良好的命名很重要
- 函数名称应该表达其行为:
getUserList
这个命名代表了获取用户列表,那么除了获取用户列表不要做别的任何事情!
- 理解算法:算法 > 无数 if
- 遵循标准约定:遵守团队定义的代码规则
- 用命名常量替代魔术数
- 不是所有情况都需要使用常量代替
- 如果魔术字很有强的自我解释能力,例如 2 圈,同时没有在多个地方进行使用,那么就可以直接使用魔术字
- 准确:在代码中做决定时,确认自己足够准确
- 避免否定性条件
if (buffer.shouldCompact())
要好于if (!buffer.shouldNotCompact())
- 函数只该做一件事
- 掩蔽时序耦合
- 当有时许耦合时,如何正确处理,请让后面的函数接收前面的参数
名称
- 采用描述性名称
- 不要太快(太随意)取名,认真思考应该取什么名称,确认名称具有描述性
- 无歧义的名称
- 名称很像的时候就会发生混淆,所以我们取名的时候需要避免这种情况
- 为较大作用范围选用较长名称
- 可以理解为,我们在简单的 for 循环里面使用 key, value 这种命名,但是作用范围变大后,这种命名就不太合适了
- 名称应该说明副作用,而不是悄默默的添加副作用