創刊準備号 · 2026年秋English · RSS
AgentKoji

数千行をリファクタリングで消す

Claude Codeのリファクタリングで古いモジュールを消すなら、呼び出し側から順に。依存の向きの調べ方とコミットの切り方をまとめた。

古い仕組みを丸ごと消すリファクタは、足すリファクタより怖いものです。エージェントに「この古いモジュールを消して」と頼むと、消すこと自体はすぐ終わります。困るのは、どこから消したかで途中の状態が壊れ、1つの大きなコミットに全部が混ざることです。この記事では、数千行規模の削除を、壊さずに小さく進める順番をまとめます。手順は、手元のダミーのリポジトリで動かして確かめました(Python 3.13.5、git 2.47.3、2026-10-16)。再現は数行のダミーのコードで行ったもので、数千行の規模では測っていません。

消す前に、依存の向きを出す

最初にやるのは、消したいものが「誰に使われ、誰を使っているか」の一覧を作ることです。消す対象の中にも上下関係があります。

手元のダミーの構成はこうです。古い整形モジュール(legacy_fmt)が、さらに古い下請けのモジュール(legacy_core)を使い、その整形モジュールを2つの画面相当のモジュールが呼んでいます。置き換え先の新しいモジュールは、すでにあります。

import 文から依存を抜き出し、Python 標準の graphlib で並べました。

== 依存グラフ(import から抽出)と削除順
legacy を使う側: ['invoice', 'report']
消す順(consumer→provider): 呼び出し側の置き換え → legacy_fmt → legacy_core

消す順は、使う側(consumer)から、使われる側(provider)へ。依存は一方向に流れているので、上流から外していけば、どの時点でもコードは閉じた状態を保てます。

依存の一覧は、エージェントに作らせてかまいません。ただし「調べた結果」をそのまま信じず、grep やスクリプトで同じ一覧を出し直して照らします。import の grep で拾えない使われ方もあるので、その点は後で触れます。

逆から消すと、途中が壊れる

エージェントは、目立つもの、つまり一番下の古い部品から消しにいきがちです。手元で、下請けのモジュールから先に消してテストを回しました。

== 失敗例: provider から先に消す
FAILED (errors=1)

使う側が残っているので、import の時点で落ちます。小さな例では直せば済みます。規模が大きいとエラーが多くの箇所に散りやすく、エージェントはそれを一つずつ「修正」し始めます(編集部の経験で、ここでは測っていません)。その修正が、消すはずだったコードを別の場所に書き写すことだったりします。

1ステップ1コミットで進める

正しい順番で、1ステップごとにテストを回し、コミットを切りました。

== 手順1: 呼び出し側を new_fmt に置き換え
OK
== 手順2: legacy_fmt を削除
OK
== 手順3: legacy_core を削除
OK
28c1d90 step3
 1 file changed, 2 deletions(-)
9648f75 step2
 1 file changed, 3 deletions(-)
9bd2477 step1
 2 files changed, 2 insertions(+), 2 deletions(-)

最初のコミットは置き換えだけ、後のコミットは削除だけ、という形になります。どのコミットでもテストが通るので、途中で問題が見つかっても、そのコミットだけを戻せます。レビューする側も、「置き換えが正しいか」と「消したものが本当に使われていないか」を、別々に見られます。

公式のドキュメントも、リファクタは小さく、テストできる単位で進めるよう勧めています(出典:https://code.claude.com/docs/en/common-workflows 、2026-10-16確認)。

並列に分けるときの注意

Claude Code には、大きな変更を並列で進める /batch があります。公式の説明では、コードベースを調べて作業を5〜30の独立した単位に分け、計画を示し、承認されると単位ごとに分離された worktree でバックグラウンドのサブエージェントを走らせ、それぞれがテストを回して変更を出します(出典:https://code.claude.com/docs/en/commands 、2026-10-16確認)。

ここで大事なのは「独立した単位」という条件です。削除のリファクタは、上で見たように順番に依存しています。使う側の置き換えが終わる前に、使われる側の削除が別の単位で走ると、それぞれの worktree では緑でも、合わせた途端に壊れます。並列にするなら、同じ段(たとえば「呼び出し側の置き換え」)の中だけを分けます。

エージェントへの頼み方

編集部では、削除のリファクタを次の文面で頼んでいます。

次のモジュールを削除する。いきなり消さないこと。
- まず、削除対象を使っている箇所と、削除対象どうしの依存の一覧を作る。
- 消す順は、使う側から使われる側へ。
- 1ステップごとにテストを回し、1コミットにする。置き換えと削除は別のコミットにする。
- テストが落ちたら、その場で別の場所に書き写して直さない。止めて報告する。

最後の行は、上で書いた「書き写して直す」を防ぐためのものです。

削った量は、始点からの差分で数える

「何行消えたか」は、作業の始点のコミットと比べて数えます。手元のダミーのリポジトリで、3つのステップをまとめて見ました。

$ git diff --shortstat HEAD~3 HEAD
 4 files changed, 2 insertions(+), 7 deletions(-)
$ git diff --name-status HEAD~3 HEAD
M	app/invoice.py
D	app/legacy_core.py
D	app/legacy_fmt.py
M	app/report.py

D が付いたファイルは丸ごと消え、M は書き換えです。削除のリファクタで A(追加)が増えていたら、消したはずのコードがどこかに書き写されていないかを疑います。行数だけでなく、この内訳まで見ると、エージェントの報告と食い違ったときにすぐ気づけます。

消したあとに確かめること

テストが全部通っても、削除が正しかった証拠にはなりません。テストが、消したものを最初から検査していないことがあるからです。

まず、消した名前がどこにも残っていないかを、コード全体で grep します。次に、import の grep だけで「未使用」と決めないことです。CSS やビルド設定、package.json の scripts からしか呼ばれていないものもあります。その確かめ方と、テストが全部通ったのに誤削除だった例は「緑のテストは何も証明しない」に書きました。

もう一つ、エージェントの「削除が完了しました」も、そのまま信じません。削った行数やコミットの数は、報告の文ではなく git log --shortstat のような実数で確かめます。報告と実数を機械で突き合わせる仕組みは「AIの「完了しました」を信じない仕組み」にまとめました。

まとめ