重複コードは、同じ処理や同じ規則がソースコードの複数箇所に書かれている状態です。研究分野では「コードクローン」と呼び、一字一句同じコピーから、変数名だけ変えたもの、書き方は違うが同じ計算をするものまでを4つの型に分けます。この記事では型の見分け方、jscpd・PMD CPD・SonarQubeで検出するときの設定値、関数抽出で解消する手順、そして重複を残すほうが安全な条件までを、実行例つきで整理します。
まとめ:重複コードの定義・検出・解消判断の要点
- 重複コード(コードクローン)は、同一または類似のコード片が複数箇所にある状態。Roy & Cordyの調査では、ソフトウェアシステム内のコードの約5〜20%が重複しうるという先行研究が紹介されている
- 型はType-1(空白・コメントだけ違う)、Type-2(名前・リテラルが違う)、Type-3(文の追加・削除あり)、Type-4(書き方が違うが同じ計算)の4つ
- 問題は「量」より修正漏れ。Juergensらの調査では、クローングループの52%に不整合な変更が見つかった
- jscpd 5.4.0は既定では変数名を変えたType-2を報告せず、
--ignore-identifiersで拾える。PMD CPD 7.28.0も同じく--ignore-identifiersが必要 - SonarQubeのSonar way品質ゲートは、新規コードの重複率3.0%以下を条件にしている
- 変更理由が同じ重複は1か所にまとめ、変更理由が違う「見た目だけ同じ」コードは残す。3回目の重複で抽出するRule of Threeが目安
以下、定義から順に根拠と手順を示します。
重複コード(コードクローン)の定義と冗長なコードとの違い
英語ではduplicate code(duplicated code)またはcode cloneと書きます。duplicateは「複製」「重複」を意味し、プログラミングでは同じ内容が2つ以上存在する状態を指します。日本語では「コードクローン」「クローンコード」「重複コード」が同じ意味で使われ、学術論文では「コードクローン」が一般的です。
Queen’s Universityの技術報告書(Roy & Cordy, 2007, TR 2007-541)は、コード片をコピーして貼り付け、そのまま、または少し手を加えて再利用することを、保守でよく知られたコードの臭い(code smell)だと位置づけています。同報告書は、ソフトウェアシステム内のコードの約5%から20%が重複しうるという複数の研究結果を紹介しています。
「冗長なコード」は重複コードより広い言葉です。重複のほかに、使われない変数、同じ条件の二重判定、標準ライブラリで1行になる処理を手で書いたもの、なども冗長に含まれます。重複コードは冗長なコードの代表例ですが、冗長なコードがすべて重複とは限りません。この記事は「同じものが複数箇所にある」重複に絞って扱います。
コードクローン4分類(Type-1〜Type-4)の見分け方
型の定義は、Roy & Cordyの報告書の分類が広く使われています。原文の要点を日本語にすると次のとおりです。
| 型 | 違ってよい部分 | 典型的な発生源 | 検出の難しさ |
|---|---|---|---|
| Type-1 | 空白・レイアウト・コメント | そのままコピー | 低(テキスト比較で可) |
| Type-2 | 識別子・リテラル・型名 | コピー後に変数名を変更 | 中(名前の正規化が必要) |
| Type-3 | 文の追加・削除・変更 | コピー後に分岐を1つ追加 | 高(類似度の閾値が必要) |
| Type-4 | 構文そのもの | 別人が同じ仕様を別実装 | 最高(意味の比較が必要) |
Type-4について、報告書は「同じ計算を異なる構文で実装した2つ以上のコード片」と定義しています。forループで合計する関数とreduceで合計する関数は、行の対応がなくてもType-4です。
型の区別は、ツールの設定を決めるときに効きます。この記事で扱うjscpdとPMD CPDは、既定では識別子の違いを無視しません。変数名を変えてコピーしたType-2は、設定を変えない限り検出結果に出てこないため、「ツールで0件だった」は「重複がない」を意味しません。
重複コードが不具合を生む仕組み:修正漏れと不整合変更
重複の実害は、同じ規則を直すときに一部だけ直してしまう「不整合な変更」です。消費税率や割引条件のような業務規則が3か所に書かれていれば、仕様変更のたびに3か所すべてを探して直す必要があります。1か所を見落とすと、画面によって合計金額が違うという不具合になります。
この影響を実データで測ったのが、Juergensらの論文「Do Code Clones Matter?」(ICSE 2009、arXiv:1701.05472で公開)です。商用とオープンソースのシステムを分析した結果、クローングループの約半数(52%)に不整合が含まれていました。著者らは、意図しない不整合な変更のおよそ2回に1回から3回に1回が不具合につながったと報告しています。
「似た部分に同じ誤りがある」状態もここから生まれます。バグを含むコードがコピーされると、バグも一緒に複製されます。1か所を直しても、他のコピーには誤りが残り続けます。テストも重複の数だけ必要になり、片方だけテストされた状態が放置されがちです。
重複コードの検出ツールと設定値
目視や単純な文字列検索だけでは、変数名を変えたType-2以降を網羅的に見つけることは困難です。検出はツールに任せ、人は「まとめるべき重複か」の判断に時間を使うのが効率的です。ここでは対象言語の広いjscpd、Java系で定番のPMD CPD、継続監視のSonarQubeを取り上げます。
jscpd 5.4.0の既定値とType-2の検出
jscpdはnpmで配布されるコピー&ペースト検出ツールです。2026年9月30日公開の5.4.0時点で、v5はNode.js実行環境が不要なRust製バイナリになり、従来のTypeScript実装はjscpd@4として別に保守されています。READMEに記載された既定値は、最小トークン数(--min-tokens)が50、最小行数(--min-lines)が5です。
会員割引と消費税を計算する12行の関数を3ファイルに用意し、jscpd 5.4.0で検査しました。order.jsとinvoice.jsは関数名だけが違うコピーで、cart.jsは引数と変数の名前をすべて変えたコピーです。元になったorder.jsは次の関数です。
export function orderTotal(items, member) {
let subtotal = 0;
for (const item of items) {
subtotal += item.price * item.quantity;
}
let discount = 0;
if (member && subtotal >= 5000) {
discount = Math.floor(subtotal * 0.1);
}
const tax = Math.floor((subtotal - discount) * 0.1);
return subtotal - discount + tax;
}
$ npx [email protected] src
Clone found (javascript)
- invoice.js [1:29 - 12:2] (12 lines, 80 tokens)
order.js [1:27 - 12:2]
Found 1 clones.
$ npx [email protected] src --ignore-identifiers
Clone found (javascript, renamed)
- cart.js [1:1 - 12:2] (12 lines, 83 tokens)
invoice.js [1:1 - 12:2]
Clone found (javascript, renamed)
- cart.js [1:1 - 12:2] (12 lines, 83 tokens)
order.js [1:1 - 12:2]
Found 2 clones.
既定の実行では、変数名を変えたcart.jsは検出されません。--ignore-identifiersを付けると識別子をすべて同一視し、Type-2として「renamed」の種別で報告されます。v5は--similarity(JavaScript/TypeScriptの関数をAST類似度で比較するType-3)や、埋め込みモデルで比較する--semantic(Type-4、実験的機能)も備えています。
PMD CPD 7.28.0でJavaのType-2を拾う設定
PMDに同梱されるCPD(Copy/Paste Detector)は、Javaを中心に多数の言語に対応しています。2026年9月25日公開の7.28.0では、pmd cpdサブコマンドで実行し、--minimum-tokensの指定が必須です。同じ計算をする2つのJavaクラス(変数名だけ違う)で試すと、次の結果になりました。
$ pmd cpd --minimum-tokens 30 --dir java --language java
(出力なし・終了コード0)
$ pmd cpd --minimum-tokens 30 --dir java --language java --ignore-identifiers
Found a 14 line (90 tokens) duplication in the following files:
Starting at line 1 of java/CartService.java
Starting at line 1 of java/OrderService.java
(終了コード4)
CPDも既定ではType-2を報告しません。--ignore-identifiersのほか、数値や文字列の違いを無視する--ignore-literals、アノテーションを無視する--ignore-annotationsがあります。重複が見つかると終了コード4で終わるため、CIではそのままジョブ失敗として扱えます。失敗させずにレポートだけ出したい場合は--no-fail-on-violationを付けると終了コードが0になります。PHPなら同系統のPHPMDの使い方とルールセットとあわせて導入できます。
SonarQubeの重複ブロック判定と品質ゲート3%
SonarQube Serverのメトリクス定義では、Java以外の言語は「100トークン以上が連続して重複し、それが10行以上(COBOLは30行、ABAPは20行)にわたる」場合に重複ブロックと数えます。Javaは「トークン数・行数に関係なく10ステートメント以上が連続して重複する」場合です。インデントと文字列リテラルの違いは無視されます。CSSとTerraformなどのIaC言語は重複検出の対象外です。
重複率(duplicated_lines_density)は「重複行数÷総行数×100」で計算されます。既定の品質ゲートSonar wayは4条件のうち1つに「新規コードの重複率が3.0%以下」を置いています。既存コード全体ではなく新しく書いた部分だけを見るため、古い重複を抱えたプロジェクトでも今日から導入できます。品質ゲートをCIに組み込む手順はコード品質の指標と品質ゲートの組み込み手順で扱っています。
重複コードを解消するリファクタリング手順
解消の基本は、重複している「規則」を1か所に移し、呼び出し側をそこへ向けることです。手順を誤ると挙動が変わるため、次の順で進めます。
- 重複箇所の現在の挙動をテストで固定する(境界値を含める)
- 共通部分を関数として抽出し、違う部分を引数にする(Extract Function・パラメータ化)
- 呼び出し側を1か所ずつ新しい関数へ置き換え、そのたびにテストを通す
- 旧コードを削除し、検出ツールで重複が消えたことを確認する
先ほどの3ファイルの重複を1つの関数にまとめると、次のようになります。割引の閾値と税率を定数として名前を付けたのがポイントで、税率変更は1行の修正で済みます。
// pricing.mjs:会員割引と税の計算ルールを1か所に置く
export const MEMBER_DISCOUNT_MIN = 5000;
export const MEMBER_DISCOUNT_RATE = 0.1;
export const TAX_RATE = 0.1;
export function calcTotal(lines, isMember) {
const subtotal = lines.reduce((sum, l) => sum + l.price * l.quantity, 0);
const discount =
isMember && subtotal >= MEMBER_DISCOUNT_MIN
? Math.floor(subtotal * MEMBER_DISCOUNT_RATE)
: 0;
const tax = Math.floor((subtotal - discount) * TAX_RATE);
return subtotal - discount + tax;
}
置き換え前に、旧関数と新関数の結果が一致することをテストで確かめます。旧関数をsrc/order.mjs、新関数をrefactor/pricing.mjsに置き、次のテストをnode refactor/test.mjsで実行します。Node.js 26.5.0で、非会員・割引あり・閾値未満(4,999円)・閾値ちょうど(5,000円)・空配列の5ケースが一致しました。条件を>=から>に書き換えると、5,000円のケースが4,950円でなく5,500円になって失敗します。境界値は「ちょうど」の値を入れないと、こうした書き換えを見逃します。
import assert from "node:assert/strict";
import { calcTotal } from "./pricing.mjs";
import { orderTotal } from "../src/order.mjs";
const cases = [
[[{ price: 1200, quantity: 3 }], false],
[[{ price: 1200, quantity: 5 }], true], // 6000円→割引600円→税540円→5940円
[[{ price: 4999, quantity: 1 }], true], // 閾値未満なので割引なし
[[{ price: 5000, quantity: 1 }], true], // 閾値ちょうど→4950円
[[], true],
];
for (const [lines, member] of cases) {
assert.equal(calcTotal(lines, member), orderTotal(lines, member));
}
処理の骨格は同じで一部の手順だけが違うType-3の重複でも、共通部分を関数として抽出できる場合があります。手順の順番を親クラスに固定するならテンプレートメソッドパターン、違う部分を差し替え可能な部品にするならStrategyパターンが候補になります。リファクタリング全般の進め方はリファクタリングの観点とタイミングを参照してください。
重複を残すべきケースとRule of Three
重複をすべて消すのは正解ではありません。見た目が同じでも、変わる理由が違うコードを1つにまとめると、片方の仕様変更のたびに引数やフラグが増え、かえって読めない関数になります。Sandi Metzは2016年1月20日のブログ記事「The Wrong Abstraction」で、RailsConf 2014の講演で述べた「重複は誤った抽象化よりはるかに安い(duplication is far cheaper than the wrong abstraction)」を改めて解説しています。
いつまとめるかの目安として、Martin Fowlerの『Refactoring』で広まり、Don Robertsに由来するとされるRule of Three(三度目の法則)があります。似たコードが2回なら様子を見て、3回目に出てきたら抽出するという経験則です。3つの実例がそろうと、どこが共通でどこが違うかが見えやすくなり、誤った抽象化を避けられます。
次の条件に当てはまる重複は、残すほうが安全です。
- 仕様の変更理由が独立している(例:会員向け割引と、別の承認手続きで変更される法人契約向け割引)
- マイクロサービスやパッケージの境界をまたぐ。共通ライブラリ化すると、片方のリリースがもう片方を巻き込む
- テストコードの準備処理。テストごとに前提が読めることを優先し、多少の重複は許容する
- 自動生成コード。生成元を直すべきなので、検出ツールの対象から外す
判断に迷ったら「この2か所は、将来いつも同時に変わるか」を問います。同時に変わるなら同じ知識の重複なのでまとめ、別々に変わるなら偶然似ているだけなので残します。この判断軸はDRY原則と重複してよいコードの見分け方で詳しく扱っています。先回りの共通化を避ける考え方はYAGNI原則も参考になります。
AIコーディング時代の重複増加とCIでの新規クローン監視
AIコーディング支援が広まった期間の変化を測った調査に、GitClearの「AI Copilot Code Quality」2025年版があります。2020年1月から2024年12月に変更された2億1,100万行を分析し、変更行に占める「移動」(リファクタリングに伴う行)の割合が2021年の24.8%から2024年の9.5%に下がった一方、コピー&ペーストに分類される変更行の割合は2020年の8.3%から2024年の12.3%に上がったと報告しています。同レポートは、コピー&ペーストが移動を上回ったのは記録上初めてだとしています。
この集計だけではAIの寄与を他の要因と切り分けられませんが、調査対象ではコピー&ペーストの割合が実際に増えています。重複が増えやすい前提で、既存の重複は後回しにし、プルリクエストで新しく増えたクローンだけを止める運用が現実的です。jscpd 5.4.0の--baseline-from-refは、指定したブランチを同じ設定で検査し、そこに無いクローンを「new」として報告します。--fail-on-new-clonesを付けると、新規クローンがあるときだけ終了コード1になります。
# .github/workflows/duplication.yml(抜粋)
- uses: actions/checkout@v7
with:
fetch-depth: 0
- run: npx [email protected] src --baseline-from-ref origin/main --fail-on-new-clones
mainに2ファイル、featureブランチにコピーを1ファイル追加した構成で実行すると、featureでは「Found 1 clones (1 new)」と表示されて終了コード1、mainでは終了コード0でした。全体の重複率で止めたい場合は--threshold 10(重複率10%超で終了コード1)も使えます。新規クローンで止める方式は、既存の重複を抱えたままでも導入初日から機能します。人によるチェックと組み合わせるならコードレビューの観点と進め方に「既存の関数で代替できないか」を1項目加えると、AIが書いた重複を検出ツールより前に止められます。
よくある質問
コードクローンとクローンコードは同じ意味ですか?
同じ意味です。どちらも英語のcode cloneに由来し、重複コード(duplicate code)とも呼ばれます。学術論文では「コードクローン」、現場では「重複コード」「コピペコード」と呼ぶことが多い程度の違いです。
プログラミングでduplicateはどういう意味ですか?
「複製」「重複」の意味です。duplicate codeは同じ内容のコードが複数箇所にある状態を指します。データベースでは重複レコード、Gitやファイル操作では複製の意味でも使われます。
重複コードはどのくらいまで許容されますか?
絶対的な基準はありませんが、SonarQubeの既定の品質ゲートSonar wayは新規コードの重複率3.0%以下を条件にしています。全体の割合より、同時に変わるべき業務規則が複数箇所にあるかどうかを優先して確認してください。
DRY原則とはどう関係しますか?
DRY原則は「同じ知識をシステム内の1か所で表現する」という設計原則で、重複コードを減らす根拠になります。ただしDRYの対象は文字列の一致ではなく知識の重複です。見た目が似ているだけのコードまでまとめる必要はありません。
リファクタリングとは何ですか?
外から見た動作を変えずに、コードの内部構造を改善する作業です。重複コードの解消は代表的なリファクタリングで、関数の抽出やパラメータ化をテストで挙動を固定したうえで行います。