代码重复检测:为什么说重复代码是“技术债的利息”

说实话,我见过太多项目被重复代码拖垮了。不是那种一眼就能看出来的复制粘贴,而是藏在各个角落、长得差不多的逻辑片段。你想想看,一个系统跑了三五年,维护的人换了好几拨,代码里到处都是“孪生兄弟”——改了一个地方,另外三个地方忘了改,bug就这么悄悄冒出来了。

我个人习惯,在项目里把重复代码当作“技术债的利息”来看待。本金是糟糕的设计,利息就是每天因为重复代码多花的时间。今天咱们就聊聊怎么识别、怎么处理这些“利息”。

核心观点:重复代码是代码质量下降的第一信号。它意味着维护成本翻倍、bug概率翻倍、重构难度翻倍。

重复代码的危害:不只是“多写了几行”

我在项目中遇到过最典型的案例:一个支付系统,同样的金额校验逻辑在三个模块里各写了一遍。后来业务要求调整校验规则,三个地方只改了俩,结果线上出了资损。你说这是谁的责任?写代码的人?还是允许重复代码存在的代码审查?

重复代码的危害,我总结成四点:

  • 维护成本爆炸:改一个逻辑,要搜遍全项目找“兄弟代码”。漏一个就出问题。
  • 理解成本上升:新同事看代码,发现好几处相似的逻辑,会困惑“到底哪个才是对的?”
  • 测试工作量翻倍:同样的功能,因为散落在各处,测试用例也得重复写。
  • 重构阻力巨大:想抽公共函数?先得确认所有重复点是不是真的“一模一样”。

我曾经踩过的坑:有个项目,重复率高达35%。我接手后想重构,结果发现很多重复代码其实“长得像但逻辑有细微差异”。强行合并反而引入了新bug。所以检测重复代码,不能只看行数,还得看语义。

检测重复代码的工具:Simian 和 CPD

工欲善其事,必先利其器。检测重复代码,我常用两个工具:Simian 和 CPD。它们各有侧重,我一般搭配着用。

Simian(Similarity Analyzer)

Simian 是个轻量级工具,支持多种语言。它的特点是快,而且能检测“近似重复”——不要求完全一样,相似度达到阈值就算。

# 命令行用法示例
java -jar simian-4.0.0.jar \
  -includes="**/*.c" \
  -threshold=6 \
  -language=c \
  -formatter=xml > simian-report.xml

参数说明:-threshold=6 表示连续6行以上相似就算重复。-language=c 指定语言。输出XML格式方便集成到CI流水线。

CPD(Copy-Paste Detector)

CPD 是 PMD 工具集里的一个模块。它比 Simian 更“智能”一些,能识别标识符重命名后的重复。比如你把变量名从 count 改成 num,CPD 依然能检测出来。

# CPD 命令行用法
pmd cpd \
  --minimum-tokens 100 \
  --language c \
  --files ./src/ \
  --format xml > cpd-report.xml

--minimum-tokens 100 表示最少100个token(不是行数)才算重复。这个值需要根据项目调,太小了误报多,太大了漏报多。

特性 Simian CPD
检测粒度 行数 Token数
近似重复检测 支持(可设阈值) 支持(重命名后仍能检测)
速度 中等
误报率 较高 较低
集成难度 低(单jar包) 中(需PMD环境)

我的建议:CI流水线里两个工具都跑。Simian 做快速扫描,CPD 做深度分析。Simian 报警的,人工确认;CPD 报警的,基本可以放心去重构。

重构策略:从“复制粘贴”到“复用”

检测出重复代码后,怎么改?我一般按这个优先级来:

  1. 提取函数:最简单的策略。把重复的代码块抽成一个函数,所有地方调用它。
  2. 参数化:如果重复代码只有少量参数不同,用函数参数来统一。
  3. 模板方法:如果重复的是算法骨架,但细节不同,用模板方法模式。
  4. 策略模式:如果重复的是条件分支,用策略模式把每个分支独立出来。

举个例子,假设你有三段这样的代码:

// 重复代码示例
int calc_a(int x) {
    int result = 0;
    for (int i = 0; i < x; i++) {
        result += i * 2;
    }
    return result;
}

int calc_b(int x) {
    int result = 0;
    for (int i = 0; i < x; i++) {
        result += i * 3;
    }
    return result;
}

int calc_c(int x) {
    int result = 0;
    for (int i = 0; i < x; i++) {
        result += i * 4;
    }
    return result;
}

重构后:

int calc(int x, int multiplier) {
    int result = 0;
    for (int i = 0; i < x; i++) {
        result += i * multiplier;
    }
    return result;
}

// 调用时:calc(x, 2), calc(x, 3), calc(x, 4)

注意:不是所有重复都要合并。如果两段代码只是“长得像”,但业务含义完全不同,强行合并反而会让代码更难理解。我见过有人把“计算折扣”和“计算运费”合并成一个函数,结果参数列表长得吓人,后来还是拆开了。

在代码审查中的应用:把重复检测变成“门禁”

代码审查是拦截重复代码的最后一道防线。我建议在审查流程里加两个环节:

  • 自动门禁:CI里跑重复检测,如果新增代码的重复率超过阈值(比如5%),直接打回。
  • 人工审查:审查者用工具生成的报告,逐条确认。重点看那些“近似重复”——工具可能漏报,但人眼能看出来。

我个人习惯,在审查意见里会这么写:

“这段逻辑和 module_a.c 第120行的 validate_input() 几乎一样。建议提取公共函数,或者至少加个注释说明为什么不能复用。”

你想想看,如果每个审查者都这么较真,重复代码根本活不过第一次提交。

知识体系图:重复代码检测全景

重复代码检测知识体系 检测工具 Simian CPD 四大危害 维护成本 爆炸 理解成本 上升 测试工作 翻倍 重构阻力 巨大 重构策略 提取函数 参数化 模板方法 策略模式 代码审查:自动门禁 + 人工确认 检测 → 识别危害 → 选择策略 → 审查落地

这张图把整个流程串起来了。从检测工具开始,到识别危害,再到选择重构策略,最后在代码审查里落地。每一步都环环相扣。

最后说一句:重复代码检测不是一次性工作。我建议每个迭代都跑一次,把重复率控制在5%以下。超过10%就要拉警报了。记住,代码写一次,但读很多次。少一点重复,多一点清爽。

公众号:蓝海资料掘金营,微信deep3321