禁止转载
<< Clean Code >>
概述
1. 本书 观念
代码质量 与其 整洁度 成正比
2. 好/坏细节
神在细节之处。细节中自有天地
不好的细节 会将 大局的魅力尽毁
3. 5S: 本质是 精益(Lean)
整理
sort: 物皆有其位
用 恰当 命令 等手段, 找到其位置
整顿
systematize: 物尽归其位
锃(zeng)亮
清除 无用的东西
标准化
保持一贯的代码风格和实践手段
简单地保持一致缩进风格就能创造价值
自律
贯彻规程
Chapter1 整洁代码
1. 代码永存
因为需求往往是 模糊的, 机器无法透彻地理解人们
2. 糟糕代码 可能 毁掉公司
Latar equals never ( 稍后等于永不 )
3. 混乱的代价
华丽新设计: 未必就好
/ 花样
/
越来越乱 —— 程序员应有的态度: 准确地告知 经理 代码中的混乱, 并争取到改进的时间预算
\
\
代码写的快的唯一方法: 始终尽可能保持代码整洁
4. 什么是 整洁代码
C++ 之父
优雅 高效
代码逻辑 直截了当 -> 叫 缺陷难以隐藏
依赖 减少 -> 便于维护
性能 调至最优 -> 省得 引诱 别人 乱优化, 搞出一堆混乱来
错误处理 分层战略
只做好一件事
Ron
简单代码
能通过所有测试
没重复
体现系统全部设计理念
包含尽量少的 实体 —— 类 函数 等手段
1) 代码重复 -> 去重
2) 有意义的命名
3) 对象 / 方法 职责太多 -> 切分 / 抽取 (Extract Method)
4) 程序由 极为相似的元素构成
在集合中查找某物 -> 封装到 更抽象的 类/方法
5. 使代码易度 实际也 使之易写
6. 修改代码 之后要比之前 干净
Chapter2 有意义的命名
良好的描述技巧
/
取好名字 的 难处
\
共有文化背景
1. 名副其实
以 功能命名
有意义 胜于 模糊
2. 避免误导
别用 accountList 表示一组账户, 除非 它真是 `List 类型`
3. 有意义的区分
1) 数字: 通常没有意义
2) 废话都是冗余
Variable/Table 不该出现在 变量/表 名 中
4. 名称 应该 能读出来
尽量 用完整单词组合, 除非 单词的缩写 能读出来
modificationTimestamp
recordId
5. 名称 应该 易搜索
不要太短 -> 搜索 匹配太多
名称长短 应与其 作用域大小 对应
6. 避免编码
1) 避免把 类型/作用域 编进名称
2) 成员前缀 m_ -> 多余
读代码时, 应学会 无视 前/后缀, 只看到 name 中 有意义的部分
3) 接口与实现
让 其中之一 带 前/后 缀 是个好主意
ShapeFactory (接口类名) / ShapeFactoryImp (实现类名)
7. 类名 应该是 名词
Customer AddressParser
8. 方法名 应该是 动词
save get/set/is
9. 每个概念 对应 1个词
给每个 抽象概念 选1个词, 并 一以贯之
3 个 类 中 都表示 获取的方法 分别叫 fetch/retrieve/get -> 3 个类中 用同一个词 才好, 如 get
10. 别用双关语
要遵循 一词一义
多个类 均有 add 方法, 只要其 para list + return value 语义等价就好
但 将 elem 放到 collection 中, add 显然不合适, 应该用 append/insert
解决方案领域
/
11. 分离
\
问题领域
12. 添加 有意义的 语境
给 name 加 前/后缀
/
很少有名称能 自我说明
\
用 良好命名的 类/函数/namespace 放 name, 给读者提供 语境
addrFirstName addrStreet addrCity
/ |
firstName street city |
\ |
Address
- firstName
- street
- city
Chapter3 函数
语言 用来描述系统。函数 是 语言的 动词, 类 是 名词
大师级程序员 `把系统当作 故事 来讲`, 而不是当作程序来写
1. 短小
20 行封顶最好
2. 只做一件事
函数应该 做 一件事。做好 这件事。只做 这一件事
怎样算只做一件事?
3种描述
函数 只做了 该函数名下 `同一抽象层 上的步骤`
该函数 不能再拆出一个函数, 而拆出的函数 不仅仅是 单纯地 重新诠释其实现
函数无法被合理地切分为 多个区段
3. 每个函数 一个抽象层
自顶向下 读代码: 向下规则
让 每个函数 后面都跟着 下一抽象层的函数
=> 查 func list 时, 就能循抽象层向下阅读了
程序就像一系列 `TO 起头的段落`, 每一段都 `描述 当前抽象层`,
并 `引用 下一抽象层` 的后续 `TO 起头段落`
// eg
要 容纳 设置和分拆 步骤, 就先 `容纳设置步骤`, 然后 纳入测试页面内容, 再 纳入分拆步骤
要 `容纳设置步骤`, 如果是 套件, 就 纳入 `套件设置步骤`, 然后 ...
要 纳入 `套件设置步骤`, 先..., 再 ...
String func()
{
buffer.append("\n"); // 低 抽象层
String pagePathName = PathParser.render(pagePath); // 中 抽象层
return pageData.getHtml(); // 高 抽象层
}
class SetupTeardownIncluder
{
private:
PageData pageData;
bool isSuite;
//...
publid:
static String render(PageData pageData)
{
return render(pageData, false);
}
static String render(PageData pageData, bool isSuite)
{
return new SetupTeardownIncluder(pageData).render(isSuite);
}
private:
SetupTeardownIncluder(PageData pageData)
{
this->pageData = pageData;
// ...
}
String render(bool isSuite)
{
this.isSuite = isSuite;
if( isTestPage() )
includeSetupAndTeardownPages();
return pageData.getHtml();
}
void includeSetupAndTeardownPages()
{
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
void includeSetupPages()
{
if(isSuite)
includeSuiteSetupPage(); // `引用 下一抽象层` 的后续 `TO 起头段落`
includeSetupPage();
}
}
4. switch 语句
switch 语句 天生要做 N 件事 => 违反 单一职责 + 开放封闭 原则
|
| 可部分避开 switch, 最多只在 `创建多态对象` 时出现
|
| (1) switch 语句 -> 埋到 Factory Method 底下, 不让 client 看到
| Factory Method 用 switch `创建多态对象`
|
| (2) 原来使用 switch 的 func -> 由 该类 的 vf 多态 地 派遣到 Derived 类 去具体实现
|/
Factory Method + 多态
例子见 << 重构 >> 中 Replace Type Code with SubClass
5. 使用 描述性 (函数)名称
长而具有描述性的名称 比 短而令人费解的名称好, 比 描述性长注释好
6. 函数参数
参数 与 函数名 处于 不同的抽象层
参数应越少越好
测试 覆盖所有可能值 的组合 让人生畏
1) 一元函数
2) 二元函数
2 个参数 是 单个参数的 有序组成部分
3) 三元参数
忽略不必要的
4) 标识参数/布尔值参数 -> 消除
5) 参数对象
7. 无 副作用
(1) check...() 中 却 出现 Session.initialize()
-> check...() 有副作用
-> 若不看内部, 以为只有 check 动作
-> check...AndInitializeSession() 更好
(2) 输出参数 在 OO 中 需求基本消失, 因为 this 也有 输出的意味在内
public void appendFooter(StringBuffer report)
|
| 最好换成 OO 调用形式
|/
report.appendFooter();
8. 分隔 set/get
9. 用 异常 代替 返回错误码
Chapter4 注释
好注释: 意义重大
/
1. /
\ 乱七八糟 的 注释 -> 搞乱代码
\ /
坏注释
\
陈旧、提供错误信息的注释 -> 破坏性
注释 可能 与代码 分隔 -> 越来越不准确
2. 注释 不能美化 糟糕的代码 -> 写注释 不如 重构代码
3. 注释 很多简单到 用一个 描述函数 替换
1) 法律信息
/
|—— 2) 对某个决定背后意图的解释
4. 好注释 |
|—— 3) 阐明 晦涩
|—— 4) Warning
\
5) TODO
程序员认为应该做, 但由于某些原因还没做
其余基本都是坏注释
Chapter5 格式
1. 格式的目的
好的 `代码风格 和 可读性` 利于代码 `可维护性 和 扩展性`
即使 代码不复存在, 你的 `风格 和 律条` 仍存活
1) 向报纸学习
/ 隔开 概念: 函数间 等
| /
/ 用
|—— 2) 空白行
\ 不用
| \
靠近的代码: 紧密关联
2. 垂直格式 |
|—— 3) 变量声明: 尽可能 靠近其 使用位置
\
4) 相关(调用关系) 函数: 放到一起, caller 放 上面
隔开 弱关联事物
/
/ 用
1) 空格
/ \ 不用
| \
连接 紧密相关的事物
3. 横向格式 |
|—— 2) 水平对齐
Socket socket;
| InputStream input;
\
3) 缩进
4. 1个团队 1套 代码格式管理规则, 然后 贯彻之
Chapter6 对象 和 数据结构
mem 设为 private: 不想让其他人 依赖 它
1. 数据抽象
1) 隐藏实现 并非只是在 `变量之上 放一个 函数层` 那么简单
隐藏实现 关乎抽象!
类 的意义: 曝露 抽象接口, 使 Client 无需了解 `数据结构 的实现` 就能操作 `数据本体`
2) 我们不愿 `曝露 数据细节`, 更愿意 `以 抽象形态 表述数据`
要以 最好的方式 呈现 对象包含的数据
(3) eg
1) 你不知道 `相应的 实现` 是在 笛卡尔坐标系 还是 级坐标系, 还是 其他。
/ 但 `该 接口` 却 呈现了一种 `数据结构 ( 即 Point )`
抽象 Point 的漂亮
\
2) 接口 固定了一套 `存取策略`:
可 `单独读取` 某坐标, 但必须通过 `一次原子操作` 设 所有坐标
// 具象 Point
class Point
{
public:
double x;
double y;
};
// 抽象 Point
interface Point
{
public:
double getX();
double getY();
void setCartesian(double x, double y); // Cartesian 笛卡尔坐标
double getR();
double getTheta();
void setPolar(double r, double theta); // 极坐标
};
2. 数据结构、对象 的 反对称性
数据结构 与 对象 本质 对立
曝露 数据
/
数据结构
\
没有提供 有意义的 函数
隐藏 数据 于 抽象 之后
/
对象
\
曝露 操作数据的 函数
=>
—————————————————————————————————————————————————————————————————————————————————
| 便于
—————————————————————————————————————————————————————————————————————————————————
过程式 (使用 数据结构) 代码 | 在 不改动 `既有 数据结构` 的前提下 添加 `新 函数`
OO 代码 | 在 不改动 `既有 函数` 的前提下 添加 `新 类`
—————————————————————————————————————————————————————————————————————————————————
|
| 反过来说
|
—————————————————————————————————————————————————————————————————————————————————
| 难以
—————————————————————————————————————————————————————————————————————————————————
过程式 (使用 数据结构) 代码 | 添加 `新 数据结构`, 因为必须修改 `所有 函数`
OO 代码 | 添加 `新 函数`, 因为必须修改 `所有 类`
—————————————————————————————————————————————————————————————————————————————————
=>
对 OO 较难的事, 对 过程式 却较容易, 反之亦然
新数据 类型 时 -> 对象 和 OO 更合适
/
系统 需要添
\
新 函数 时 -> 过程式 更合适
一切都是对象 只是1个传说
eg
// 过程式
// 1) 给 Geometry 加 primeter() 函数, 各 形状类 不变
// 2) 加 一个 新形状类 -> 改 Geometry 中 所有函数
class Square
{
public:
Point topLeft;
double side;
};
class Circle
{
public:
Point center;
public double radius;
};
class Geometry
{
public:
double PI = 3.14;
double area(Object shape)
{
if( typeof(shape) == Square )
{
Square s = (Square)shape;
return s.side * s.side;
}
else if( typeof(shape) == Circle )
{
Circle c = (Circle)shape;
return PI * c.side * c.side;
}
}
};
// 多态式
class Square: public Shape
{
private:
Point topLeft;
double side;
public:
double area()
{
return side * side();
}
};
class Circle: public Shape
{
private:
Point center;
double radius;
public:
double PI = 3.14;
double area()
{
return PI * radius * radius;
}
};
3. Demeter 定律
模块 不应了解它所操作的 对象 的 内部情形
对象 隐藏数据, 曝露 操作
类 A 的 方法 f 只应该调用 以下对象 的 方法
1) A
2) f 创建的对象
3) 作 参数 传给 f 的 对象
4) A 的 实体变量 持有的 对象
方法 `不应调用` 由任何函数 `返回的对象 的 方法`
即 只跟朋友谈话, 不跟陌生人谈话
违反 Demeter 定律的例子
// ScratchDir: 暂存目录
String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
(1) 连串调用 时 坏风格, 应 切分
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
String outputDir = scratchDir.getAbsolutePath();
上述代码 是否违反 Demeter 定律?
若 ctxt / Options / ScratchDir 是 对象/数据结构 -> 违反 / 不违反
(2) 混杂
无论初衷是什么, public set()/get() 函数 都把 private memData 公开化,
诱导 外部函数 以 过程式程序 使用 数据结构的方式 使用 这些 memData
(3) 隐藏结构
ctxt.getAbsolutePathOfScratchDirOption();
或
ctxt.getgetScratchDirOption().getAbsolutePath(); // 假定 getgetScratchDirOption() 返回的是 数据结构 而非 对象
感觉都不太好
如果 ctxt 是个 `对象`, 就应该 要求它 `做点什么`, 不该要求它 给出内部情形
哪为何要 得到 临时目录的绝对路径 ? 看同模块中 其 用途即可
String outFile = outputDir + "/" + className.replace('.', '/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);
不同层级的细节混杂
初衷: 为了 创建指定名称的临时文件流
=> 直接让 ctxt 做这件事
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
看起来像 对象做的事了 ! ctxt 隐藏了其 内部结构, 防止 当前函数 因 浏览 它不该知道的对象 而 违法 Demeter 定律
4. 数据传送对象 DTO
(1) DTO
只有 public 变量 + 没有 函数 -> 最精炼的 数据结构
应用
与 数据库 通信、解析 套接字传递的消息
(2) Active Record: 特殊的 DTO
public(可 bean /豆式 访问 的) 变量 + save() / finc() 等 函数
不应把 业务规则方法 放进 Active Record
-> 解决
把 Active Record 当作 数据结构, 创建 包含 业务规则、隐藏 内部实际 (可能就是 Active Record 实体 ) 的独立对象
(3) "bean" 结构
private 变量 + set()/get() 函数
Chapter7 错误处理
错误发生时, 确保代码照常工作
1. 使用 异常 而非 返回错误码
好处
1) 代码更整洁
2) 隔离 业务处理流程 和 错误处理
public class DeviceController
{
public void sendShutDown()
{
DeviceHandle handle = getHandle(DEV1);
if(handle != DeviceHandle.INVALID)
{
DeviceRecord record = getDeviceRecord(handle);
if(record.getStatus() != DEVICE_SUSPEND)
{
pauseDevice(handle);
....
}
else
{
logger.log("Device suspend.")
}
}
else
{
logger.log("Invalid handle for: " + DEV1.toString() );
}
}
}
|
| 依 caller 需要 定义 异常类 DeviceShutDownError
|/
public class DeviceController
{
public void sendShutDown()
{
try
{
tryToShutDown();
}
catch(DeviceShutDownError e)
{
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError
{
DeviceHandle handle = getHandle(DEV1);
DeviceRecord record = getDeviceRecord(handle);
pauseDevice(handle);
...
}
private DeviceHandle getHandle(DeviceID id)
{
...
throw new DeviceShutDownError("Invalid handle for: " + id.toString() );
}
}
2. 先写 Try-Catch-Finally
异常的妙处: 定义了一个范围
try/catch 代码块 像事物/将程序维持在一种持续状态
3. 别返回 null 值
4. 别传递 null 值
Chapter8 边界
将 外来代码 整合进 自己的代码
1. 使用 第三方代码
第三方代码: 普适性
\
=> 系统边界 出问题
/
使用者 : 特定需求
包容 Sensor 类 的 Map 映射图
Map sensors = new HashMap();
Sensor s = (Sensor)sensors.get(sensorId);
|
| 泛型
|/
Map<Sensor> sensors = new HashMap();
...
Sensor s = sensors.get(sensorId);
|
| Map 接口修改时, 很多地方要改
|
|/
public class Sensors
{
private Map sensors = new HashMap();
public Sensor getById(string id)
{
return (Sensor)sensors.get(id);
}
}
2. 浏览和学习边界
学习性测试
编写测试 来 浏览 和 理解 第三方代码
3. 学习 log4j
文档 无需看太久 -> 编写 测试用例 -> 运行 -> 出错 -> 再多读文档 / Google / 百度查解决方案
|\ |
| |
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
封装自己的 日志类
将 程序 业务处理部分 与 日志类 接口隔离
4. 使用 尚不存在的代码
定义自己的使用的 接口(在我们控制下, 有助于保持 Client 代码 更可读 ) + Adapter 模式 + FakeImplement
————————————————————————— \ —————————————————————————————
| CommunicationsController| ——————— | << interface >> |
| | / | Transmitter |
————————————————————————— |—————————————————————————————|
|+ transmit(frequency, stream)|
—————————————————————————————
/_\
|
_ _ _ _|_ _ _ _ _
| |
| | ——————————————
———————————————— ———————————————————— \ | << future >>
|FakeTransmitter | | TransmitterAdapter |——————————— | Transmitter |
| | | | / ——————————————
———————————————— ———————————————————— | API |
——————————————
5. 整洁边界
代码中 少数几处引用 第三方 边界接口
Chapter9 单元测试
测试驱动开发
测试代码 和 生产代码 一样重要
自动化单元测试程序
整洁测试: 可读性好、明确、简洁
每个测试 一个概念
FIRST
F fast
I Independent
R Repeatable
S Self-Validating
T Timely
Chapter10 类
1. 类的组织
变量列表
static 常量
private memData
public func
|
| 调
|/
private 工具函数
2. 类应该短小
函数 大小: 以 行数 衡量
类 大小 : 以 职责 衡量
类名 应 描述其 职责
无法为类命名为 精确的名称 时, 类 就太长了
1) SRP: 单一职责 原则
类 或 模块 应有且只有 一条加以修改的理由
2) 内聚
高内聚: 类中 变量和方法 相互依赖 合成一个逻辑整体
3) 保持 高内聚 就会得到许多 短小的类
大函数 拆分为 小函数: 原本 local var 要作 参数 传递
-> 若 这些 local var 提升为 类的 memData, 则 无需传递
-> 类 的 内聚性 降低: 堆积 越来越多 只为 少量函数 共享 而存在的 memData
-> 让 这些 函数 拥有自己的类: 拆分类 (按职责拆分)
4) DIP 依赖倒置 原则
类 应该 依赖于 抽象, 而不是 实现细节
最佳实践
抽象类 只呈现 概念
具体类 包含 实现细节
让 Client 类 依赖 抽象类/接口 -> 以 解耦 Client 与 Implement
Chapter11 系统
1. 将系统的 构造 与 使用 分开
软件系统 应将 启始过程 与 之后的运行时逻辑 分离开
(1) 延迟 初始化/赋值
真正用到对象前, 无需操心构造, 启动时间更短
public Service getService()
{
if(service == null)
service = new MyServiceImp(...); // 构造 与 运行时逻辑 混杂 -> 违反 单一职责
return service;
}
仅出现1次的 延迟初始化 还不算严重问题, 但 应用程序中往往有许多 类似情况
(2) 将 构造 分解到 main 模块
main 模块 创建系统所需对象, 传递给 应用程序, 应用程序 只管使用
——————————————— 2: run \ ————————————————
| main/构造过程 | —————————/——————— | application |
——————————————— / / ————————————————
| | |
| | |
| 1: build | |
| | |
\|/ | \|/
—————————————— 1.1: construct \ ————————————————————
| Builder | ———————|————————— | Configured Object |
—————————————— | / ————————————————————
依赖箭头 从 main 向外, 表示 应用程序 对 main/构造过程 一无所知
(3) 工厂
让 应用程序 负责 确定 何时创建对象
——————————————— 2: run \ ——————————————————————
| main/构造过程 | —————————/——————— | OrderProcessing |
——————————————— / / ——————————————————————
| | | \
| | | \
| 1: build | | \
| | | \
\|/ | \|/ \
————————————————————— | 1.1: construct —————————————————— \
| LineItemFactoiryImp | —|———————————————|\ | LineItemFactoiry | \
————————————————————— | |/ |——————————————————— \
| | + makeLineItem | \
| ——————————————————— \
| \/
| \ ————————————
|—————————————————————————————————————————————————————— | LineItem |
/ ————————————
依赖都从 main 指向 OrderProcessing 应用程序, 表示 应用程序 与 如何构建 LineItem 的细节 分离
构建能力 由 在 main 这一边的 LineItemFactoiryImp 持有
但 应用程序能 完全控制 LineItem 何时构建
(4) 依赖注入 控制反转
Chapter12 迭进
1. Kent Beck 简单设计 4 原则
(1) 运行所有测试
(2) 消除重复
(3) 保证表达力
(4) 尽可能减少 类 和 方法的数量
类和方法 短小 + 整个系统短小
避免教条主义
1) 为 每个类 创建 接口
2) 字段 和 行为 必须切分到 数据类 和 行为类
上述优先级 依次降低
网友评论