重复代码(Duplicated Code):坏味道识别与重构实战指南

24种代码坏味道系列 · 第2篇


1. 开篇场景

你是否遇到过这样的场景:在 processUsers 方法中写了一段验证逻辑,然后在 processProducts 方法中又写了几乎相同的验证代码?当你需要修改验证规则时,必须在多个地方同步修改,稍有不慎就会遗漏某个地方,导致系统行为不一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void processUsers(std::vector<std::string>& users) {
for (auto& user : users) {
if (user.empty()) {
std::cout << "Error: Empty user name" << std::endl;
continue;
}
if (user.length() < 3) {
std::cout << "Error: User name too short" << std::endl;
continue;
}
// 处理用户...
}
}

void processProducts(std::vector<std::string>& products) {
// 完全相同的验证逻辑重复了
for (auto& product : products) {
if (product.empty()) {
std::cout << "Error: Empty product name" << std::endl;
continue;
}
if (product.length() < 3) {
std::cout << "Error: Product name too short" << std::endl;
continue;
}
// 处理产品...
}
}

这就是重复代码的典型症状。相同的代码结构在多个地方出现,就像同一首歌的多个翻唱版本,虽然旋律相同,但每个版本都需要单独维护。

当业务规则发生变化时(比如验证规则从”长度至少3”改为”长度至少5”),你需要在所有重复的地方逐一修改。这不仅浪费时间,更重要的是容易出错——漏掉一个地方,就会导致系统行为不一致,产生难以追踪的bug。


2. 坏味道定义

重复代码是指相同的代码结构在多个地方重复出现,违反了DRY(Don’t Repeat Yourself)原则。

就像同一份文档的多个副本,当需要更新内容时,必须同步更新所有副本,否则就会出现信息不一致的问题。

核心问题:代码重复意味着逻辑重复,任何修改都需要在多个地方同步进行,增加了维护成本和出错概率。


3. 识别特征

🔍 代码表现:

  • 特征1:相同的代码块在多个函数/类中出现
  • 特征2:只有变量名不同,但逻辑结构完全相同的代码
  • 特征3:相似的代码模式反复出现(如相同的错误处理、验证逻辑)
  • 特征4:复制粘贴后只修改了少量内容的代码
  • 特征5:多个类中有相同的方法实现

🎯 出现场景:

  • 场景1:快速开发时,直接复制粘贴现有代码并稍作修改
  • 场景2:多个开发者独立实现相似功能,没有代码复用
  • 场景3:重构不彻底,只修改了部分重复代码
  • 场景4:缺乏代码审查,重复代码没有被及时发现

💡 快速自检:

  • 问自己:如果这段逻辑需要修改,我需要在几个地方同时修改?
  • 问自己:这段代码是否在其他地方出现过?
  • 工具提示:使用 grep 或 IDE 的”查找相似代码”功能,可以快速发现重复模式

4. 危害分析

🚨 维护成本:修改一处逻辑需要在多处同步,时间成本增加2-3倍

⚠️ 缺陷风险:容易遗漏某些地方的修改,导致bug,风险增加60%

🧱 扩展障碍:新功能开发时不知道应该复用哪段代码,容易产生新的重复

🤯 认知负担:需要记住哪些地方有重复代码,增加了心理负担


5. 重构实战

步骤1:安全准备

  • ✅ 确保有完整的单元测试覆盖,特别是涉及重复代码的功能
  • ✅ 创建重构分支:git checkout -b refactor/extract-common-logic
  • ✅ 使用版本控制,便于回滚

步骤2:逐步重构

重构前(问题代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class BadExample {
public:
void processUsers(std::vector<std::string>& users) {
// 重复的验证逻辑
for (auto& user : users) {
if (user.empty()) {
std::cout << "Error: Empty user name" << std::endl;
continue;
}
if (user.length() < 3) {
std::cout << "Error: User name too short" << std::endl;
continue;
}
// 处理用户...
std::cout << "Processing user: " << user << std::endl;
}
}

void processProducts(std::vector<std::string>& products) {
// 完全相同的验证逻辑重复了
for (auto& product : products) {
if (product.empty()) {
std::cout << "Error: Empty product name" << std::endl;
continue;
}
if (product.length() < 3) {
std::cout << "Error: Product name too short" << std::endl;
continue;
}
// 处理产品...
std::cout << "Processing product: " << product << std::endl;
}
}
};

问题分析

  • processUsersprocessProducts 中有完全相同的验证逻辑
  • 如果验证规则改变(如最小长度改为5),需要在两个地方修改
  • 错误消息中的”user name”和”product name”可以统一为”name”

重构后(清洁版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class GoodExample {
private:
// ✅ 提取公共验证逻辑
bool validateName(const std::string& name) {
if (name.empty()) {
std::cout << "Error: Empty name" << std::endl;
return false;
}
if (name.length() < 3) {
std::cout << "Error: Name too short" << std::endl;
return false;
}
return true;
}

public:
void processUsers(std::vector<std::string>& users) {
for (auto& user : users) {
// ✅ 复用验证逻辑
if (!validateName(user)) continue;
std::cout << "Processing user: " << user << std::endl;
}
}

void processProducts(std::vector<std::string>& products) {
for (auto& product : products) {
// ✅ 复用验证逻辑
if (!validateName(product)) continue;
std::cout << "Processing product: " << product << std::endl;
}
}
};

关键变化点

  1. 提取方法(Extract Method)

    • 将重复的验证逻辑提取到 validateName 方法中
    • 统一错误消息,使用通用的”name”而不是”user name”或”product name”
  2. 简化调用

    • 两个方法都调用 validateName,代码更简洁
    • 验证逻辑集中管理,修改时只需改一处
  3. 提高可维护性

    • 如果需要修改验证规则(如最小长度改为5),只需修改 validateName 方法
    • 如果需要添加新的验证规则(如不能包含特殊字符),也只需修改一处

步骤3:重构技巧总结

使用的重构手法

  • 提取方法(Extract Method):将重复的代码块提取为独立方法
  • 统一命名(Rename):将相似的命名统一,便于识别重复

注意事项

  • ⚠️ 提取方法时,确保参数命名足够通用,适用于所有使用场景
  • ⚠️ 如果重复代码有细微差异,先统一差异,再提取
  • ⚠️ 提取后要运行所有测试,确保行为没有改变

6. 预防策略

🛡️ 编码时:

  • 即时检查

    • 复制粘贴代码后,立即问自己:这段代码是否可以提取为公共方法?
    • 编写新功能时,先检查是否有类似的现有代码可以复用
    • 使用IDE的”查找相似代码”功能,及时发现重复
  • 小步提交

    • 每次提交前检查是否有重复代码
    • 如果发现重复,立即重构后再提交

🔍 Code Review清单:

  • 重点检查

    • 新代码是否与现有代码重复?
    • 是否有可以提取的公共逻辑?
    • 错误处理和验证逻辑是否统一?
  • 拒绝标准

    • 明显的复制粘贴代码(超过5行相同)
    • 可以复用但没有复用的代码
    • 多个地方有相同业务规则但实现不同

⚙️ 自动化防护:

  • IDE配置

    • 启用代码重复检测插件(如 CPD - Copy/Paste Detector)
    • 配置相似代码阈值(如超过10行相同代码时警告)
  • CI/CD集成

    • 在CI流水线中集成代码重复检测工具(如 jscpdPMD CPD
    • 设置重复代码阈值,超过阈值时阻止合并
    • 定期生成代码重复报告,识别需要重构的区域

下一篇预告:过长函数(Long Function)- 如何将”巨无霸”函数拆分为可管理的小函数