《代码整洁之道》读书笔记
命名
- 名副其实,体现本意
- 避免误导
- 有意义的区分,要区分名称,就要让读者能够鉴别出不同之处
- 长名称胜过短名称,名称长度应该和它的作用于大小相对应
- 使用读得出来的名称
- 使用可搜索的名称
- 避免使用编码,避免类型编码,避免成员前缀,接口的前缀和实现的后缀取舍
- 避免思维映射,明确才是王道
- 类名,类名和对象名应该是名词或名词短语
- 方法名,应当是动词或动词短语
- 别扮可爱
- 每个概念对应一个名词
- 别用双关语,代码作者应尽力写出易于理解的代码
- 使用解决方案领域名称,比如访问者模式的程序名称AccountVisitor就富有意义
- 使用源自所涉及问题领域的名称
- 不要添加没用的语境,只要短名称足够清楚,就要比长名称要好
函数
- 函数应该短小
- 函数应该是做一件事,并做好这一件事
- 函数中的语句应该在同一抽象层级上
- switch语句应该隐藏在较低的抽象层级,如抽象工厂中,而且永远不重复。应该符合单一原则SRP,开闭原则OCP
- 函数参数
- 一元函数的普遍形式
- 标识参数
- 二元函数
- 三元函数
- 参数对象
- 参数列表,如:
public String format(String format, Object... args)
- 动词与关键字,如:
writeField(name)
- 无副作用,函数要么做一件事,要么回答一个问题,不要同时做这两个
- 参数应该尽量少,如果参数太多,应该把几个参数抽象成对象
- 分隔指令与询问
- 使用异常,不要返回错误码
- 抽离错误处理语句,处理错误的函数不应再做其他事。
- 消除冗余,减少重复
- 结构化编程,大函数中只该有一个return语句,不能有goto语句
注释
- 注释不能美化糟糕的代码,不要依赖注释来解释意图,而是尽量通过代码本身
- 对意图的注释,对特殊代码段的解释,有助于阅读代码的人更好的理解代码
- 减少喃喃自语,废话,以及误导性的注释
- 不要注释代码,直接删掉
格式
- 短文件短的类易于理解
- 概念间垂直方向上的间隔,不同概念用空白行隔开,关系紧密的概念应该相互靠近;变量声明应尽可能靠近其使用位置
- 横向格式。行上限字符<200个字符;不必水平对齐
- 团队规则
对象和数据结构
- 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,使用面向对象代码便于在不改动既有函数的前提下添加新类。反过来讲,过程式代码难以添加新数据结构因为必须修改所有函数,面向对象代码难以添加新函数因为必须修改所有类
- 得墨忒耳律(The Law of Demeter)认为,模块不应了解它所操作对象的内部情况。方法不应调用由任何函数返回的对象的方法。只和朋友谈话,不和陌生人谈话
- 在builder模式、函数式编程、Spark框架应用、Flink框架应用等连串调用很常用。连串调用/链式调用是否合理具体情况具体分析。连串调用是肮脏的风格是因为违反了得墨忒耳律。这类连串调用应该避免:
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
最好做如下切分:1
2
3Options opts = ctxt.getOptions(); File scratchDir = opts.getScratchDir(); final string outputDir = scratchDir.getAbsolutePath();
错误处理
- 使用异常而非返回错误码
- 使用不可控异常(unchecked exception)。可控异常(checked exception)的利与弊:每个方法前面都列出它可能传递给调用者的异常,而且这些异常是方法类型的一部分,如果签名与代码实际所做之事不符代码在字面上就无法编译;可控异常的代价就是违反开闭原则,对软件中较低层级的修改,就需要在catch语句和抛出异常处之间的每个方法签名中声明该异常。如果在编写一套关键代码库,则可控异常有时也会有用:必须捕获异常,但是对于一般的应用开发,其依赖成本高于收益
- 给出异常发生的环境说明
- 依调用者需要定义异常类。将第三方API定义打包类非常有用,降低对它的依赖,测试自己的代码时,打包有助于模拟第三方调用
- 定义常规流程。特例模式,创建一个类或配置一个对象,用来处理特例
- 别返回null值
- 别传递null值
边界
- 学习性测试第三方API
- 将第三方API包装起来,以此来把第三方API的边界接口与程序的其他部分隔离开来
- 整洁的边界。边界上的代码需要清晰地分割和定义了期望的测试。我们通过代码中少数几处引用第三方边界接口的位置来管理第三方边界,可以使用Adapter模式将我们的接口转换为第三方提供的接口,当第三方代码有改动时修改点也会更少
单元测试
- TDD(Test-Driven Development)三定律
- 定律一、在编写不能通过的单元测试前,不可编写生产代码
- 定律二、只可编写刚好无法通过的单元测试,不能编译也算不过
- 定律三、只可编写刚好足以通过当前失败测试的生产代码
- 保持测试整洁,测试代码和生产代码一样重要
- 每个测试一个断言,每个测试一个概念
- F.I.R.S.T原则(Fast,Independent,Repeatable,Self-Validating,Timely)
- 快速(Fast),测试应该快(及时反馈出业务代码的问题)
- 独立(Independent) 每个测试流程应该独立
- 可重复(Repeatable) 测试应该在任何环境上都能重复通过
- 自我验证(Self-Validating) 测试应该有bool输出
- 及时(Timely) 测试应该及时编写
类
- 类应该短小
- 类只应该有一个权责——只有一个修改的理由(单一权责原则,SRP)
- 系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达到期望的系统行为
- 保持内聚性就会得到许多短小的类
- 如果类丧失了内聚性,就拆分它
- 为了修改而组织
- 类应该对扩展开发,对修改封闭(开放-闭合原则,OCP)
- 类应该依赖于抽象而不是具体细节,通过部件之间的解耦来隔离修改(依赖倒置原则,DIP)
系统
- 将系统的构造和使用分开。
- 依赖注入(Dependency Injection),控制反转(Inversion of Control)
- 扩容
- 面向切面编程(Aspect-Oriented Programming),类的横向解耦
- 测试驱动系统架构。最佳的系统架构由模块化的关注面领域组成,每个关注面均用语言中的对象实现。不同的领域直接用最不具有侵害性的方面或类方面工具整合起来。这种架构能测试驱动
- 系统需要 领域特定语言(Domain-Specific Language)。领域特定语言允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用POJO(Plain Ordinary Java Object)来表达
迭进
- 简单设计规则1:运行所有测试
- 简单设计规则2~4:重构
- 不可重复。可用通过应用模板方法模式来消除明显的重复
- 表达力
- 尽可能少的类和方法
并发编程
- 关于编写并发软件的中肯说法
- 并发会在性能和编写额外代码上增加一些开销
- 正确的并发是复杂的,即便对于简单的问题也是如此
- 并发缺陷并非总能重现, 所以常被看做偶发事件忽略,未被当做真的缺陷看待
- 并发常常需要对设计策略的根本性修改
- 并发防御原则
- 单一权责原则(SRP)。建议:分离并发相关代码与其他代码
- 推论:限制数据作用域。建议:谨记数据封装;严格限制对可能被共享的数据的访问
- 推论:使用数据副本
- 推论:线程应尽可能地独立。尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集
- 了解执行模型
- 生产者-消费者模型
- 读者-作者模型
- 宴席哲学家
- 保持同步区域微小。尽可能减小同步区域
- 编写正确的关闭代码很难。例如:父线程告知全体子线程结束信号并且等待子线程结束,如果子线程死锁;父线程等待子线程结束,两个子线程之间有生产者/消费者模型操作,消费者线程可能在等待生产者线程发来消息,而无法接收关闭信号
- 测试线程代码。编写有潜力暴露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行
- 将伪失败看作可能的线程问题。不要将系统错误归咎于偶发事件
- 先使非线程代码可工作。不要同时追踪非线程缺陷和线程缺陷
- 编写可插拔的线程代码,能在不同的配置环境下运行
- 装置试错代码
- 硬编码。手工插入wait(), sleep(), yield(), priority()的调用,但是这种手法有些缺点
- 自动化。
- 了解Java并发库。java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks
味道与启发
注释
- 不恰当的信息注释,废弃的注释,冗余的注释,糟糕的注释都应该删除
- 注释掉的代码也应该删除,因为版本控制工具能保留它
环境
- 需要简单几步就能实现构建。单个命令检出系统,单个命令构建
- 需要简单几步就能实现测试
函数
- 不要过多的参数,不要输出参数,标识参数,删除不用的函数
一般性问题
- 一个源文件中存在多种语言。理想的源文件只包括一种语言
- 明显的行为未被实现
- 不正确的边界行为。别依赖直觉,探索每种边界条件
- 减少重复
- 代码抽象层级。抽象类来容纳高层级概念,派生类来容纳低层次的概念。
- 基类不该依赖于派生类
- 设计良好的模块有着非常小的接口
- 不互相了依赖的东西股改耦合
- 选择算子参数。多个函数通常优于单个函数传递某些代码来选择函数行为
- 使用解释性变量
- 用多态替代 if/else 和 switch/case
- 封装边界条件
- 在较高层级放置可配置数据
- 避免传递浏览
感悟:简单设计原则
- 命名名副其实,体现本意
- 函数应该短小,参数尽量少;无副作用,做一件事或回答一个问题
- 运行所有测试
- 重构
- 减少重复
- 尽量少的类和方法
- 接口要小
- 提高内聚,减少耦合
- 表达力(通过不断尝试和调整,增强代码的表达力)