The Boy Scout Rule
“让营地比你来时更干净。”
这是本书最核心的建议。每次签入代码时,都要让代码比签出时更整洁一点。不需要大改,改好一个变量名、拆分一个过长的函数即可防止代码腐烂。
生产力归零
随着代码混乱度的增加,团队生产力会逐渐趋近于零。增加人手只会制造更多混乱(因为新人不熟悉系统)。
唯一出路: 保持代码整洁。
程序员的艺术
整洁代码不仅仅是原则,更是一种像画家一样的“代码感”。
它能让你看到代码的坏味道,并本能地知道如何通过一系列微小的重构将其转化为整洁的代码。
Intention-Revealing
变量、函数或类的名称应该回答所有的大问题:
它为什么存在?它做什么?怎么用?
如果名称需要注释来补充,那这个名称就不及格。
明确而非聪明
不应当让读者在脑中把你的名称翻译成他们熟知的名称。
例如:单字母变量名(除循环中的i,j,k外)通常是糟糕的。清晰是王道。
Do One Thing
函数应该做一件事。做好这件事。只做这件事。
如果一个函数内部包含多个抽象层级(如既有HTML生成又有路径解析),它就是做了多件事。
One Level of Abstraction
函数中的所有语句都要在同一抽象层级上。
降级法则: 代码应像读报纸文章一样,自顶向下。每个函数后面都紧跟下一层级的函数。
越少越好
最理想的参数数量是0,其次是1,再次是2。
标识参数(Flag Argument)是丑陋的: 传入布尔值意味着函数至少做两件事。应该拆分为两个函数。
代码在变,注释不常变
程序员不能坚持维护注释。不准确的注释比没注释坏得多。
真理只存在于代码中。别给糟糕的代码加注释——重写它。
Clean Up!
- 注释掉的代码: 绝对禁止!
- 日志式注释: 交给Git处理。
- 循规蹈矩的注释: 不要给每个getter/setter写注释。
- 右括号后的注释: 说明函数太长了。
Vertical Formatting
源文件应像报纸文章一样。
名称应当简单且一目了然。最顶部给出高层次概念和算法。细节往下渐次展开。
关系越紧密,距离越近
变量声明: 应尽可能靠近其使用位置。
相关函数: 若A调用B,B应紧随A之后(自顶向下),形成自然的阅读流。
Data/Object Anti-Symmetry
对象: 隐藏数据,暴露行为。便于添加新对象类型,难以添加新行为(需修改所有类)。
数据结构: 暴露数据,没有行为。便于添加新行为(只需改函数),难以添加新数据类型。
Law of Demeter
模块不应了解它所操作对象的内部情形。
火车失事(Train Wrecks): ctxt.getOptions().getScratchDir()...
这是典型的反例。如果是对象,应告诉它做什么,而不是问它内部有什么。
我们很少控制系统中的全部软件。如何将第三方代码(库、框架)整洁地整合进我们的系统?
Learning Tests
不要直接在生产代码中尝试第三方API。
编写测试来验证我们对第三方API的理解。这不仅免费,还能在第三方库升级时自动检测兼容性。
Adapter Pattern
不要让第三方代码的细节泄露到系统中。使用适配器(Adapter)封装边界接口,如 `Map`。
这样当第三方代码变动时,只需修改适配器一处。
—— 边界 (Boundaries) 深度详解 ——
最典型的例子是 Java 的 java.util.Map。它功能强大(clear, get, put...),但这恰恰是它的风险。
- 风险:传递 Map 给接收者,接收者有权调用
clear(),但这可能不是你的本意。 - 解决方案:不要将 Map 在系统中到处传递。将其封装在一个类(如
Sensors)中,只暴露业务需要的方法(如getById)。
Adapter Pattern
当连接的子系统API还没设计好时:
1. 定义一个我们希望拥有的接口。
2. 编写代码调用这个接口。
3. 当真实API可用时,写适配器来桥接。
Encapsulation
避免让太多代码知道第三方库的细节。
通过包装 (Wrapping) 或 适配器 (Adapter),将第三方代码限制在极少的类中。方便更换库或模拟测试。
不要直接阅读冗长的文档,而是通过编写测试来探索。
Log4j 学习性测试过程:
- 尝试打印 "hello" → 发现缺少 Appender。
- 添加 ConsoleAppender → 发现缺少输出流。
- 添加 PatternLayout → 成功。
- 最终成果: 得到一组可工作的初始化代码,并将其封装到自己的 Logger 类中,隔离 Log4j 边界。
本章检查清单 Checklist
- ✅ 依靠你能控制的东西: 如果无法控制第三方代码,也要控制与它的连接方式。
- ✅ 管理边界: 尽量减少引用第三方代码的地方。
- ✅ 使用 Wrapper/Adapter: 让代码读起来像是在使用自己的业务语言。
- ✅ 编写边界测试: 确保第三方库的升级不会破坏系统。
- 在编写不能通过的单元测试前,不可编写生产代码。
- 只编写刚好无法通过的单元测试(编译失败也算)。
- 只编写刚好能通过当前失败测试的生产代码。
Fast: 运行要快。
Independent: 相互独立,无依赖。
Repeatable: 任何环境可重复。
Self-Validating: 输出布尔值。
Timely: 及时编写。
每个测试一个断言: 尽量保持每个测试函数只测试一个概念。
软件系统应将启始过程(对象的构造和依赖关系连接)与运行时的逻辑分离开来。
方法: 将全部构造过程搬迁到 main 模块中,或使用依赖注入 (DI) 容器。
持久化、事务、安全等关注点不应污染业务对象(POJO)。
使用面向切面编程 (AOP) 能够实现模块化,将这些关注点非侵入式地集成到系统中。
系统必须是可验证的。全面测试推动了低耦合、高内聚的设计。
消除重复是重构的核心。重复代表了额外的工作、额外的风险和不必要的复杂性。
代码应清晰表达作者的想法。使用好名字、小函数、标准设计模式名称。
在遵循前三条原则的基础上,避免过度设计。防止无意义的教条主义。
并发是一种解耦策略:解耦“做什么(What)”和“何时做(When)”。
防御原则
执行模型
警惕陷阱
死锁(Deadlock)、活锁(Livelock)、饥饿(Starvation)。
测试建议: 编写会引起问题的测试,然后在不同的配置和负载下频繁运行(Jiggling策略)。不要忽略一次性失败。
Successive Refinement
核心教训: 这一章通过Args程序的演变展示了,整洁代码不是一蹴而就的。
必须先写出脏代码(但要有测试覆盖!),然后进行一系列微小的、安全的重构步骤,最终得到整洁的代码。
注:第15章分析了JUnit框架内部,第16章重构了SerialDate,均强调了测试覆盖率和持续改进的重要性。
这是作者总结的“代码异味”终极清单,是全书的浓缩。