Skip to content
本站总访问量次, 访客数人次

代码整洁之道(Clean Code)读书笔记

前言

这是一本介绍改善代码质量的书。本书给出了一系列行之有效的整洁代码操作实践。

好的代码需要符合各种规范,大家熟知的有:开闭原则、单一责任原则等等。为什么要符合这些原则呢?在此之前我从未思考过这件事情,直到工作后、并读完这本书时,有了很深刻的感悟:

  • 对于个人来说:写出可读性高的代码是一项非常重要的技能。公司里面的程序员都是团队合作,你写的代码需要经过同事review过后才能合入到线上分支。如果你的代码别人看不懂、或者实现得十分挫,那你的 merge request 大概率会被拒绝。打工人的时间都十分宝贵,你的同事不会花半天来理解你糟糕的实现。
  • 对于项目来说:无论是开源项目还是公司内部的项目,开发人员变更、项目功能迭代 都是常有的事,因此保证高质量的项目代码(个人理解可以约等于可维护性)应当是最重要的事情之一。

这本书里面提到了大量的、写出高质量代码的方法。虽然很难一次性掌握,但有三个概念让我印象十分深刻:

  • 可扩展性:在功能演进时,是否要修改大量代码、甚至是重构?
  • 可读性:是否很容易让人读懂,并且没有歧义?
  • 简洁性:代码的逻辑、实现是否精简?

只要在写代码的时候,大脑里时刻想着这3个概念,就能联想到这本书里的诸多方法,就能做到“下笔如有神了”!写好代码就像写好文章一样,好的代码、文章都需要通过符合一定的规范,让别人很轻易地读懂

笔记内容有所侧重,不能代替原文。有条件建议看完笔记后阅读原文。

第一章 整洁代码

第二章 有意义的命名

  • 命名中不要包含无用信息。
    • e.g. 在变量名上添加一些不必要的前缀、后缀,它们不能提供更多有用的信息。
  • 使用读得出来的名称。
    • e.g. 变量名 timestamp 优于 ymdhms(表达年月日时分秒的意思)
  • 使用可搜索的名称
    • 在大型项目中,经常需要通过全文搜索、跳转来找到相关的代码实现;变量名、函数名、类名需要可搜索
    • e.g. 不要直接使用魔数、字符串字面量、硬编码,使用有意义的大写变量名指代,或者使用枚举类型。int PI=3.14
  • 接口和实现的命名:
    • 接口在类名前加I开头:这个不咋常见,但是有的代码会这么干
    • 实现类的类名加上Impl后缀:java.net.SocketOptions是接口类,java.net.DatagramSocketImpl 和 java.net.SocketImpl 是它的两个实现类(通常是抽象类)
  • 避免歧义
    • 如变量 pi 通常指代圆周率,避免用它指代其他
  • 类名应当是名词或者名词短语,并且有区分度
    • e.g. 如 Customer,避免Info(没有表达什么信息),避免ParseAddress(AddressParser更好)
  • 方法名应当是动词或者动词短语
    • e.g. 常用的动词前缀包括 post、delete、save、get、set、is
  • 命名不要使用俚语,要含义明确,通用性强
    • e.g. abort() 比 eatMyShorts() 更好
  • 给每个抽象概念一个词,并一以贯之
    • e.g. 当表示获取概念时,只用 fetch、retrieve、get 中的一个
  • 优先使用解决方案领域的名称
    • e.g. 使用某种设计模式时,类的命名需要遵循设计模式中概念。如 java.net.DatagramSocketImplFactory 是 datagram socket 实现的 一个工厂接口;java.nio.file.FileVisitor 是访问者模式下一个文件 visitor

第三章 函数

  • 函数原则1:短小
    • e.g. if/else/while 等 代码块保持一行代码,如果要做很多事情,就抽象出一个函数。
  • 函数原则2:只做一件事,做好一件事
  • 保证函数内的逻辑在同一层抽象
    • e.g. 类似第1步、第2步 等等,第 1.1 步的具体实现不要同时出现,可以将第 1.1 步放到一个函数调用中。如 getOrCreateInstance 函数逻辑:判断是否有Instance,有的话返回,没有的话调用 createInstance 函数并赋值,最后返回。
  • 对于需要switch的函数,可以考虑是否要引入工厂类,利用多态实现。
    • e.g. 对于函数中有 switch 根据参数返回对应的类的逻辑,引入工厂类,在工厂的generate方法里面使用Switch 语句
  • 函数名称要有描述性,不要怕长;一组函数名要能描述完成一件事情的步骤。
    • e.g. getAttribute, updateAttribute, setAttribute 完成对数据库的更新
  • 函数参数尽可能少,函数名+参数名 的组合最好能描述清楚要干的事情
    • e.g. String updatePassword(String password)
  • 明确函数的副作用(注释等手段),但尽量减少函数产生副作用的情况
  • if 条件语句 只做判断,不修改状态
    • e.g. if(set(attribute, val)) ... 这里set(string attribute, string val) 的那是,当 attribute 不存在时会返回false。这个实现不好,因为 if(set(attribute, val)) ... 语句表意不清晰,无法知道set失败是因为属性不存在还是因为属性已经被设置过了。修改成 if(attributeExists(attribute)) {set(attribute, val);}
  • 使用异常替代错误码
    • try 代码块、catch 代码块中逻辑抽象出来,定义成一个函数
    • 错误码可扩展性差,而异常可通过继承来扩展,并且异常可以定义异常信息,方便定位问题
  • 避免重复的方法
    • e.g. get的逻辑分别在 get()、getOrCreate() 方法中都实现了,可以将 getOrCreate中的逻辑替换为对 get() 方法 第一个调用
  • 如何写出满足前述要求的函数:先完成功能开发,然后按照前述规则重构。

大师级程序员讲系统当成故事来讲,而不是当成程序来写。

第四章 注释

  • 注释是代码的补充,常用于代码表述不清楚的时候,最好能做到不注释也能表达清楚意思
  • 有用的注释:
    • 法律信息,如开源代码声明licence
    • 函数意图、参数、返回值的描述
    • 重要的warning: e.g. Don't run unless you have time to kill
    • ToDo
  • 避免误导性注释、多余的注释

第五章 格式

  • 每个空白行都是一条线索,标识出新的逻辑的开始
  • 函数变量声明应当经可能靠近其使用的位置
  • 类实体变量
    • c++ 的剪刀原则,将类变量放在类定义底部
    • java 放在类定义的顶部
  • 定义函数时,自上而下的调用依赖顺序,或者自下而上的调用依赖顺序
  • 注意对齐与缩进, 可以使用checkstyle来规范项目代码,比如google的c++/java checkstyle
  • 新增代码风格与项目原有代码风格保持一致。或者在立项之前,团队提前约定好代码风格。

第六章 对象和数据结构

  • 将变量设为私有的理由:不让变量被其他项目、组件依赖。
    • e.g. 一旦被其他组件依赖,升级、变更将变得复杂,甚至导致事故。但是如果变量是私有的,就表明没有不变更的承诺,由变更导致的问题需要调用方自己承担。(公司内项目之间复杂的依赖关系是个令人头疼的问题,笔者在公司中就遇到过这样的事故)

第七章 错误处理

  • 使用异常而非返回码:前面已有描述
  • 给出异常发生的环境说明
    • 打印异常栈、以及发生异常情况的说明
  • 别返回null值:尽量使用空对象,而不是返回null
    • e.g. 避免链式调用时,在 null 值上调用方法 抛出 NPE
  • 传递的参数尽量不为 null:避免复杂的 null 值判断 或者 在null值上面调用方法导致 NPE
  • 将异常处理逻辑抽离出来放到函数中去,保持调用函数短小、逻辑清晰

第八章 边界

  • 对不稳定的第三方API进行包装后使用(减少耦合),避免因接口调用变动后而大量修改代码。尤其使用一些不稳定的第三方库。
    • e.g. 调用某个第三方实现的hashmap,定义一个 HashMapWrap,将 get、put、contains 等方法包装起来
  • 不同模块之间使用接口、Fake对象(XXXAdapter类)对接,从而实现解耦、并行开发
    • e.g. XXXAdapter 类屏蔽了具体的实现。

第九章 单元测试

  • 每个单测中需要有断言语句,每个单元测试只测试一个概念
    • 有断言语句保证测试能检测出错误;只测试一个概念 能够反向 系统中实现的函数只做一件事

第十章 类

  • 类中内容的顺序
    • 公有静态变量-> 私有静态变量-> [公有成员变量]-> 私有成员变量 -> 方法
  • 类应该短小:1. 单一权责原则 single responsiblity principal(srp), 2. 类应该高内聚,即每个方法尽量用到所有成员变量,否则考虑将类拆开
  • 类抽象设计时,尽量符合开闭原则(OCP),即通过类继承来新增特性
  • 依赖反转(DIP),类应当依赖抽象的设计/概念,而不是细节。避免依赖的细节变动。
    • e.g. 即依赖接口,而不是具体的实现

第十一章 系统

  • 分开类的初始化与使用:工厂模式 & DI/IOC
    • e.g. 主要是讲 IOC,使用某个类时不用先手动初始化参数类
  • AOP: 面向切面编程,随着系统的演化,各个切面的具体实现也会随之变化
    • e.g. 日志切面、监控切面 等等
  • 高度模块化 + 测试驱动
    • 最佳的系统架构由模块化的关注面领域组成,每个关注面均采用纯Java(或其他语言)对象实现,不同领域之间用最不具有侵害性的方面或类方面工具整合起来。这种架构能测试驱动,就像代码一样。

第十二章 迭进

  • 不可重复:1. 降低代码的重复, 2. 避免逻辑上的重复(可以使用模板方法 设计模式)
    • 可以配置 IDE,检测出重复的代码
    • 重复代码除了让人观感不好,另外,修改实现时容易遗漏,从而出错。
  • 表达力:大部分软件项目的主要成本在于长期维护,因此代码的可读性十分重要

第十三章 并发编程

  • 并发防御原则:减少线程间的共享数据
  • 深入了解 Java并发库
    • 《Java并发编程》(《Concurrent Programming In Java》)
    • 掌握 java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks
  • 尽可能减少同步区域:即 锁住的 代码行数尽可能少
  • 尽早考虑关闭、退出问题
    • e.g. 线程的退出,或关闭文件句柄

第十四章 逐步改进

先编写可运行的程序,然后随着功能扩展,通过增加抽象设计、模块化,来逐步改进。

第十五章 JUnit内幕

略(前述方法的应用)

第十六章 重构 SerialDate

略(前述方法的应用)

第十七章 味道与启发

罗列了一些代码规范,这里只摘抄原文中个人认为较为重要的几点。

  • 一般性问题:明显的行为未被实现
    • e.g. 在实现字符串类的时候,使用者自然而然地预期有 忽略大小、格式化 两个函数功能。
  • 用命名常量来替代魔数
  • 一般性问题:掩蔽时序耦合。实现一组函数时有必要通过函数的参数让函数间的时序耦合可见。
    • e.g. getUp(), dressUp(), work() 三个函数可以定义为 AwakeMan getUp(), DressedMan dressUp(AwakeMan), void work(DressedMan),从而通过创建的顺序暴露了时序上的耦合,避免函数不按顺序调用。
  • 封装边界条件,把处理corner case 的代码集中到一处,不要散落在代码中。
    • e.g. 比如把代码中的各种 + 1 操作抽象成一个函数,nextLevel() 等。
  • 函数中的语句应当在同一抽象层级上,该层级应该是函数名所示操作的下一层
    • e.g. renderHtml()方法里面包含的逻辑是 renderHeader()、renderBody()、renderFooter()
  • 常量 vs 枚举:使用枚举优于使用常量(Java)
    • 因为枚举 拥有方法、字段,比常量更优表达性
  • 名称应该说明副作用
    • e.g. get() vs getOrCreate()
  • 使用代码覆盖率工具,可以代码中加入测试依赖,或者IDE插件
  • 测试应该快速:加快UT执行时间也有利于提高开发效率
    • 如果开发完,跑一次UT耗时比模拟上线测试还慢,那不如直接在测试环境测试。

附录A 并发编程 II

略,并发编程相关知识

附录B org.jfree.date.SerialDate

略,附录的代码清单。

Comments