Clean Code

Table of Contents

1

1.1 Meta Notes

1.2 Books

1.2.1 DONE Combo #1

  1. DONE 金字塔原理
  2. TODO Beast Machines: Transformers (cartoon)
  3. DONE 论持久战
  4. DONE 一看就懂的经济常识全图解
  5. DONE 刻意练习
  6. DONE 系统之美
  7. DONE 邓小平时代

1.2.2 INPROGRESS Combo #2

  1. DONE 新经济, 新规则 (Kevin Kelly)
  2. DONE 麦肯锡问题解决方法与技巧
  3. DONE 国产遥感卫星进展与应用

1.2.5 INPROGRESS Effective Java

1.2.6 DONE Don't Make Me Think

1.2.7 DONE Clean Code

1.2.8 INPROGRESS Deep Learning

1.2.9 INPROGRESS Async JavaScript

1.2.10 INPROGRESS ng-book2

1.2.11 INPROGRESS Combo #3: SLAM

2 Info

3 Contents

3.1 第 1 章 整洁代码

1.1 要有代码 1.2 糟糕的代码 1.3 混乱的代价 1.3.1 华丽新设计 1.3.2 态度 1.3.3 迷题 1.3.4 整洁代码的艺术 1.3.5 什么是整洁代码 1.4 思想流派 1.5 我们是作者 1.6 童子军军规 1.7 前传与原则 1.8 小结 1.9 文献

  • 即便是人类倾其全部的直觉和创造力, 也造不出满足客户模糊感觉得成功系统来
  • 澡泽 wading
  • 勒布朗 (LeBlanc) 法则: 稍后等于永不 (Later equals never)
  • 即使你是为医生, 病人请求你在给她做手术前别洗手, 因为那会花太多时间, 你会照办吗? 本该是病人说了算; 但医生却绝对应该拒绝遵从. 为什么? 因为医生比病人更了解疾病和感染的风险. 医生如果按病人说的办, 就是一种不专业的态度 (更别说是犯罪了).
  • 干净利落的抽象 (crisp abstraction)
  • 我在意代码重复, 如果同一段代码反复出现, 就**表示某种想法未在代码中得到良好的体现**, 我尽力去找出到底那是什么, 然后再尽力用清晰地表达出来
  • 任何门派都并非绝对正确. 不过, 身处某一门派时, 我们总以其所专之技为善
  • 童子军军规: 让营地比你来时更干净

3.2 第 2 章 有意义的命名

2.1 介绍 2.2 名副其实 2.3 避免误导 2.4 做有意义的区分 2.5 使用读得出来的名称 2.6 使用可搜索的名称 2.7 避免使用编码 2.7.1 匈牙利语标记法 2.7.2 成员前缀 2.7.3 接口和实现 2.8 避免思维映射 2.9  类名 2.10 方法名 2.11 别扮可爱 2.12 每个概念对应一个词 2.13 别用双关语 2.14 使用解决方案领域名称 2.15 使用源自所涉问题领域的名称 2.16 添加有意义的语境 2.17 不要添加没用的语境 2.18 最后的话

  • 做有意义的区分
  • 使用可搜索的名称 (拒绝 magic number)
  • 聪明程序员和专业程序员之间的区别在于, 专业程序员了解, *"明确是王道"*
  • 言到意到, 意到言到
  • 使用解决方案领域名称???: 给这些事取个技术性的名称, 通常是最靠谱的做法
  • 添加前缀 addrFirstName, addrLastName, 以此提供语境

3.3 第 3 章 函数

3.1 短小 3.2 只做一件事 3.3 每个函数一个抽象层级 3.4 switch 语句 3.5 使用描述性的名称 3.6 函数参数 3.6.1 一元函数的普遍形式 3.6.2 标识参数 3.6.3 二元函数 3.6.4 三元函数 3.6.5 参数对象 3.6.6 参数列表 3.6.7 动词与关键字 3.7 无副作用 3.8 分隔指令与询问 3.9 使用异常替代返回错误码 3.9.1 抽离 Try/Catch 代码块 3.9.2 错误处理就是一件事 3.9.3 Error.java 依赖磁铁 3.10 别重复自己 3.11 结构化编程 3.12 如何写出这样的函数 3.13 小结 3.14 SetupTeardownIncluder 程序 3.15 文献

  • 怎样才能让函数表达其意图?
  • 经验告诉我, 函数就要小, 100 太长, 20 行封顶最好
  • 函数不应该大到足以容纳嵌套结构, 所以, 函数的缩进层次不应该多于一层或两层
  • 函数是做一件事情呢, 还是做三件事情? 要确保函数只做一件事情, 函数中的语句都在同一抽象层级上
  • SRP, single responsibility rule 单一责权原则
  • OCP, open closed principle, 开放闭合原则
  • 别害怕花时间取名字; 你当尝试不同的名称, 实测其阅读效果
  • 分隔指令与询问 (query and command)
  • 使用异常代替返回错误码
  • 依赖磁铁: dependency magnet, 许多其他类都得导入和使用它
  • Dijkstr 认为, 每个函数都应该有一个入口, 一个出口; 遵循这些规则, 意味着每个函数 中只该有一个 return 语句, 循环中不能有 break 或 continue 语句, 而且永永远远不能有任何 goto 语句

3.4 第 4 章 注释

4.1 注释不能美化糟糕的代码 4.2 用代码来阐述 4.3 好注释 4.3.1 法律信息 4.3.2 提供信息的注释 4.3.3 对意图的解释 4.3.4 阐释 4.3.5 警示 4.3.6 TODO 注释 4.3.7 放大 4.3.8 公共 API 中的 Javadoc 4.4 坏注释 4.4.1 喃喃自语 4.4.2 多余的注释 4.4.3 误导性注释 4.4.4 循规式注释 4.4.5 日志式注释 4.4.6 废话注释 4.4.7 可怕的废话 4.4.8 能用函数或变量时就别用注释 4.4.9 位置标记 4.4.10 括号后面的注释 4.4.11 归属与署名 4.4.12 注释掉的代码 4.4.13 HTML 注释 4.4.14 非本地信息 4.4.15 信息过多 4.4.16 不明显的联系 4.4.17 函数头 4.4.18 非公共代码中的 Javadoc 4.4.19 范例 4.5 文献

  • 因为注释会撒谎, 不准确的注释要比没注释坏的多; 只有代码能忠实地告诉你它做的事
  • 程序员应当负责将注释保持在**可维护, 有关联, 精确**的高度
  • 如何改善? 简单到只需要创建一个描述与注释所言同一事物的函数即可
  • *"它并不能比代码本身提供更多的信息"*
  • 拒绝位置标记
  • 当你发现自己想标记右括号, 其实应该做的是缩短函数
  • 曾经有一段时间, 注释掉的代码可能有用. 但我们已经有源码控制系统如此之久…
  • TMI: 信息过多
  • 不明显的联系

3.5 第 5 章 格式

5.1 格式的目的 5.2 垂直格式 5.2.1 向报纸学习 5.2.2 概念间垂直方向上的区隔 5.2.3 垂直方向上的靠近 5.2.4 垂直距离 5.2.5 垂直顺序 5.3 横向格式 5.3.1 水平方向上的区隔与靠近 5.3.2 水平对齐 5.3.3 缩进 5.3.4 空范围 5.4 团队规则 5.5 鲍勃大叔的格式规则

  • 代码格式关乎沟通, 而沟通是专业开发者的头等大事
  • 有可能用大多数为 200 行, 最多 500 行的单个文件构造出色的系统
  • 垂直方向, 水平方向
  • 遵循 '无需拖动滚动条到右边的原则'
  • 它们需要拥有一致和顺畅的风格

3.6 第 6 章 对象和数据结构

6.1 数据抽象 6.2 数据、对象的反对称性 6.3 得墨忒耳律 6.3.1 火车失事 6.3.2 混杂 6.3.3 隐藏结构 6.4 数据传送对象 6.5 小结 6.6 文献

  • 为什么还是有那么多程序员给对象自动添加赋值器和取值器, 将私有变量公之于众, 如同它们根本就是公有变量一般呢?!! (lombok…)
  • 暴露了实现
  • 对象和数据结构之间的二分原理: 过程式代码 (使用数据结构的代码) 便于在不改动既有数据结构的前提下添加新函数; 面向对象代码便于在不改动既有函数的前提下添加新类
  • 得墨忒定律 (The Law of Demeter) 认为, 模块不应该了解它所操作的对象的内部情形. 对象隐藏数据, 暴露操作 (行为)

3.7 第 7 章 错误处理

7.1 使用异常而非返回码 7.2 先写 Try-Catch-Finally 语句 7.3 使用不可控异常 7.4 给出异常发生的环境说明 7.5 依调用者需要定义异常类 7.6 定义常规流程 7.7 别返回 null 值 7.8 别传递 null 值 7.9 小结 7.10 文献

  • Java 程序员们一直在争论可控异常 (checked exception) 的利弊
  • 特例模式: (special case pattern): 创建一个类或者配置一个对象, 用来处理特例; 你来处理特例, 客户代码就不用应付异常行为了, 异常行为被封装到特例对象中
  • 如果将错误处理隔离看待, 独立于主要逻辑之外, 就能写出强固而整洁的代码. 做到这一点, 我们就能 单独处理它, 大大提升了代码的可维护性
  • 别返回 null 值, 别传递 null 值

3.8 第 8 章 边界

8.1 使用第三方代码 8.2 浏览和学习边界 8.3 学习 log4j 8.4 学习性测试的好处不只是免费 8.5 使用尚不存在的代码 8.6 整洁的边界 8.7 文献

  • 编写测试来遍览和理解第三方代码, Jim Newkirk 把这叫做**学习性测试 (learning tests)**
  • 学习测试毫无成本; 而且, 还可以用来检验第三方库的行为是否改变
  • 使用尚不存在的代码: 契约测试
  • 边界上的代码需要清晰的分割和定义了期望的测试

3.9 第 9 章 单元测试

9.1 TDD 三定律 9.2 保持测试整洁 9.3 整洁的测试 9.3.1 面向特定领域的测试语言 9.3.2 双重标准 9.4 每个测试一个断言 9.5 F.I.R.S.T. 9.6 小结 9.7 文献

  • 我会讲代码和操作系统隔离开, 而不是直接调用标准的时间功能; 我会伪造一套及时函数, 这样就能全面控制时间;
  • TDD 三定律:
    • fail 测试前, 不写生产代码
    • 编写恰能 fail 的测试 (red)
    • 先生产仅能通过测试的代码 (green)
  • 单元测试: 可扩展, 可维护, 可复用
  • 有了测试, 你才能毫无顾虑地改进架构和设计
  • 整洁的测试有三个要素: 可读性, 可读性, 可读性 (表现力很重要)
  • 不要有双重标准: 有些事情在生产环境不能做, 在测试环境可以做??
  • given-when-then, build-operate-check
  • 每个测试有一个概念 (focus 的点)
  • FIRST (快独可自及): fast, independent, repeatable, self-validating, timely

3.10 第 10 章 类

10.1 类的组织 10.2 类应该短小 10.2.1 单一权责原则 10.2.2 内聚 10.2.3 保持内聚性就会得到许多短小的类 10.3 为了修改而组织 10.4 文献

  • 保持内聚性就会得到许多短小的类

3.11 第 11 章 系统

11.1 如何建造一个城市 11.2 将系统的构造与使用分开 11.2.1 分解 main 11.2.2 工厂 11.2.3 依赖注入 11.3 扩容 11.4 Java 代理 11.5 纯 Java AOP 框架 11.6 AspectJ 的方面 11.7 测试驱动系统架构 11.8 优化决策 11.9 明智使用添加了可论证价值的标准 11.10 系统需要领域特定语言 11.11 小结 11.12 文献

  • 本章讨论如何在较高的抽象层级, 系统层级上保持整洁
  • test double, mock objects
  • *"一开始就做对系统" 纯属神话; 我们应该只去实现今天的用户故事, 然后重构, 明天再扩展系统, 实现新用户故事*

3.12 第 12 章 迭进

12.1 通过迭进设计达到整洁目的 12.2 简单设计规则 1:运行所有测试 12.3 简单设计规则 2~4:重构 12.4 不可重复 12.5 表达力 12.6 尽可能少的类和方法 12.7 小结 12.8 文献

  • OO 的五大原则是指 SRP、OCP、LSP、DIP、ISP。
    • SRP: Single Responsibility Principle 单一职责原则
    • OCP: 开闭原则,很简单,一句话:“Closed for Modification; Open for Extension”
    • LSP: Liskov Substitution Principle, 里氏替换原则: “Inheritance should ensure that any property proved about supertype objects also holds for subtype objects.”——“继承必须确保超类所拥有的性质在子类中仍然成立。”
    • DIP: Dependence Inversion Principle, 依赖倒置, 要依赖于抽象,不要依赖于具体. 抽象不应当依赖于细节;细节应当依赖于抽象;要针对接口编程,不针对实现编程。
    • ISP: Interface Segregation Principle, 接口隔离原则. 使用多个专门的接口比使用单一的总接口要好
  • 不过, 做到有表达力的最重要方式却是尝试
  • 太多时候, 我们能请写出工作的代码, 就转移到下一个问题上, 没有下功夫调整代码, 让后来者易于阅读

3.13 第 13 章 并发编程

13.1 为什么要并发 13.2 挑战 13.3 并发防御原则 13.3.1 单一权责原则 13.3.2 推论:限制数据作用域 13.3.3 推论:使用数据复本 13.3.4 推论:线程应尽可能地独立 13.4 了解 Java 库 13.5 了解执行模型 13.5.1 生产者 - 消费者模型 13.5.2 读者 - 作者模型 13.5.3 宴席哲学家 13.6 警惕同步方法之间的依赖 13.7 保持同步区域微小 13.8 很难编写正确的关闭代码 13.9 测试线程代码 13.9.1 将伪失败看作可能的线程问题 13.9.2 先使非线程代码可工作 13.9.3 编写可插拔的线程代码 13.9.4 编写可调整的线程代码 13.9.5 运行多于处理器数量的线程 13.9.6 在不同平台上运行 13.9.7 装置试错代码 13.9.8 硬编码 13.9.9 自动化 13.10 小结 13.11 文献

  • 对象是过程的抽象, 线程是调度的抽象
  • 建议: 简读可用的类: reentrant lock, semaphore, count down latch
  • java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.lock
  • 生产者-消费者模型
  • 读者-作者模型
  • 宴席哲学家模型

3.13.1 ReentrantLock (reetrant 折返)

用 synchronied 来做同步, 可以获取一个 lock, 但要等另一个 lock, 结果死锁:

public void transferMoneyWithSync(Account fromAccount, Account toAccount,
				  float amount) throws InsufficientAmountException {
    synchronized (fromAccount) {
	// acquired lock on fromAccount Object
	synchronized (toAccount) {
	    // acquired lock on toAccount Object
	    if (amount > fromAccount.getCurrentAmount()) {
		throw new InsufficientAmountException(
						      "Insufficient Balance");
	    } else {
		fromAccount.debit(amount);
		toAccount.credit(amount);
	    }
	}
    }
}

用 ReetrantLock 做 "timed and polled lock-acquisition":

public boolean transferMoneyWithTryLock(Account fromAccount,
					Account toAccount, float amount) throws InsufficientAmountException, InterruptedException {

    // we are defining a stopTime
    long stopTime = System.nanoTime() + 5000;
    while (true) {
	if (fromAccount.lock.tryLock()) {
	    try {
		if (toAccount.lock.tryLock()) {
		    try {
			if (amount > fromAccount.getCurrentAmount()) {
			    throw new InsufficientAmountException(
								  "Insufficient Balance");
			} else {
			    fromAccount.debit(amount);
			    toAccount.credit(amount);
			}

		    } finally {
			toAccount.lock.unlock();
		    }
		}

	    } finally {
		fromAccount.lock.unlock();
	    }
	}
	if(System.nanoTime() < stopTime)
	    return false;

	Thread.sleep(100);
    }//while
}

if another thread comes and interrupts the current thread, the lock should release and perform the exit or shut down operations to cancel the current task. 可以取消:

public boolean sendOnSharedLine(String message) throws InterruptedException {
    lock.lockInterruptibly();
    try{
	return cancellableSendOnSharedLine(message);
    } finally {
	lock.unlock();
    }
}

private boolean cancellableSendOnSharedLine(String message) {
    .......
}

https://dzone.com/articles/what-are-reentrant-locks

3.14 第 14 章 逐步改进

14.1 Args 的实现 14.2 Args:草稿 14.2.1 所以我暂停了 14.2.2 渐进 14.3 字符串参数 14.4 小结

  • 写出好作文是一个逐步改进的过程
  • 毁坏程序的最好方法之一就是以改进知名大动其结构
  • 用测试来保护
  • marshalling (编组)
  • 满足仅仅让代码能工作的代码的程序员不够专业

3.15 第 15 章 JUnit 内幕

15.1 JUnit 框架 15.2 小结

  • 对于两个身处狭窄空间的奇客, 还有什么比拿出笔记本电脑开始编码来的更自然呢?

3.16 第 16 章 重构 SerialDate

16.1 首先,让它能工作 16.2 让它做对 16.3 小结 16.4 文献

  • 作者编写这个类, 是为了**响应我自己也常常感到的痛苦**
  • 我们再一次遵从了童子军军规, 我们迁入的代码, 要比迁出的时候整洁了一点

3.17 第 17 章 味道与启发

17.1 注释 17.2 环境 17.3 函数 17.4 一般性问题 17.5 Java 17.6 名称 17.7 测试 17.8 小结 17.9 文献

  • 删代码? 没人会删除它, 因为大家都假设别人需要它或是有进一步计划
  • 遵循 *"最小惊异原则" (the principle of least suprise) / the principle of least astonishment*
  • 每次看到重复代码, 都代表遗漏了抽象
  • 死代码
  • 类的方法只应对其所属类中的变量和函数感兴趣, 不该垂青其他类中的变量和函数
  • 使用多个函数, 通常优于向单个函数传递某些代码来选择函数行为
  • 让程序可读的最有利方法之一就是将计算过程打散成用有意义的单词命名的变量中放置的中间值
  • 知道解决方案是正确的, 或者这种知识和理解的最好途径, 往往是重构函数, 得到某种整洁而足具表达力, 清楚呈现如何工作的东西
  • 2*pi*r 这个 2 不是 magic number (不能描述自我描述的符号), 提取出去也没啥卵用
  • 常常有必要使用时序耦合, 但你不应该掩蔽它. 排列函数参数, 好让他们被调用的次序显而易见
  • "编写害羞代码", 确保模块只了解其直接写作者, 而不了解整个系统的游览图 (panorama??)
  • 不要太快取名, 确认名称具有描述性; 记住, 事物的意义随着软件的演化而变化
  • 名称的作用范围越大, 名称就该越长, 越准确

3.18 附录 A 并发编程 II

A.1 客户端 / 服务器的例子 A.1.1 服务器 A.1.2 添加线程代码 A.1.3 观察服务器端 A.1.4 小结 A.2 执行的可能路径 A.2.1 路径数量 A.2.2 深入挖掘 A.2.3 小结 A.3 了解类库 A.3.1 Executor 框架 A.3.2 非锁定的解决方案 A.3.3 非线程安全类 A.4 方法之间的依赖可能破坏并发代码 A.4.1 容忍错误 A.4.2 基于客户代码的锁定 A.4.3 基于服务端的锁定 A.5 提升吞吐量 A.5.1 单线程条件下的吞吐量 A.5.2 多线程条件下的吞吐量 A.6 死锁 A.6.1 互斥 A.6.2 上锁及等待 A.6.3 无抢先机制 A.6.4 循环等待 A.6.5 不互斥 A.6.6 不上锁及等待 A.6.7 满足抢先机制 A.6.8 不做循环等待 A.7 测试多线程代码 A.8 测试线程代码的工具支持 A.9 小结 A.10 教程:完整代码范例 A.10.1 客户端 / 服务器非线程代码 A.10.2 使用线程的客户端 / 服务器代码

3.19 附录 B org.jfree.date.SerialDate

3.20 结束语

  • test obsessed

Author: TANG ZhiXiong

Created: 2018-09-05 Wed 10:55

Emacs 25.3.2 (Org mode 8.2.10)

Validate