《代码整洁之道》读书笔记
根据书名,可以知道这本书围绕“代码整洁”的思想和方法展开,但是个人认为,它不仅仅强调了代码整洁内容,更多的还包括代码测试、系统设计、并发编程的部分内容。
全书共分为17个章节和2个附加章节。整本书的章节感觉编排有点杂乱,根据个人理解整理成如下结构图:
概念
整洁代码
为何要有整洁代码?
- 代码确然是我们最终用来表达需求的语言,代码永存
- 糟糕的代码会毁灭一家公司
PS:当前信息时代,软件是生活方方面面,需求来源方法面面。尽管技术的进步,让解释需求的语言更接近现实,但当前其“代码”的本质没有变。
糟糕的代码为何会产生?
- 我们都说过有朝一日回头清理(亲手造成的混乱)。但敌不过勒布朗法则:稍后等于永不。
- 态度:抱怨需求变化背离设计,哀叹进度紧张,以及不专业的程序员
- 未始终保持代码整洁:混乱的源头在于当初的混乱,开发者背负期限压力,只好继续制造混乱
什么样的代码是整洁代码?
书中引述了多位其他书作者对整洁代码的描述,总结起来:
- 直接了当,缺陷难以隐藏
- 尽量减少依赖,便于维护
- 依据某种分层战略完善错误处理
- 性能调整至最优
- 不隐藏设计者意图
- 干净利落的抽象和直截了当的控制语句
- 应当有单元测试和验收测试
- 有意义的命名
- 仅提供一种做事的途径
- 尽量少的依赖关系,且要明确地定义和提供清晰的、尽量少的API
- 代码通过字面表达含义
- 几乎没有改进的余地,若企图改进,总会回到原点
- 能通过所有测试
- 没有重复代码
- 体现系统中的全部设计理念
- 包括尽量少的实体,比如类、方法、函数等
- 对象或方法不能做的事太多
- 提早构建简单抽象
- 深合己意,让编程语言是专为解决那个问题存在
作者信息与观点
- 对象导师整洁代码派
- 读代码比写代码的时间比例大,使之易读也是使之易写
- 必须时时保持代码整洁。童子军军规:让营地比你来时更干净。
- 作者曾著《敏捷软件开发:原则、模式与实践》
基本习惯
个人把这几个章节归并到一起来讲,主要因为对与程序运行,命名、格式、注释等是无关的要素。更多的体现再代码的书写习惯上。
有意义的命名
如何做到有意义命名?
- 名副其实:降低代码的模糊度(即上下文在代码中未被明确体现的程度)
int d; //elapsed time in days
int elapsedTimeInDays;
-
避免误导:拼写前后不一致,正确的方法是要以同样的方式写出同样的概念
-
做有意义的区分:如
accountData
与account
,在特定场合下,读者需要鉴别不同之处的方式区分两者。 -
使用读的出来的名称;使用可搜索的名称
-
避免使用编码
- 匈牙利语标记法应用Java中会增加阅读难度
- 不必用m_前缀来标明成员变量
- 接口与实现无需使用
I
前缀或Imp
后缀来区别
-
避免思维映射:不应当让读者在脑中把你的名称翻译为他们熟知的名称
-
类名和对象名称应该是名词或名词短语,不应当是动词;方法名应当是动词或动词短语
-
程序中不要使用俗话或俚语
-
抽象概念并选定一个词,且一以贯之;避免使用双关语,遵循”一词一义“
-
使用解决方案领域名称;使用源自所涉及问题领域的名称
-
添加有意义的语境,不添加没用的语境:如Address可以表示地区的地址,也可以表示IP等网络地址。
-
起名的难点:需要良好的描述技巧和共有的文化背景
格式
注重代码格式的目的
- 代码格式关乎沟通
- 代码格式影响代码的可维护性和可扩展性
- 代码格式很重要,需要严肃对待
垂直格式
- 一般可以使用200行左右(最高不超过500行)的代码文件即可构造出一个系统
- 短文件比长文件易于理解
- 源文件名称应当简单且一目了然
- 概念件垂直方向上的区隔:在封包声明、导入声明和每个函数之间,都有空白行隔开。
- 垂直方向上靠近:紧密相关的代码相互靠近
- 变量声明:应尽可能靠近其使用位置
- 实体变量:应该在类的顶部声明
- 相关函数:若某个函数调用另外一个,应把他们放到一起,调用尽可能在被调用者上面
- 概念相关:概念相关的代应该放到一起。相关性越强的彼此之间距离就应该越短。
横向格式
- 推荐短小的行,并尽量保持无需拖动滚动条可以看全代码行
- 适当的实用空格,强调操作符或代码间亲密关系
- 水平对齐方式没什么用,也会影响代码格式化工具的使用
- 适当的缩进可以有助于维护、阅读代码
- 遵循团队一致的代码风格
注释
注释的意义
- 并不能美化糟糕的代码
- 尽量用代码阐述和表达意图
- 注意维护注释
什么样是好注释
- 法律信息:如版权、外部文档的链接
- 提供信息的注释:如抽象方法的反回值的描述
- 解释意图:比如解释比较函数的返回值的含义
- 警示:对于一些非法或不安全行为提出警示
- TODO:未完成的内容标记
- 放大:用来放大某种看来不合理之物的重要性
- 公共API重点Javadoc
什么样是坏注释
- 不完整且表达不对的喃喃自语
- 多余且冗长的注释
- 误导性的注释
- 循规式的注释
- 日志式的注释
- 废话的注释
- 位置标记
- HTML注释
- 括号后面的注释
- 归属与署名
- 注释的代码
- 非本意的注释,与代码表达意义太远
- 大长段注释
- 八股式的Javadoc
程序结构
函数
如何保证函数的整洁?
- 函数的规则是:短小,更短小
- 函数的缩进层级不该多于一层或两层
- 函数应该做一件事,做好这件事,只做这件事
- 函数的语句要在同一抽象层级上
- 使用具有描述性的名称
- 分隔指令和询问
- 使用异常替代返回错误码
- 把错误处理当做一件事,处理错误的函数不该做其他事
- 减少重复
函数参数
- 最理想的参数数量是0,其次是1,再次是2,应尽量避免3参函数
- 尽量不使用标识参数
- 若函数参数过多,参数应封装为类
类
类如何组织?
类中的代码遵循标准的Java约定,一般按照出现的先后顺序为:
- 公共静态常量
- 私有静态变量
- 私有实体变量
- 公共函数
- 公共函数调用的私有函数紧随在该公共函数后面
类如何保持整洁?
- 第一条规则:类应该短小;第二天规则:还是要更短小
- 以计算权责的方式衡量
- 单一权则原则(SRP):类或模块应该只有一条加以修改的理由
- 内聚:类应该只有少数实体变量。若一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性
- 保持内聚会得到许多短小的类
- 遵循开发闭合原则(OCP):类应该对扩展开放,对修改封闭
- 遵循依赖倒置原则(DIP):类应该依赖抽象而不是依赖具体细节
对象和数据结构
数据抽象
- 隐藏实现,暴露抽象
对象和数据结构差异
- 对象把数据隐藏于抽象之后,暴露操作数据的函数
- 数据结构暴露其数据,不提供有意义的函数
对象与数据结构的二分原理
- 过程式的代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数;面向对象代码便于在不改动既有函数的前提下添加新类
- 过程式的代码难以添加新的数据结构,因为必须修改既有函数;面向对象代码难以添加新函数,因为必须修改所有类
得墨忒耳律
- 模块不应了解它所操作对象的内部情形;方法不应调用任何函数返回的对象的方法
即类C的方法f只应该调用以下对象的方法:
- C
- 由f创建的对象
- 作为参数传递给f的对象
- 由C的实体变量持有的对象
- 违法得墨忒耳律的代码常被称作火车失事;会导致对象和数据结构的混杂,增加了添加新函数的难度,也增加了添加新数据结构的难度
数据传送对象
- 只有公共变量,没有函数的类。这种数据结构有时被称为数据传送对象(DTO)
- ActiveRecord是一种特殊的DTO形势,这类数据结构中应避免掺杂业务规则方法
系统
这个部分主要围绕系统设计展开,包含错误处理,系统边界,持续改进方面
错误处理
- 使用异常而非返回码
- 有限书写捕获异常代码,以确定异常范围
- 使用未检异常
- 抛出异常时应当提供足够的环境说明,以便判断错误的来源于位置;创建充分的错误消息
- 依据调用者需要定义异常:定义异常类时,最重要的考虑应该是如何被捕获
- 不返回null值,不传递null值,防止意外Null异常
边界
- 建议不要将Map(或边界上的其他接口)在系统中传递。如果使用类似Map这样的接口,就把它保留在类或近亲类中,避免公众API中返回边界接口,或将边界接口作为参数传递给公共API
- 对第三方代码的了解可通过编写学习性测试来遍览和理解第三方代码,边界测试可以减轻外来的变化带来的负担
- Fack和Adapter是边界测试,也是应对未知边界的一种好方法
- 边界上的代码需要清晰的分割和定义了期望的测试,避免过多了解第三方代码中特定信息
- 减少引用第三方边界接口的代码位置
系统
将系统的构造和使用分开
软件系统应将起始过程和起始过程之后的运行时逻辑分离开,在起始过程中构建应用对象,也会存在互相缠结的依赖关系
- 将全部的构造过程搬迁到main或被称之为main的模块中
- 使用工厂方法构建对象
- 使用依赖注入,代理方法,控制反转,面向反面编程的
测试驱动系统架构
- 最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java对象实现。不同的领域之间用最不具有侵害性的方面或类方面工具整合起来,这种架构能测试驱动,就像代码一样。
优化决策
- 延迟决策之最后,基于最有可能的信息作出决策
使用可论证价值标准
- 不要纠缠于某个名声大噪的标准,却丧失了对为客户实现价值的关注
系统需要领域特定的语言
- DSL可以填平领域概念和实现概念的代码之间的“壕沟”
无论是设计系统还是单独模块,别忘了使用大概可开展工作的最简单方案。
迭进
简单设计的规则(以下按照重要程度排列)
- 运行所有测试
- 不可重复
- 表达了程序员的意图
- 尽可能减少类和方法的数
要点
- 设计必须制造出如预期一般工作的系统,这是首要因素
- 全面测试并持续通过所有测试的系统,就是可测试系统
- 测试消除了对清理代码就会破坏代码的恐惧
- 消除重复代码使得更容易的重构,符合整洁系统的目标
- 模板方法模式是一种移除高层重复的通用技巧
- 编写良好的单元测试具有表达性
- 表达力最重要的方式是尝试
并发
并发编程
为什么要并发?
- 并发是一种解耦策略,把做什么(目的)和何时做(时机)分解开
- 解耦的目的与时机能明显地改进应用程序的吞吐量和结构
迷思与误解
- 并发总能改进性能
- 编写并发程序无需修改设计
- 采用WEB技术的时候,理解并发问题并不重要
对并发的中肯认知
- 并发会在性能和编写额外代码上增加一些开销
- 正确的并发是复杂的,即便对于简单的问题也是如此
- 并发缺陷并非总能被重现,所以常被看做偶发事件而忽略,未被当作真的缺陷看待
- 并发常常需要对设计策略做根本性修改
并发防御原则
- 单一全责原则(SRP)
- 方法/类/组件应当只有一个修改的理由
- 并发相关代码有自己的开发、修改和调优的生命周期
- 并发相关代码有自己要应对的挑战,它和非并发相关代码不同,而且往往更为困难
- 即便没有增加周边应用程序的负担,写的不好的并发代码可能的出错方式数量也已经具有足够的挑战性
- 建议:分离并发相关代码与其他代码
- 限制数据作用域
- 谨记数据封装;严格限制可能对被共享的数据的访问
- 使用数据副本
- 线程应尽可能地独立
- 尝试将数据分解为可被独立线程操作的独立子集
执行模型
定义 | 描述 |
---|---|
限定资源 | 并发环境中有着固定尺寸或数量的资源 |
互斥 | 每一时刻仅有一个线程能访问共享数据或共享资源 |
线程饥饿 | 一个或一组线程在很长时间内或永修被禁止 |
死锁 | 两个或多个线程互相等待执行结束。每个线程都拥有其他线程需要的资源, 如果得不到其他线程的拥有的资源,就无法终止。 |
活锁 | 执行次序一致的线程,每个都想要起步,但发现其他线程已经“在路上”。 由于竞争的原因,线程会持续尝试起步,但在很长时间内都却无法如愿, 甚至永远无法启动 |
- 生产者-消费者模型
- 读者-作者模型
- 宴席哲学家模型
警惕同步方法直接的依赖
建议:避免使用一个共享对象的多个方法
有时必须使用一个共享对象的多个方法。在这种情况发生时:
- 基于客户端的锁定——客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码(一般基于客户端的锁定不可靠)
- 基于服务端的锁定——在服务端内创建锁定服务端的方法,调用所有方法,然后解锁。让客户端代码调用新方法
- 适配服务器——创建执行锁定的中间件。这是一种基于服务端的锁定的例子,但不修改原始服务端代码
保持同步区域微小
- 应尽可能少地设计临界区
编写正确的关闭代码
- 尽早考了关闭问题,尽早令其正常工作。这会花费比你预期的更多的时间。检视既有算法,因为这可能会比想象中难的多。
测试线程代码
建议:编写有潜力暴露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,就跟踪错误,别因为后来测试通过了而忽略造成失败的缺陷。
- 将伪失败看做可能的线程问题:不要将系统的错误归咎于偶发事件
- 先使非线程代码可工作:不要同时追踪非线程缺陷和线程缺陷,确保代码在线程之外可以工作
- 编写可插拔的线程代码:
- 编写可在数个配置环境下运行的线程代码
- 单线程与多线程不同的情况下执行
- 线程代码与实物或测试替身互动
- 用运行快速、缓慢、有变动的测试替身执行
- 将测试配置为能运行一定数量的迭代
- 编写调整的线程代码:允许线程数量的可调整
- 运行多于处理器数量的线程
- 在不同平台上运行
- 装置试错代码
- 硬编码:可以手工向代码中插入
wait,sleep,yield,priority
等调用 - 自动化:使用面向方面编程框架,cglib,ASM工具装置试错代码
- 硬编码:可以手工向代码中插入
并发编程II
死锁
死锁的发生的四个条件
- 互斥
- 上锁与等待
- 无抢夺机制
- 循环等待
测试
单元测试
TDD三定律
- 第一定律 在编写不能通过的单元测试前,不可编写生产代码
- 第二定律 只可编写刚好无法通过的单元测试,不能编译也算不通过
- 第三定律 只可编写刚好足以通过当前失败测试的生产代码
保持测试整洁
- 测试代码和生产代码一样重要
- 测试代码需要被思考,被设计,被照料
- 整洁测试的三个要素:可读性、可读性和可读性
- 测试程序呈现:构造——操作——检验
- 构造测试数据
- 操作测试数据
- 检验操作结果
- 总体上要讲不必要的细节抽象为函数
- 面向特定领域的测试语言
- 双重标准:测试的开发与生产代码开发是有不同的评价标准
- 每个测试一个断言
- 每个测试一个概念
FIRST
整洁测试应该遵循5条规则:
- 快速:测试应该够快
- 独立:测试应该相互独立
- 可重复:测试应当可以在任何环境中重复通过
- 自足验证:测试应该有布尔值输出,不应该通过查看日志文件确认是否通过
- 及时:测试应及时编写
Junit内幕
个人感觉与整洁无关,本初略
实验
该部分两个章节,需自行体会作者实验
总结
味道与启发
注释
- C1 不恰当的信息
- C2 废弃的注释
- C3 冗余注释
- C4 糟糕的注释
- C5 注释掉的代码
环境
- E1 需要多步才能实现的构建
- E2 需要多步才能做到的测试
函数
- F1 过多的参数
- F2 输出参数违反直觉
- F3 标识参数(布尔参数)
- F4 死函数(永不被调用的函数)
一般性问题
- G1 一个源文件中存在多种语言
- G2 明显的行为未被实现
- G3 不正确的边界行为
- G4 忽视安全
- G5 重复
- G6 在错误的抽象层级上的代码
- G7 基类依赖派生类
- G8 信息过多:限制类或模块暴露的接口数量
- G9 死代码:删除不执行的代码
- G10 垂直分隔:变量和函数应该靠近在被使用的地方定义
- G11 前后不一致:最小惊异原则,小心选择约定,选中就持续遵循
- G12 混淆视听:没有实现的默认构造器无用
- G13 人为耦合:不互相依赖的东西不该耦合
- G14 特性依恋:类的方法只对其所属类中的变量和函数感兴趣
- G15 选择算子参数:不适用选择算子作为函数参数
- G16 晦涩的意图:联排表达式、匈牙利表示法、魔术数遮蔽作者的意图
- G17 位置错误的权责
- G18 不恰当的静态方法
- G19 使用解释性变量
- G20 函数名称应该表达起行为
- G21 理解算法
- G22 把逻辑依赖改为物理依赖
- G23 用多态提点if/else或switch/case
- G24 遵循标准约定
- G25 用命名常量替代魔术数
- G26 准确:代码中做决定时,确认自己足够准确
- G27 结构甚于约定
- G28 封装条件
- G29 避免否定条件
- G30 函数只做一件事
- G31 掩蔽时序耦合
- G32 别随意
- G33 封装边界条件
- G34 函数应该只在一个抽象层级上
- G35 在较高层级放置可配置数据
- G36 避免传递浏览
Java
- J1 通过使用通配符避免过长的导入清单
- J2 不要继承常量
- J3 常量和枚举
名称
- N1 采用描述性名称
- N2 名称应与抽象层级相符
- N3 尽可能使用标准命名法
- N4 无歧义命名
- N5 为较大作用范围选用较长的名称
- N6 避免编码
- N7 名称应该说名副作用
测试
- T1 测试不足:不能“看起来够了”
- T2 使用覆盖率工具
- T3 别略过小测试
- T4 被忽略的测试就是对不确的事物的疑问
- T5 测试边界条件
- T6 全面测试相近缺陷
- T7 测试失败的模式有启发性
- T8 测试覆盖率的模式有启发性
- T9 测试应该快速
参考
- 《代码整洁之道》 [美]罗伯特·C.马丁 著 韩磊 译