《代码整洁之道》

总体观点
2008 年,罗伯特·马丁开场很狠:代码永远不会消失,需求永远在变,“写出整洁的代码”是程序员的本职工作,不是锦上添花。这本书是他几十年经验浓缩的“代码卫生手册”。
核心观点:代码是写给人看的,顺便让机器执行。读代码的时间是写代码的十倍以上,所以第一要务是“好读”。名字名副其实、函数短到只做一件事、注释能不写就不写、格式像报纸一样有条理。
它把“代码品味”这种玄学变成可执行、可检查的清单。和《重构》常被一起读:一个教你“怎么写出好代码”,一个教你“怎么把坏代码变好”。
核心观点
-
1. 名字要名副其实
好名字让读者不看实现就知道它是干嘛的;烂名字(data、temp、info)等于埋地雷。宁可长一点,也别含糊。
getData() 取什么数据?改成 getUserOrderHistory() 三秒看懂。布尔 flag 改成 isOrderPaid,一眼知道“订单是否已支付”。 -
2. 函数只做一件事,而且要做好
函数要小到只做一件事:能用一句话说清它在干嘛、且这句话里没有“和”“或”,就是一件事。函数一长,读的人脑子里记一堆上下文,累且容易错。
processOrder() 200 行又校验库存又算价格又发短信:拆成 checkStock()、calculatePrice()、sendSms()、saveOrder(),主函数变四行“目录”,想看细节再点进去。 -
3. 注释是失败,别用注释弥补烂代码
好代码不需要注释解释“在做什么”,代码自己说清楚了。需要写注释,往往说明代码没写好。但解释“为什么”这么写的注释还是必要的——那是代码说不出来的。
“i 加 1”是废话注释;“订单 30 分钟未支付自动取消,法务定的规则”有用——它解释的是代码背后的“为什么”。 -
4. 童子军规则:让营地比你来时更干净
离开营地要比来时更干净。每次改代码顺手让它干净一点点:改个烂命名、删段重复、拆个长函数。不用多,每次一点点,质量像复利一样越滚越好。
修 bug 看到拼错的 recieve,顺手改成 receive 再提交。一年下来,无数个小动作让代码库慢慢变干净。 -
5. 别返回null,用异常说清楚
返回 null 是最常见的偷懒:调用方一不留神忘了判空,线上就崩。要么返回有意义的东西(空集合),要么直接抛异常——异常藏不住,不处理立刻告诉你。
查用户订单,用户没订单返回 null,调用方写 order.getTotal() 直接空指针,线上事故。返回空列表,循环一下啥事没有。 -
6. 测试是最好的文档
一套好的单元测试,比任何文档都更能说明“代码是干嘛的”——文档会过时,测试不会,因为测试不过,代码合并不进去。
新同事想知道下单接口有哪些规则,翻三个月没更新的 Wiki,不如直接看测试:满 200 减 20、未支付 30 分钟自动取消……条条测试就是条条活规矩。 -
7. 一个类只有一个“变”的理由
一个类只干一件事,只因为一种原因被修改。又管数据库又管发邮件又管算价格,需求一变就得改,改多就容易出 bug。职责分开,各自安好。
UserService 里有注册、发欢迎邮件、生成报表,改邮件模板一不小心碰坏注册逻辑。拆成三个类,改邮件只动 EmailService,注册稳如泰山。
全书思维导图 · 知识脉络树
全书结构
-
第1章 整洁代码
糟糕代码拖垮研发效率直至团队停摆;推行童子军军规——离开时让代码比你来时更干净整洁。
每次修改遗留代码顺手把周边的变量命名优化并删除废弃注释,一年下来老模块不但没腐化反而越来越清爽。 -
第2章 有意义的命名
命名要名副其实、表达明确意图、避免误导、做有意义的区分并使用读得出来的名称。
用 `daysSinceLastModification` 替代神秘的 `d` 或 `ds`,任何同事看到代码瞬间明白其含义。 -
第3章 函数
函数应该短小、只做一件事、同一抽象层级,参数尽可能少(最好不超过两个),杜绝输出参数与标识参数。
将一个带有 `isVip` 标识布尔参数的庞大函数,拆分为 `renderVipPage()` 和 `renderRegularPage()` 两个干净函数。 -
第4章 注释
注释不能美化糟糕的代码,能自表达的代码胜过冗余解释;只在表达意图或警示暗礁时保留必要注释。
与其在混乱代码旁写三行“这里检查手机号格式”,不如直接提取出一个函数 `isValidPhoneNumber()`。 -
第5章 格式
垂直空白区分逻辑概念段落,横向缩进反映层级结构,代码组织应像报纸新闻一样自上而下顺畅展开。
把公共入口函数放在类顶端,底层被调用的私有辅助方法放在下方,自上而下阅读如读故事。 -
第6章 对象和数据结构
对象隐藏内部数据并暴露抽象行为,数据结构暴露数据无内在行为,严格遵守得墨忒耳律(只与直接朋友通信)。
避免写出 `order.getCustomer().getAddress().getCity()` 这样暴露内部深层结构的火车车厢式链式调用。 -
第7章 错误处理
优先使用不可控异常而非返回繁琐错误码,先写 try-catch-finally 建立防线,杜绝传递 null 和返回 null。
在查询不到数据时返回空的不可变列表 `Collections.emptyList()`,彻底根除调用方到处写 `if (list != null)`。 -
第8章 边界
严密封装第三方库与外部接口调用,编写学习性测试验证外部边界行为,使用适配器隔离不可控依赖。
引入第三方日志或缓存库时用一个简单的门面包装起来,未来更换开源依赖库只需改动门面类。 -
第9章 单元测试
测试代码与生产代码同等重要,遵循 TDD 与 FIRST 原则(快速、独立、可重复、自我验证、及时)。
保证几百个单元测试在三秒内跑完且不依赖外部数据库,工程师每次保存代码都能实时验证逻辑正确性。 -
第10章 类
类应该短小并遵循单一职责原则(SRP),追求高内聚,类结构设计应向抽象接口依赖而非具体实现。
将一个兼顾 SQL 拼装、结果集映射与业务校验的巨型 DAO 类拆分为 SqlBuilder、RowMapper 和 OrderService。 -
第11章 系统
将系统的构造(实例化与装配)与使用分离,使用依赖注入和面向切面(AOP)保持业务逻辑纯粹。
通过 Spring 依赖注入把数据源注入到业务类中,业务类内部无需出现任何 `new DatabaseConnection()` 硬编码。 -
第12章 迭进
遵循简单设计的四条规则:运行所有测试、消除重复、表达程序员意图、尽量减少类和方法数量。
在重构复杂逻辑时,先跑通全套自动化测试,再合并两处几乎完全一样的分支计算逻辑,消除冗余。 -
第13章 并发编程
分离并发控制代码与纯业务计算,严格限制共享可变数据的作用域,警惕死锁与线程饥饿。
使用线程安全的 ConcurrentHashMap 并配合不可变对象传参,避免在普通 HashMap 上手工写多重 synchronized 乱锁。 -
第14章 逐步改进
以 Args 命令行解析器为例,逐步拆解重构,展示从初版杂乱代码演进为优雅面向对象架构的完整实战。
从一个布尔解析开始,逐步引入泛型参数类型解析器接口,每加一个新类型都保持结构整洁且测试全绿。 -
第15章 JUnit内幕
深入剖析 JUnit 核心模块 ComparisonCompactor,现场演示对已经很优秀的代码进行极限打磨的工匠手艺。
将变量名反复打磨、拆分带有副作用的布尔提取条件,让原本晦涩的字符串差异对比算法一目了然。 -
第16章 重构SerialDate
解构与重构开源日期库 SerialDate,展示真实工业级遗留代码的完整测试补全、结构梳理与坏味道清洗。
为没有任何保护的开源老代码补充完整单测,然后清除常量接口反模式,将魔数全部提升为枚举类。 -
第17章 味道与启发
系统汇总涵盖注释、环境、函数、通用设计、命名与测试等维度的坏味道与启发式规则清单,作为代码审查指南。
在团队 Code Review 中对照清单排查“是否传递了布尔标识”、“是否有重复代码”,快速提升整体代码素养。