读《代码整洁之道》

date
Aug 21, 2022
slug
read-clean-code
status
Published
tags
阅读
type
Post
lang
summary
工作后就很久没有完整的阅读过技术类型的书,最近抽时间读完了早该阅读的《代码整洁之道》,本文简单总结。
工作后就很久没有完整的阅读过技术类型的书,最近抽时间读完了早该阅读的《代码整洁之道》,本文简单总结。

简评

本书讲解整洁代码的意义,如何辨别整洁的代码,如何写好的代码等,对编码中的命名、函数、注释、类、测试等都做了总结,非常贴合日常编程中的发生的种种问题。是一本适合所有程序员认真读一读的好书。

整洁代码的重要性

作为专业的程序员自然要需要追求好代码,也需要知道糟糕代码带来的灾难,同时知道如何将糟糕代码改成好代码。

糟糕的代码毁了公司

业务迅速迭代,所有程序员疯狂糊屎。短期看起来可能没什么问题,但是总有一天会发现无法继续维护。

混乱的代价

随看混乱的增加,团队生产力也持续下降,趋向于零。当生产力下降时,管理层就只有体事可做了:增加更多人手到项目中,期望提升生产力。可是新人并不熟悉系统的设计(不想维护屎山)。
最终导致重构成新的系统,新的系统又需要实现所有老系统的所有功能。而在新系统能够完全和老系统媲美之前,业务老板压根不想让新系统代替老系统(鬼知道会有多少 Bug)。
同时重构之后的新系统一定会更好吗?如果他们还是没有遵守整洁代码的规范,那么将又会是一坐新的屎山,新的轮回。

读与写花费时间的比例超过 10 : 1

当在写新代码时,我们一直在读旧代码!
读代码是 Coding 时,最常发生的事情,尤其是维护他人的模块,或者自己长期没有维护的模块。
相信你一定有看了 1 个小时代码,最终只改了 1 行的经历(不管你有没有,反正我肯定有,而且不只 1 次,是很多次)。
例如我之前维护的一个比较复杂的的模块,因为初期设计的不合理,导致自己过了一段时间没有维护之后,再上手维护的阅读成本就非常高(也就是常说的自己看不懂自己写的什么东西)。

稍后等于永不

重构应该在日常开发的时候持续的去做,而不是 “等有时间再说”
这句话说的真的很好,且不光是写代码,生活着很多事情都是这样的。例如准备和谁吃个饭,如果不立即开约,那么过一段时间可能就忘了。“下次一定“ 这个梗就是这么来的吧~
这点真的应该牢记。

必须时时保持代码整洁

让营地比你来时更干净。
就像每次都做一点小的改造,解决所有小问题,自然整体就会变好。
如果每次签入时,代码都比签出时干净,那么代码就不会腐坏。清理并不一定要花多少功夫,也许只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复代码,清理一个嵌套 if 语句。
 
随时随地重构垃圾代码是我立即学会的事情并进行实践的事情,推荐大家也立即行动起来。 稍后等于永不,其实和《搞定Ⅰ 无压工作的艺术》中讲的一样,一件小事如果能立即完成,就立刻完成它,而不是稍后在做。喝完牛奶就洗杯子,吃完饭就洗碗,别让这些小事拖垮自己。

有意义的命名

在代码中取名真的是一件很困难,但同时又很重要的一件事,书中讲了几点需要重点注意的事。
也可以看看这个:忍者代码

名副其实

变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在, 它做什么事,应该怎么用。
如果名称需要注释来补充,那就不算是名副其实。比如这样:
notion image
如果团队有 Lint 规则的话,就不要限制命名的长度了。相信我,太短的命名没有办法在很大的作用范围内描述清楚自身的意义。

做有意义的区分

  1. 别用什么 a1, a2 作为变量命名(当心被砍)。
  1. 不要用什么 ArrayList 作为命名,既然有 List 了,为什么还要用 Array 呢?
// 他们有什么区别吗? getActiveAccount(); getActiveAccounts(); getActiveAccountInfo(); // 直接用这个就可以 getActiveAccount();

使用读得出来的名称

例如 getAccount 肯定好过 getAct ,还有人喜欢这么写 listlst ???
也就是说别用缩写,或者说别用别人看不懂的缩写,没必要,别坑了以后的自己。况且有 IDE 的补全,所以不要怕名称太长。

方法名用动词

方法名应当是动词或动词短语,如 postPayment、deletePage 或 save。

添加有意义的语境

通过添加语境的方式让命名更易读懂。
当描述地址相关的变量时,可以添加前缀 addrFirstNameaddrLastNameaddrState 等,以此提供语境。至少,读者会明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为 Address 的类。

不要添加没用的语境

例如要写一个商品发布(GoodsPublish)的页面,这时候给所有的名称前面都加上 GoodsPublish 就是一件很蠢的事情,例如 GoodsPublishApi, GoodsPublishStore。因为编码时 IDE 提示的备选项会多出一堆选项。
 
命名是一件很重要的事情,好的命名能让代码可读性提升 100%! 能用命名解决的事情,就不要再加注释了。

函数

系统由函数组成,我们应该学习如何写好每一个函数。

短小

短小的函数更利于阅读和理解。
书中指出不成超过 20 行,但我感觉太难了。只能尽量让函数尽可能的短吧。总之编码的时候要牢记函数要短小。

只做一件事

一个函数做很多事情的时候,可读性就大大降低,测试难度也很高。
只做一件事的函数更便于理解和测试。

单一权责原则

例如在调用服务端接口的时候,以前可能会说,你给这个接口加个 type 然后根据 type 不同,做不同的事情。其实这个时候应该是让服务端再开一个接口,做单独的事情。

使用描述性的名称

简单的说就是,函数的名称要能看懂,能讲清楚这个函数是做什么的,同时不要太概括,例如渲染 HTML,renderDOMToPage 就比 render 强。
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。

函数参数

  1. 最理想的参数数量是零:过多的参数测试用例就很难写,因为把所有参数的组合搞出来就是一件痛苦的事情。一般来说参数数量越少的代码越容易理解;
  1. 标识参数:标识参数指的就是 Bool 类型的参数,使用 Bool 参数就明显代表了这个 fun 在做两种事情,所以这个时候我们应该把他们拆开!
  1. 参数对象:如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了;
    1. 看起来像作弊,但其实并不是;
    2. 因为参数对象有着自己的定义,例如我传的是 goods 商品信息,那么它的定义就是商品信息,就是一个整体,我在使用的时候也知道 goods 是个什么东西;

无副作用

也就是我们常说的纯函数,这种函数通常来说更容易理解 & 不容易坑人!
谁也不想某个函数偷偷的就把我的变量给改了吧?

使用异常替代返回错误码

这个说的就是多用 try catch,而不是抛出看不懂的状态码。

如何写出这样的函数

先按照感觉写,然后不停的优化你的函数,打磨这些代码,分解函数、修改名称、消除重复。
我(作者)并不从一开始就按照规则写函数。我想没人做得到。大师级程序员把系统当作故事来讲,而不是当作程序来写。
这一点很重要,先把要实现的功能写出来,再去思考如何优化函数。使用 IDE 的重构功能可以很方便的完成这件事。

注释

注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败
当我们无法用程序语言去描述代码意图时,我们才会使用注释进行弥补。

注释不能美化糟糕的代码

带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多。与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。

用代码来阐述

能用代码结束清楚的东西,就不要用注释了。

好注释

  1. 提供信息的注释:例如:这个函数被用在什么地方等等
  1. 对意图的解释:例如:为什么这部分代码一定要放在这个位置
  1. 警示:这串代码别乱动,因为 xxx 原因,这个在代码库里挺常见的,哈哈~
  1. 其他的还有:法律信息、TODO、API 注释

坏注释

  1. 喃喃自语的废话、多余的注释(还不如看代码,扰乱读者)
  1. 误导性的注释:通常是代码逻辑发生变更,而注释并没有一起变动
    1. 这就是为什么能不用注释尽量不要用注释,代码永远是有效的,而注释不是
  1. 日志式注释、归属与署名:
    1. 很久以前,在模块开始处创建并维护这些记录还算有道理。那时,我们还没有源代码控制系统可用。如今,这种冗长的记录只会让模块变得凌乱不堪,应当全部删除。
  1. 可怕的废话:
    1. interface Person { // 名称 name: string // 年龄 age: number // 性别 male: boolean }
  1. 能用函数或变量名称描述时时就别用注释
  1. 注释掉的代码:没用的代码就删了吧,有 Git 帮你存呢,再说留着真的有参考意义吗?
  1. 信息过多:注释里面加了太多的废话或者无关的细节描述
 
如果你发现自己需要写注释,再想想看是否有办法翻盘,用代码来表达。每次用代码表达,你都该夸奖一下自己。每次写注释,你都该做个鬼脸,感受自己在表达能力上的失败。
 

代码格式

格式关乎沟通,而沟通是专业开发者的头等大事。

垂直格式

短文件通常比长文件易于理解
向报纸学习,报纸由许多篇文章组成;多数短小精悍。有些稍微长点儿。很少有占满一整页的。这样做,报纸才可用。假若一份报纸只登载一篇长故事,其中充斥毫无组织的事实、日期、名字等,没人会去读它。
概念间垂直方向上的区隔和靠近
用空行来分隔不同的代码思路;紧密相关的代码需要互相靠近,不要乱放,中间不要插奇奇怪怪的东西。
垂直顺序
被调用的函数应该放在执行调用的函数下面。这样就建立了一种自顶向下贯穿源代码模块的良好信息流。

横向格式

每行代码的字符过多时实在不便于阅读,所以尽量小一点,不超过 120 字符;字符少一点也便于我们分屏查看代码

团队规则

每个程序员都有自己喜欢的格式规则,但如果在一个团队中工作,就是团队说了算。
一定要用格式化工具 & 校验工具来保证代码格式,不会有人真的想让团队靠自觉性统一代码格式吧?给 CI 加上格式校验吧,务必让所有团队开发者遵守团队规则。

错误处理

使用异常而非返回码

使用 try catch finally 的方式来编写你的代码,而不是抛出难以看懂的错误码。
finally 保证了在发错发生后代码能够正常运行。

给出异常发生的环境说明

简单来说就是 catch 的错误需要上报上来,事实也是如此,业务中的 catch 上报非常重要。
代码中 catch 之后不做任何事的代码就是毒瘤中的毒瘤!请务必干掉这些代码。我被这种代码肯了不知道多少次。
场景复现:程序表现不符合预期 → 完全没有错误日志 → debug 半天发现被 .catch 住了,但是 catch 里面并没有任何处理
 
整洁代码是可读的,但也要强固。可读与强固并不冲突。如果将错误处理隔离看待,独立于主要逻辑修就能得出强国面整洁代码。

单元测试

测试很重要,保障代码可扩展、可维护、可复用。国内的现状是很难在业务代码中增加测试用例,原因我之前也分析过,如果实在没法给业务代码加上测试,请工具库请务必加上测试,为了自己!

TDD 三定律

  1. 在编写不能通过的单元测试前,不可编写生产代码
  1. 只可编写刚好无法通过的单元测试,不能编译也算不过
  1. 只可编写刚好足以通过当前失败测试的生产代码

保持测试整洁

测试代码和生产代码一样重要
测试代码质量低会造成难以修改、阻碍开发的问题。
测试带来一切好处
代码可扩展、可维护、可复用。
没有测试个工具库代码,真的改不动(不敢改),因为你担忧改动会引入不可预知的缺陷。
整洁的测试
  1. 可读性:看不懂的测试用例和看不懂的代码一样恐怖;
  1. 构造-操作-检验(BUILD-OPERATE-CHECK)模式:就按照这个流程写测试用例,让读测试的人可以快速明白测试 case 的做什么;
  1. 打造了一套包装这些API的函数和工具代码:通过封装工具代码,让编写测试变得简单;

每个测试一个断言

目的是让每个 case 都便于理解(有点难…)。
单个测试中的断言数量应该最小化
这个感觉现实一点,也好实现一点,断言尽量的少。
每个测试一个概念
测试多个概念会导致理解成本陡增,一个测试用例不要做多个事情,这个也是便于理解 case。

F.I.R.S.T 原则

  1. 快速(Fast)
    1. 测试应该够快。测试应该能快速运行。测试运行缓慢,你就不会想要频繁地运行它。如果你不频繁运行测试,就不能尽早发现问题,也无法轻易修正,从而也不能轻而易举地清理代码。最终,代码就会腐坏。
  1. 独立(Independent)
    1. 测试应该相互独立。某个测试不应为下一个测试设定条件。你应该可以单独运行每个测试,及以任何顺序运行测试。当测试互相依赖时,头一个没通过就会导致一连串的测试失败,使问题诊断变得困难,隐藏了下级错误。
  1. 可重复(Repeatable)
    1. 测试应当可在任何环境中重复通过。你应该能够在生产环境、质检环境中运行测试,也能够在无网络的列车上用笔记本电脑运行测试。如果测试不能在任意环境中重复,你就总会有个解释其失败的接口。当环境条件不具备时,你也会无法运行测试。
  1. 自足验证(Self-Validating)
    1. 测试应该有布尔值输出。无论是通过或失败,你不应该查看日志文件来确认测试是否通过。你不应该手工对比两个不同文本文件来确认测试是否通过。如果测试不能自足验证,对失败的判断就会变得依赖主观,而运行测试也需要更长的手工操作时间。
  1. 及时(Timely)
    1. 测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。如果在编写生产代码之后编写测试,你会发现生产代码难以测试。你可能会认为某些生产代码本身难以测试。你可能不会去设计可测试的代码。

味道与启发

代码什么时候让人感觉不舒服?书中列举了一些例子。

注释

  1. 不恰当的信息:别传达和代码无关的多余信息,例如修改历史记录(Git 会帮你记录)
  1. 废弃的注释:尽快删除
  1. 冗余注释:看注释不如看代码
  1. 糟糕的注释:要写注释就好好写,别写看不懂、难读的注释
  1. 注释掉的代码
    1. 污染模块、分散读者注意力
    2. 注释掉基本上就是废弃代码了,直接删除就可以,如果真的需要,看之前版本就可以

环境

  1. 需要多步才能实现的构建
    1. 系统应该能够使用一个命令进行构建
  1. 需要多步才能做到的测试
    1. 一行命令运行所有测试

一般性问题

  1. 明显的行为未被实现:指的是函数名称所描述的行为,函数并没有实现,这就导致其他程序员不再信任函数名称
  1. 不正确的边界行为:每种边界条件在我们编码的时候都需要考虑到,而不是依赖直觉,“感觉没问题” 不代表没问题
    1. // ❌错误 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 } }
  1. 忽视安全:请务必遵守各种 SOP
  1. 重复:
    1. 每次看到重复代码,都代表遗漏了抽象
    2. 尽量不要复制粘贴代码,能做好抽象就做好抽象
  1. 信息过多:设计良好的模块有着非常小的接口,让你能事半功倍
  1. 死代码:请务必删除废弃代码
  1. 人为耦合:不互相依赖的东西不该耦合
  1. 晦涩的意图:这点我认为很重要,代码如果为了追求简短写的晦涩难懂,不如写长一些,可读性高一些
  1. 使用解释性变量:良好的命名很重要
  1. 函数名称应该表达其行为:
    1. getUserList 这个命名代表了获取用户列表,那么除了获取用户列表不要做别的任何事情!
  1. 理解算法:算法 > 无数 if
  1. 遵循标准约定:遵守团队定义的代码规则
  1. 用命名常量替代魔术数
    1. 不是所有情况都需要使用常量代替
    2. 如果魔术字很有强的自我解释能力,例如 2 圈,同时没有在多个地方进行使用,那么就可以直接使用魔术字
  1. 准确:在代码中做决定时,确认自己足够准确
  1. 避免否定性条件
    1. if (buffer.shouldCompact()) 要好于 if (!buffer.shouldNotCompact())
  1. 函数只该做一件事
  1. 掩蔽时序耦合
    1. 当有时许耦合时,如何正确处理,请让后面的函数接收前面的参数

名称

  1. 采用描述性名称
    1. 不要太快(太随意)取名,认真思考应该取什么名称,确认名称具有描述性
  1. 无歧义的名称
    1. 名称很像的时候就会发生混淆,所以我们取名的时候需要避免这种情况
  1. 为较大作用范围选用较长名称
    1. 可以理解为,我们在简单的 for 循环里面使用 key, value 这种命名,但是作用范围变大后,这种命名就不太合适了
  1. 名称应该说明副作用,而不是悄默默的添加副作用

© CC BY-SAJin 2019 - 2023

Powered by Vercel & Notion