name: clean-code description: 在编写、审查或重构代码时使用。适用于命名变量或函数、结构化类、处理错误、编写测试,或当代码感觉复杂或难以理解时。基于Robert C. Martin的《Clean Code》。
整洁代码
概述
整洁代码读起来像写得好的散文。每个名称都揭示意图。每个函数都讲述一个故事。每个类都有单一目的。目标不仅仅是让代码工作——而是让代码能被其他人快速理解、安全修改和自信扩展。
“整洁代码总是看起来像是关心它的人写的。” — Michael Feathers
“当每个例程都大致符合你的预期时,你就知道你在编写整洁代码了。” — Ward Cunningham
童子军规则: 让代码比你找到时更整洁。每次提交都应该提高质量,即使只是轻微改进。小的改进会累积。
章节参考
本技能提供概述和快速参考。详细指导和示例,请参见章节文件:
chapters/names.md- 有意义的名称(意图揭示、可搜索、可发音)chapters/functions.md- 函数(小、做一件事、参数少)chapters/comments.md- 注释(为什么避免、什么是可接受的)chapters/objects-and-data.md- 对象和数据结构(德米特法则、DTO)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更糟——默认禁止它 |
# 坏 - 到处都是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:注释掉的代码
一种恶行。立即删除它。
手艺
“编写整洁代码需要纪律性地应用无数小技巧,通过艰苦获得的‘清洁感’。代码感是关键。”
整洁代码不是机械地遵循规则写出来的。它来自驱动纪律的价值观——关心手艺、尊重代码读者,并以专业工作为荣。
你如何编写整洁代码? 初稿笨拙——长函数、嵌套循环、任意名称、重复。你精炼:拆出函数、更改名称、消除重复、缩小方法。没有人从一开始就写出整洁代码。
让软件工作和让它清洁是不同的活动。我们大多数人头脑中空间有限,所以我们首先专注于让代码工作。问题是,太多人认为一旦程序工作,我们就完成了。 我们未能切换到组织和清洁。我们转向下一个问题,而不是回去将过载的类拆分成解耦单元。
不要。回去。清理它。让它比你找到时更好。