第七章 代码的味道
一
入职两个月后,王志远觉得自己已经不算新人了。
他能独立完成一个中等复杂度的功能,能在站会上流畅地汇报进度,能写出规范的提交信息,能在 PR 描述里写清楚改了什么、为什么改。
他甚至开始有了一点自信——上个 Sprint 他完成了所有任务,没有延期,没有线上 bug。周旋经理在站会上点名表扬了他:"志远最近进步不错。"
这种自信在周二下午被苏薇彻底击碎了。
二
那天苏薇在 Review 他的一个 PR——一个用户管理模块的搜索功能。代码量不大,大概三百多行,王志远自己觉得写得还不错。
然后他看到了苏薇的 Review 评论。
一共十七条。
第一条:"这个方法 85 行,做了查询、过滤、排序、分页、格式化五件事。一个方法只应该做一件事。"
第二条:"变量名 list1、list2、temp——这是什么?三个月后你自己能看懂吗?"
第三条:"这段逻辑和 OrderService 里的 filterByDate 几乎一模一样,复制粘贴的吧?"
第四条:"这个 if-else 嵌套了四层,我数了一下缩进,最深的地方有八个 tab。这不是代码,是俄罗斯套娃。"
第五条:"魔法数字。if (status == 3) ——3 是什么意思?已删除?已冻结?已成仙?"
……
王志远越看越脸红。他觉得苏薇在鸡蛋里挑骨头——功能是好的啊,测试也通过了,为什么要纠结这些"细节"?
他忍不住在 PR 的评论区回了一条:"功能已经实现了,测试也通过了,这些命名和格式的问题有那么重要吗?"
三分钟后,苏薇回复了一条:
"来会议室。"
三
会议室里,苏薇把他的代码投影到了白墙上。
"你说功能实现了,测试通过了,所以代码没问题。"苏薇的语气很平静,但王志远已经学会了——苏薇越平静,说明她越认真。"那我问你一个问题:这段代码,除了你自己,还有谁能看懂?"
王志远看了看屏幕上的代码,想说"应该都能看懂吧",但他仔细看了一下那个 85 行的方法,发现自己都需要花几秒钟才能理清逻辑。
苏薇继续说:"你以为代码是写给机器看的?机器只认字节码,压根不看你的源文件。代码是写给人看的——给你自己看,给你队友看,给三个月后接手这个模块的倒霉蛋看。"
她在白板上写了一行字:
好代码的四个标准:正确、易懂、易改、高效。
"你的代码满足了第一条——正确。功能是对的,测试通过了。但后面三条全不及格。"
她指着屏幕上的代码,逐一分析:
"不易懂:变量名 list1、list2 没有语义,别人看到不知道里面装的是什么。方法名 process 太笼统,不知道在处理什么。85 行的方法,读到第 50 行的时候已经忘了第 10 行在干嘛。"
"不易改:五件事情揉在一个方法里,如果将来要改排序逻辑,你得在 85 行代码里找到排序的那十几行,小心翼翼地改,还要担心会不会影响到其他四件事。如果排序逻辑是一个独立的方法,改起来就简单多了。"
"不高效:你把所有数据查出来再在内存里过滤和排序,数据量小的时候没问题,数据量大了就是灾难。过滤和排序应该在 SQL 层面做,让数据库来干它擅长的事。"
王志远听着,脸上的不服气慢慢消退了。他开始意识到,苏薇说的不是"鸡蛋里挑骨头",而是他的代码确实有问题——只不过这些问题不会让程序崩溃,而是会让维护代码的人崩溃。
四
"那怎么改?"王志远问。
苏薇没有直接回答,而是从包里掏出一本书,往桌上一放。
封面上写着:《重构:改善既有代码的设计》——Martin Fowler
"这本书你先看前五章。看完之后,回来重写你的代码。"
王志远拿起书翻了翻,有点厚。
苏薇看出了他的犹豫:"不用一口气啃完。先翻第三章——'代码的坏味道'。Martin Fowler 列了二十多种代码臭味,你那段代码我数了数,至少占了七八种。"
"代码臭味?"
"对。就像食物变质了会发臭一样,代码写得不好也会'发臭'。你闻不到,但有经验的人一眼就能看出来。"
她在白板上列了几种常见的代码臭味:
常见代码臭味(Code Smells)
├── 过长方法(Long Method):一个方法超过 20 行就要警惕
├── 过大类(Large Class):一个类承担了太多职责
├── 重复代码(Duplicated Code):复制粘贴是万恶之源
├── 魔法数字(Magic Number):代码里出现意义不明的数字
├── 过深嵌套(Deep Nesting):if-else 嵌套超过三层
├── 命名不清(Unclear Naming):list1、temp、data、process
├── 过长参数列表(Long Parameter List):方法参数超过 3 个
└── 注释补偿(Comments as Deodorant):用注释解释烂代码,不如把代码写清楚
"最后这条你记好了。"苏薇说。"好多人觉得代码写得烂没关系,加个注释就完事了。但真正好的代码,你看变量名、方法名就知道它在干嘛,根本不需要注释。注释是用来解释'为什么这么干'的,不是用来翻译代码的。"
五
那天晚上,王志远回到出租屋,翻开了《重构》。
他先看了第三章"代码的坏味道",然后对照着自己的代码,一条一条地检查。结果触目惊心——苏薇说他的代码占了七八种臭味,他自己数了一下,至少有九种。
他又翻到第一章,看到了 Martin Fowler 的一句话:
"任何一个傻瓜都能写出计算机可以理解的代码。唯有好的程序员能写出人类可以理解的代码。"
这句话像一记闷棍,敲在他的脑门上。
他一直以为"代码能跑就行",但现在他意识到——能跑只是最低标准。代码的生命周期里,编写只占 20% 的时间,剩下 80% 的时间都在阅读和修改。如果代码写得不好,那 80% 的时间就是在受罪。
他开始重写那个搜索功能。
六
重写的过程比他想象的痛苦。
不是技术上的痛苦——重构的手法并不复杂。痛苦的是心理上的——他要亲手拆掉自己写的代码,承认它们是"臭"的。
他先把那个 85 行的方法拆成了五个小方法:
// 重构前:一个方法做五件事
public Page<UserDTO> process(Map<String, Object> params) {
// 85 行代码,查询+过滤+排序+分页+格式化全在一起
}
// 重构后:每个方法只做一件事
public Page<UserDTO> searchUsers(UserSearchRequest request) {
List<User> users = queryUsers(request); // 查询
List<User> filtered = filterUsers(users, request); // 过滤
List<User> sorted = sortUsers(filtered, request); // 排序
Page<User> paged = paginate(sorted, request); // 分页
return convertToDTO(paged); // 格式化
}
然后他把变量名全部改了:
// 重构前
List<User> list1 = dao.getAll();
List<User> list2 = new ArrayList<>();
for (User temp : list1) {
if (temp.getStatus() == 3) {
list2.add(temp);
}
}
// 重构后
List<User> allUsers = userRepository.findAll();
List<User> activeUsers = allUsers.stream()
.filter(user -> user.getStatus() != UserStatus.DELETED)
.collect(Collectors.toList());
魔法数字也消灭了——3 变成了 UserStatus.DELETED,10 变成了 DEFAULT_PAGE_SIZE,30 变成了 MAX_EXPORT_DAYS。
四层嵌套的 if-else 用卫语句(Guard Clause)拆开了:
// 重构前:四层嵌套
if (request != null) {
if (request.getKeyword() != null) {
if (request.getKeyword().length() > 0) {
if (request.getKeyword().length() <= 50) {
// 真正的业务逻辑
}
}
}
}
// 重构后:卫语句,提前返回
if (request == null) return emptyPage();
if (StringUtils.isBlank(request.getKeyword())) return emptyPage();
if (request.getKeyword().length() > 50) {
throw new IllegalArgumentException("关键词不能超过50个字符");
}
// 真正的业务逻辑
重复代码也处理了——他把和 OrderService 里重复的日期过滤逻辑提取成了一个公共方法,放在 DateUtils 工具类里。
改完之后,他数了一下——原来的 300 多行代码变成了 250 行左右。行数没减多少,但结构完全不同了。每个方法都不超过 20 行,每个变量名都能看出含义,每个常量都有名字。
他自己读了一遍,发现不需要任何注释就能看懂每一段代码在做什么。
这种感觉很奇妙——就像打扫完房间之后,虽然东西还是那些东西,但整个空间都亮堂了。
七
他重新提交了 PR。这次苏薇的 Review 评论只有三条:
- "好多了。
searchUsers这个方法名比process清晰一百倍。" - "
DateUtils.filterByDateRange的参数顺序建议改成(list, startDate, endDate),更符合直觉。" - "建议加一个
@Deprecated注解标记旧的process方法,给调用方一个过渡期。"
三条。从十七条到三条。
王志远改完后,PR 顺利合入。
他在 PR 的评论区写了一句:"谢谢苏薇的 Review,学到了很多。"
苏薇回复了一个字:"嗯。"
但王志远觉得这个"嗯"比上次的十七条评论更让他开心。
八
从那以后,王志远开始有意识地关注代码质量。
他把苏薇推荐的《重构》看了前十章,又自己找了一些资料,学习了几个重要的编程原则:
KISS(Keep It Simple, Stupid)——保持简单。能用简单的方式实现的,不要用复杂的方式。不要为了炫技而写复杂的代码。
他想起陈锐有一次写了一段用了三层泛型嵌套加反射的代码,在站会上得意地说"这段代码很优雅"。赵北辰瞄了一眼说:"全组能看懂这段代码的,不超过两个人。这不叫优雅,叫自嗨。"
DRY(Don't Repeat Yourself)——不要重复自己。如果同样的逻辑出现了两次以上,就应该提取成公共方法。复制粘贴是技术债的最大来源。
SOLID 原则——这是面向对象设计的五个基本原则:
- Single Responsibility(单一职责):一个类只做一件事
- Open/Closed(开闭原则):对扩展开放,对修改关闭
- Liskov Substitution(里氏替换):子类可以替换父类
- Interface Segregation(接口隔离):接口要小而专
- Dependency Inversion(依赖倒置):依赖抽象,不依赖具体实现
他一开始觉得 SOLID 太抽象了,看了半天也没完全理解。赵北辰给他举了一个例子:
"你家里的插座是不是都是统一标准的?不管你插电视、冰箱还是手机充电器,都能用。这就是接口隔离和依赖倒置——电器不依赖具体的插座,插座也不依赖具体的电器,它们都依赖一个统一的'接口标准'。"
"如果每个电器都需要一个专用插座呢?那你家墙上得装几十种不同的插座,换一个电器就得重新装修。这就是'依赖具体实现'的后果。"
王志远一下子就懂了。
九
有一天,赵北辰在 Review 王志远的代码时,说了一段让他印象深刻的话:
"志远,你现在的代码比两个月前好了很多。但我想跟你说一个更高层次的东西。"
他在白板上写了四个字:
代码即文档
"很多人觉得代码和文档是两回事——代码是给机器看的,文档是给人看的。但好的代码本身就是最好的文档。"
他举了一个例子:
// 坏代码:需要注释才能理解
// 检查用户是否有权限执行操作
if (u.getR() == 1 || u.getR() == 2 && u.getD() != null
&& u.getD().before(new Date())) {
// 执行操作
}
// 好代码:自解释,不需要注释
if (user.isAdmin() || user.hasValidMembership()) {
executeOperation();
}
"你看,好的代码读起来就像英语句子:'如果用户是管理员,或者用户有有效的会员资格,就执行操作。'不需要任何注释,意图一目了然。"
"那注释什么时候该写?"王志远问。
"注释应该解释为什么,而不是是什么。"赵北辰说。
// 坏注释:解释"是什么"(代码本身已经说了)
i++; // i 加 1
// 好注释:解释"为什么"(代码看不出来的背景信息)
// 由于支付网关的限制,重试间隔必须大于 3 秒,否则会被限流
Thread.sleep(3000);
"如果你发现自己需要写很多注释来解释代码在做什么,那说明代码本身写得不够清楚。与其加注释,不如重构代码。"
王志远把这段话记在了笔记本上,旁边画了一个大大的星号。
十
又过了两周,王志远在 Review 马德华的一个 PR 时,发现了一段代码:
public void doSomething(String a, int b, String c, int d,
boolean e, String f, Date g) {
// 200 行代码...
}
七个参数,方法名叫 doSomething,方法体 200 行。
他犹豫了一下,在 Review 评论里写了一条:"德华哥,这个方法的参数有点多,方法名也不太清晰。要不要考虑用一个 Request 对象封装参数,方法名改成更具体的?"
他写完之后有点忐忑——马德华是老员工,自己一个新人去 Review 老员工的代码,会不会不太合适?
马德华的回复很快:"小胖,你说得对,我改改。"
王志远松了一口气。他发现马德华虽然在工作态度上有点油滑,但在技术问题上还是能听进去意见的。
苏薇看到了这条 Review 评论,在旁边加了一条:"同意小胖的意见。另外建议把 200 行的方法体也拆一下。"
马德华回复:"好好好,我都改。"
那天下班的时候,马德华路过王志远的工位,拍了拍他的肩膀:"小胖,你现在 Review 代码越来越有模有样了。"
王志远笑了笑:"都是被苏薇 Review 出来的。"
"被苏薇 Review 过的人,代码质量都不会差。"马德华感慨道。"她是我见过最严格的 Reviewer,但也是最靠谱的。"
王志远点了点头。他想起两个月前,苏薇给他的第一次 Review——十七条评论,每一条都像一巴掌。但现在回头看,那十七巴掌打醒了他。
十一
那天晚上,王志远在笔记本上写下:
Day 60
苏薇给我的 PR 提了十七条 Review 评论。十七条。我数了三遍。
当时觉得是十七巴掌,现在回头看,这是入职以来收到的最好的礼物。每一条都指向一个我没意识到的坏习惯——变量名随便起、方法写到五十行不拆、复制粘贴当重用。
"能跑"只是及格线。好代码要正确、易懂、易改。说白了就四个字:高内聚,低耦合。
还有一个感悟:Code Review 不是找茬,是一群人一起把代码变好。被 Review 是学习,Review 别人也是学习。
他合上笔记本,拿起《重构》继续看。
书的第一章有一句话他特别喜欢:
"重构的目的不是为了让代码更短,而是为了让代码更清晰。"
他觉得这句话不只是在说代码。
人也一样。成长的目的不是为了变得更"厉害",而是为了变得更清晰——清楚自己在做什么,清楚自己为什么这么做,清楚自己还差什么。
窗外的月光很亮。他看了一眼手机,十一点半。
明天还有一个 PR 要提交。他决定在提交之前,先自己 Review 一遍——用苏薇的标准。
:::{admonition} 小胖的笔记 :class: tip
本章知识卡片:代码质量与重构
1. 好代码的四个标准
| 标准 | 含义 | 检查方法 |
|---|---|---|
| 正确 | 功能符合需求,没有 bug | 单元测试、集成测试 |
| 易懂 | 别人能快速理解代码意图 | 让队友读一遍,看能否看懂 |
| 易改 | 修改一个功能不会影响其他功能 | 模块化、低耦合 |
| 高效 | 性能满足要求 | 压测、性能分析 |
2. 常见代码臭味(Code Smells)
| 臭味 | 表现 | 重构手法 |
|---|---|---|
| 过长方法 | 方法超过 20 行 | 提取方法(Extract Method) |
| 过大类 | 一个类做太多事 | 提取类(Extract Class) |
| 重复代码 | 相同逻辑出现多次 | 提取公共方法 |
| 魔法数字 | if (status == 3) |
用常量或枚举替代 |
| 过深嵌套 | if-else 超过 3 层 | 卫语句(Guard Clause) |
| 命名不清 | list1、temp、process |
用有意义的名字 |
| 过长参数 | 方法参数超过 3 个 | 用参数对象封装 |
| 注释补偿 | 用注释解释烂代码 | 重构代码使其自解释 |
3. 编程三原则
KISS(Keep It Simple, Stupid)
能用简单方式实现的,不要用复杂方式。代码是写给人看的。
DRY(Don't Repeat Yourself)
同样的逻辑不要写两遍。复制粘贴 = 技术债。
YAGNI(You Aren't Gonna Need It)
不要提前实现"将来可能用到"的功能。需要的时候再加。
4. SOLID 原则
| 原则 | 全称 | 一句话解释 | 生活类比 |
|---|---|---|---|
| S | Single Responsibility | 一个类只做一件事 | 一把钥匙开一把锁 |
| O | Open/Closed | 对扩展开放,对修改关闭 | 插座标准统一,新电器直接插 |
| L | Liskov Substitution | 子类可以替换父类 | 实习生能顶替正式员工的基本工作 |
| I | Interface Segregation | 接口要小而专 | 遥控器按钮分区,不要一个万能键 |
| D | Dependency Inversion | 依赖抽象,不依赖具体 | 充电器用 USB-C 标准,不绑定品牌 |
5. 注释的正确用法
❌ 解释"是什么"(代码本身应该说清楚)
i++; // i 加 1
✅ 解释"为什么"(代码看不出来的背景)
// 支付网关限流规则:重试间隔必须 > 3秒
Thread.sleep(3000);
✅ 解释"为什么不"(看起来奇怪的决策)
// 不用 StringBuilder 是因为这里字符串拼接次数 < 3,
// JVM 会自动优化,StringBuilder 反而增加代码复杂度
6. Code Review 检查清单
| # | 检查项 |
|---|---|
| 1 | 方法是否超过 20 行? |
| 2 | 变量名/方法名是否有意义? |
| 3 | 是否有重复代码? |
| 4 | 是否有魔法数字? |
| 5 | if-else 嵌套是否超过 3 层? |
| 6 | 方法参数是否超过 3 个? |
| 7 | 是否有不必要的注释? |
| 8 | 异常处理是否完善? |
推荐阅读:《重构:改善既有代码的设计》——Martin Fowler
:::