《代码整洁之道 Clean Code》学习笔记 Part 1
阅读原文时间:2023年08月21日阅读:3

前段时间在看《架构整洁之道》,里面提到了:构建一个好的软件系统,应该从写整洁代码做起。毕竟,如果建筑使用的砖头质量不佳,再好的架构也无法造就高质量的建筑。趁热打铁,翻出《代码整洁之道》再刷一遍。

《代码整洁之道 Clean Code》学习笔记 Part 1

衡量代码质量的唯一标准:WTF/min

糟糕代码的代价

  • 理解、维护成本高。每次修改影响到好几处代码

  • 每次修改都会产生意想不到的问题,导致惧怕、抵触改动

  • 初期进展“迅速”,后续生产力持续下降,趋向于零

  • 破窗理论:如果对糟糕的代码放任不管,就会使人们争相效仿,甚至变本加厉

  • 不要想着先写出糟糕的代码,然后改进。勒布朗法则:Later equals never

  • Dealine 不是写出糟糕代码的借口,赶上 dealine 的唯一方法是写出 Clean Code

什么是 Clean Code

  • 消除重复
  • 只做一件事
  • 提高表达力:代码本身能够清晰地表达意图,不需要注释。良好的命名是提高表达力的重要手段
  • 提早构建简单抽象(小规模抽象)
  • 代码意图直截了当,不隐藏设计意图,叫 bug 难以隐藏
  • 减少依赖关系,使之便于维护
  • 性能调至最优,避免诱导他人来乱优化
  • 几乎没有改进的余地。如果你企图改进他,总会回到原点

童子军军规

Make the camp cleaner than when you arrived.

每次提交代码时,代码都比 checkout 时更干净,那么代码就不会变坏:

  • 改好一个变量名
  • 拆分一个过长的函数
  • 消除一点点重复代码
  • 清理一个嵌套 if

总结

“消除重复,只做一件事,提高表达力,小规模抽象”概括了本书的全部内容。

为什么需要好的命名

读代码和写代码的时间大于 10:1,好的名字让人更容易理解和修改代码,节省下来的时间远远大于起名字所花的时间。

  • 命名是一件很严肃的事情,是一个程序员的基本功
  • 如果发现给函数或者类起一个合适的名字很困难,要反思函数和类的设计是否合理、是否过于复杂,违反了单一职责原则

名副其实

  • 变量、函数或者类的名字应该可以回答:它为什么存在,做什么事,该怎么用。如果还需要注释来补充,就不算名副其实
  • 一个好的函数名只要看名字就知道干了什么事,而不需要跳转进去看实现,有助于保持在一个较高的抽象层级上快速理解代码

一些命名建议

  • 类名和对象名应该是名词或名词短语
  • 函数或方法名应该是动词或动词短语
  • 不要自己发明一些奇奇怪怪的缩写
  • 使用可搜索名称,变量名的长短应和作用域大小相对应
  • 通常单字母变量名不是好的选择,除非用作循环控制变量的 i、j、k(较小的作用域)
  • 统一术语:不要混用 start/begin,get/fetch/retrieve、controller/manager/mgr… 混用多种术语让人困惑,增加记忆负担
  • 避免歧义:如集合有一个 add 方法,可能是对两个集合进行求和,如果想向集合插入数据,再用 add 命名就不合适,应该用 insert 或 append
  • 避免误导:避免名字中的 l10O
  • 避免很长的名字中藏着细微的差别:XYZControllerForEfficientHandlingsOfStrings 和 XYZControllerForEfficientStorageOfStrings
  • 使用编程相关术语,如设计模式 Singleton、Facade、Visitor…
  • 使用特定领域/行业的术语
  • 添加有意义的语境:命名空间、类、函数都可以提供语境。如果没有这么做,可以添加前缀
  • 避免无意义的语境:例如开发一个名为 GasStationDeluxe 的应用,给其中每一个类都加上 GSD 前缀就是没有意义的

避免无意义的区分

  • 如果有三个类:Product、ProductInfo、ProductData,人们无法从名字中判断这三个类有什么区别,到底该用哪一个类。其中 Info、Data 就是毫无意义的区分

  • customer 和 customerInfo、account 和 accountData、message 和 theMessage,这些都是无意义的区分,即无法通过名字区分这两个变量有什么区别

例子

copyChars(char* a1, char* a2)

copyChars(char* dst, char* src)

thread t1;
thread t2;

thread mainThread;
thread signalHandlingThread;

避免使用编码

  • 避免匈牙利命名法,历史产物,现在编译器可以进行类型检查
  • 避免成员变量的 m_ 前缀,当类足够小的时候,就不需要成员前缀*
  • 不要给接口类名加 I 前缀

注1:关于这一点,应该遵循特定项目/组织的规定,比如 Google 的 C++ 编码规范要求成员变量使用 _ 后缀,常量使用 k 前缀…

注2:命名规范也可也通过工具进行自动检查,如 clang-tidy

这章后面单独说

  • “别给糟糕的代码加注释——重新写吧”
  • 好的代码有表达力(Self-documenting),不需要注释
  • 注释总是一种失败:注释可能说谎,修改代码有时会忘记修改注释,只有代码才能忠实的告诉你它做的事
  • 短小的函数、良好的命名表达力不差于注释
  • 注释不是越多越好,废话注释反而会干扰正常的代码阅读:本来结构紧凑的代码可以一览无遗,但是如果混入了大量的废话注释,就可能需要来回滚动屏幕才能看到全览

可以出现的注释

  • 版权声明
  • Public API 的说明(如 doxygen 格式的接口说明)
  • TODO 注释
  • 对意图的解释、阐述(强调 WHY,而不是 HOW)
  • 警示、强调

要避免的注释

  • 日志式注释:作者、变更记录等等 --> git 才是这些信息的归宿
  • 位置标记、分割,如 ///////////// INCLUDE ////////////////////
  • 注掉的的代码应该删掉,真的需要也可以从 git 历史中找到
  • 废话注释、循规蹈矩的注释:The constructor, Destructor, the name, the version…
  • 如果发现写注释的时候,只是机械的体力劳动,甚至只是复制粘贴, 那么这种注释多半没什么用,写了也未必有人看

格式化工具

我几乎没有操心过格式问题:缩进几个空格、在什么位置放括号、等号两边是否有空格、每行最多多少字符……所有这些问题都不应该花费任何精力。只要配置好了格式规则,有很多工具(如 clang-format)可以帮我们自动格式化代码,甚至可以配置为在保存文件或者 git commit 时,自动调用格式化工具格式化代码。

垂直格式

在本章中,有一点 clang-format 这样的工具可能还做不到,那就是垂直格式

  • 以适当空行分割
  • 相关函数、概念相关的代码应该放在一起
  • 避免废话注释把关联紧密的代码分开
  • 自顶向下:被调函数应该放在执行调用函数的下面。建立一种自顶向下贯穿代码的信息流:重要的高层抽象逻辑放在源文件的开头,越接近底层的实现细节越靠近文件的底部。这样只要阅读源码文件的上面几个函数就能大致了解整个文件的作用,无需了解低层实现细节

Team Rules

成熟的程序员不该纠结格式规则。每个程序员都有自己喜欢的规则,但只要在一个团队项目中,那么团队规则说得算。

说到这里,我想起了一个真实的小故事:博世的汽车多媒体部门,很多软件模块的代码缩进是 3 个空格。我一直好奇这奇葩的规则是怎么来的,直到某次例会,组里某个在博世工作了十几年的资深大佬讲出了背后的故事:制定编码规范的时候一部分程序员坚持用 2 个空格,而另外一部分坚持 4 个空格,谁也无法说服对方,最终各让一步,决定使用 3 个空格缩进。