第七章 代码的味道

一

入职两个月后,王志远觉得自己已经不算新人了。

他能独立完成一个中等复杂度的功能,能在站会上流畅地汇报进度,能写出规范的提交信息,能在 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 评论只有三条:

  1. "好多了。searchUsers 这个方法名比 process 清晰一百倍。"
  2. "DateUtils.filterByDateRange 的参数顺序建议改成 (list, startDate, endDate),更符合直觉。"
  3. "建议加一个 @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

:::