名称: 洁净代码 描述: 在编写、审查或重构代码时使用。适用于命名变量或函数、构建类、处理错误、编写测试,或当代码感觉复杂或难以理解时。基于Robert C. Martin的《代码整洁之道》。
代码整洁之道
概述
整洁的代码读起来像优美的散文。每个名称都揭示意图。每个函数都讲述一个故事。每个类都有一个单一目的。目标不仅仅是让代码工作——而是让其他人能快速理解、安全修改和自信扩展的代码。
“整洁的代码看起来总是像是由关心的人编写的。” — Michael Feathers
“当每个例行程序都如你所预期时,你就知道你在处理整洁的代码。” — Ward Cunningham
童子军规则: 离开时让代码比你发现时更整洁。每次提交都应该提高质量,哪怕只是略微。小的改进会累积。
章节参考
本技能提供概览和快速参考。有关详细指导和示例,请参阅章节文件:
chapters/names.md- 有意义的名称(揭示意图、可搜索、可发音)chapters/functions.md- 函数(小、做一件事、参数少)chapters/comments.md- 注释(为何避免、什么是可接受的)chapters/objects-and-data.md- 对象和数据结构(德米特法则、DTOs)chapters/error-handling.md- 错误处理(异常、空值处理、特例模式)chapters/tests.md- 单元测试(TDD、F.I.R.S.T.、整洁测试)chapters/classes.md- 类(SRP、内聚性、OCP、DIP)smells-and-heuristics.md- 完整的代码异味参考(66个异味及解释)
快速参考:名称
名称应揭示意图并可搜索。
| 规则 | 差 | 好 |
|---|---|---|
| 揭示意图 | d |
elapsedTimeInDays |
| 避免误导 | accountList(不是List) |
accounts |
| 区分清晰 | a1, a2 |
source, destination |
| 可发音 | genymdhms |
generationTimestamp |
| 可搜索 | 7 |
MAX_CLASSES_PER_STUDENT |
| 类 = 名词 | Process |
Customer, Account |
| 方法 = 动词 | data |
postPayment(), save() |
避免: 在类名中使用Manager、Processor、Data、Info——它们暗示职责不清晰。
关键见解: 如果需要注释来解释变量是什么,不如重命名它。
快速参考:函数
大小和范围
- 理想: 4-10行,很少超过20行
- 缩进级别: 永远不超过一或两个
- 做一件事 — 如果可以用非重复的名称提取另一个函数,那它做得太多
参数
| 数量 | 指导 |
|---|---|
| 0 | 最佳 |
| 1 | 好 |
| 2 | 可接受 |
| 3+ | 避免—封装到对象中 |
标志参数(布尔值)很丑陋。 它们宣称函数做两件事。拆分它:
# 差
def render(is_suite: bool): ...
# 好
def render_for_suite(): ...
def render_for_single_test(): ...
关键规则
- 命令查询分离: 做某事或回答某事,不要同时做
- 无副作用: 如果
checkPassword()也初始化会话,那它撒谎了 - 偏好异常而非错误码: 分离快乐路径和错误处理
- 提取try/catch块: 错误处理是一件事
快速参考:注释
注释,充其量是必要的恶。注释的正确使用是弥补我们在代码中表达失败的缺陷。
删除这些注释
- 冗余的 — 重申代码所说的
- 日记/变更日志 — 使用git
- 注释掉的代码 — 一种可憎之物,git记得
- 噪音 —
// 默认构造函数,// 递增 i - 闭合大括号 —
} // 结束 if意味着嵌套太多
可接受的注释
- 法律声明
- 意图解释(为什么,而不是什么)
- 后果警告(
// 需要30分钟) - TODO(但要清理它们)
- 澄清外部库行为
规则: 当你感到想注释时,首先尝试重构代码,使注释变得不必要。
快速参考:错误处理
错误处理很重要,但如果它模糊了逻辑,那就是错误的。
| 规则 | 细节 |
|---|---|
| 使用异常而非返回码 | 分离算法和错误处理 |
| 提供上下文 | 包括失败的操作和失败类型 |
| 包装第三方API | 最小化依赖,启用模拟 |
| 使用特例模式 | 返回处理特例的对象(空列表、默认值) |
| 不要返回null | 增加工作,引发NullPointerException |
| 不要传递null | 比返回null更糟—默认禁止 |
# 差 - 到处都是空值检查
if employees is not None:
for e in employees:
total += e.pay
# 好 - 返回空集合而非null
for e in get_employees(): # 如果没有,返回[]
total += e.pay
快速参考:类
单一职责原则(SRP)
一个类应该有一个,且仅有一个,改变的理由。
测试:
- 你能推导出简洁的名称吗?(避免
Manager、Processor、Super) - 你能在25个词内描述它,不使用“如果”、“和”、“或”、“但是”吗?
内聚性
方法应使用类的实例变量。当方法围绕某些变量聚集而不围绕其他变量时,应拆分类。
开闭原则(OCP)
类应对扩展开放,对修改关闭。通过子类化添加新行为,而不是修改现有代码。
依赖倒置原则(DIP)
依赖抽象,而非具体细节。注入依赖以提高可测试性。
# 差 - 没有网络就无法测试
class Portfolio:
def __init__(self):
self.exchange = TokyoStockExchange()
# 好 - 可注入,可测试
class Portfolio:
def __init__(self, exchange: StockExchange):
self.exchange = exchange
快速参考:测试
TDD三定律
- 除非有失败的测试,否则不写生产代码
- 不写超过足以失败的测试
- 不写超过足以通过的生产代码
F.I.R.S.T.原则
- 快速 — 运行迅速,以便经常运行
- 独立 — 不相互依赖
- 可重复 — 在任何环境中结果相同
- 自我验证 — 布尔输出(通过/失败)
- 及时 — 在生产代码之前编写
整洁测试
- 可读性 至关重要
- 使用 构建-操作-检查 模式
- 创建领域特定的测试语言
- 每个测试一个概念(不一定是一个断言)
警告: 测试代码和生产代码同样重要。如果让测试腐烂,代码也会腐烂。
对象 vs 数据结构
| 概念 | 隐藏 | 暴露 | 易于添加… |
|---|---|---|---|
| 对象 | 数据 | 函数 | 新类型 |
| 数据结构 | 无 | 数据 | 新函数 |
万物皆对象的想法是个神话。 有时你只需要简单的数据结构和操作它们的程序。
德米特法则
一个方法应只调用:
- 类自身的方法
- 它创建的对象的方法
- 作为参数传递的对象的方法
- 实例变量持有的对象的方法
不要 调用允许函数返回的对象的方法(火车残骸):
# 差
output_dir = ctxt.get_options().get_scratch_dir().get_absolute_path()
# 好 - 告诉对象做工作
bos = ctxt.create_scratch_file_stream(class_file_name)
最关键的气味
来自第17章的全面列表,这些是最重要的:
G5: 重复
软件中所有罪恶的根源。 每次重复都是一个错过的抽象机会:
- 相同代码 → 提取到函数
- 重复的switch/if-else → 多态性
- 相似算法 → 模板方法或策略模式
G30: 函数应做一件事
如果你能从中提取另一个函数,原函数做得太多。
N1: 选择描述性名称
名称是代码可读性的90%。花时间明智选择。
F1: 参数太多
零最好,然后一、二、三。更多需要理由。
F3: 标志参数
布尔参数意味着函数做两件事。拆分它。
G9: 死代码
未执行的代码。删除它—版本控制记得。
G11: 不一致性
如果你用一种方式做某事,所有类似事情都用相同方式做。
C5: 注释掉的代码
一种可憎之物。立即删除。
技艺
“编写整洁的代码需要纪律性地运用无数小技巧,通过艰苦获得的‘整洁感’来应用。代码感是关键。”
整洁的代码不是机械遵循规则写出的。它来自驱动纪律的价值观—关心技艺,尊重代码读者,并以专业工作为傲。
你如何编写整洁代码? 初稿很笨拙—长函数、嵌套循环、随意名称、重复。你提炼:拆分函数、更改名称、消除重复、缩减方法。没有人一开始就写出整洁代码。
让软件工作和让代码整洁是不同的活动。我们大多数人头脑空间有限,所以先专注于让代码工作。问题是太多人认为程序一旦工作就完成了。 我们未能切换到组织和整洁。我们继续下一个问题,而不是回去将臃肿的类拆分成解耦单元。
不要这样。回去。清理它。留下比发现时更好的状态。