生成AIでリファクタリングするときは、「コードを説明させる → 現在の挙動を固定するテストを書かせる → 1種類ずつ小さく直させる → 差分を説明させる」の4段階に分け、毎回「公開インターフェースを変えない・挙動を変えない・1回1種類」の制約を付けるのが基本です。 リファクタリングは、外から見た動作を変えずに内部の構造を整える作業です。「このコードをきれいにして」の1行で頼むと、整理と一緒に動作まで変わることがあります。

この記事では、4段階それぞれにコピペできるプロンプトのテンプレートを用意しました。短い架空のコード(備品の利用料の計算)を Python と Java で用意し、直す前後のコードとテストの実行結果、動作が変わってしまった変更をテストが見つけたときの出力も載せています。最後に、変更の種類別の指示文一覧と、受け入れ前の確認表を付けました。

確認環境:Ubuntu 24.04、Python 3.11.15+pytest 9.1.1、OpenJDK 21.0.10+JUnit 4.13.2、git 2.43.0。掲載したコードとテストは、この環境で実行した結果を添えています。プロンプトは特定の AI 製品に依存しない書き方で、AI の出力例は載せていません。直したあとのコードは、手順に沿ってこの記事で用意した見本です。

4段階の早見表

段階 AIに渡すもの AIに出させるもの 人がすること 次へ進む条件
1. 説明させる 対象のコード、呼び出し元の情報 処理の流れ、入出力、分岐の一覧、気になる点 説明とコードを見比べる 分岐と境目を自分の言葉で言える
2. 挙動を固定する コード、段階1の分岐一覧 現在の挙動をそのまま確かめるテスト 直す前のコードでテストを実行する 全件が通る
3. 小さく直す コード、テスト、変更の種類を1つ 直したコード(1種類の変更だけ) テストを実行し、通ったらコミットする 全件が通る
4. 差分を説明させる 直す前後の差分 変更点の表、挙動が変わりうる箇所 差分を読み、説明と見比べる 説明と差分が合っている

毎回付ける制約は次の3つです。

制約 プロンプトに書く文 守られたかの確かめ方
公開インターフェースを変えない 「関数名・引数の名前と順番・戻り値の型・例外の種類を変えない」 差分で def や public の行が変わっていないか
挙動を変えない 「入力が同じなら出力も同じにする。不自然に見える挙動も直さず、気づいた点として報告する」 段階2のテストが全件通るか
1回1種類 「今回の変更は {改名} だけ。ほかに直したい点は提案として列挙し、コードには入れない」 差分が1つの種類で説明できるか

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「生成AIでリファクタリングするプロンプト集|レガシーコード改善の手順」

題材:架空の利用料計算のコード

題材は、社内の備品を借りたときの利用料を計算する関数です。変数名が1文字で、数値がそのまま書かれ、同じ計算が2か所に並んでいます。

# fee.py
def calc(t, h, m, d):
    r = 0
    if t == 1:
        if h <= 4:
            r = h * 300
        else:
            r = 4 * 300 + (h - 4) * 200
        if d == 6 or d == 7:
            r = r + r * 20 / 100
    elif t == 2:
        if h <= 4:
            r = h * 500
        else:
            r = 4 * 500 + (h - 4) * 350
        if d == 6 or d == 7:
            r = r + r * 20 / 100
    else:
        r = 0
    if m == "G":
        r = r - r * 10 / 100
    if r > 5000:
        r = 5000
    return int(r)

段階1:コードを説明させるプロンプト

最初に直させるのではなく、読ませて説明させます。目的は、分岐と境目の一覧を手に入れることです。これが段階2のテストの材料になります。

あなたは {Python 3.11} に詳しいエンジニアです。
下のコードを読み、動作を説明してください。コードの書き換えは行わないでください。

<code>
{対象のコードを貼る}
</code>

<context>
- 呼び出し元: {予約確定の処理から1か所で呼ばれている}
- 分かっていること: {t は備品の種別、h は利用時間、m は会員区分、d は曜日(1=月〜7=日)}
- 分からないこと: {上限 5000 の由来}
</context>

<output_format>
1. 概要(2〜3文)
2. 引数と戻り値(表:名前/型/意味/コードから読み取った根拠)
3. 分岐の一覧(表:条件/そのときの計算/境目になる値)
4. 計算の順番(割増し・割引き・上限などが、どの順に適用されるか)
5. 不自然に見える挙動(負の値、想定外の種別、大文字と小文字の違いなどの入力でどうなるか)
6. コードだけでは判断できないこと(質問として列挙)
</output_format>

<rules>
- コードに書かれていることだけを根拠にする。推測は「推測」と明記する。
- 改善案はこの段階では出さない。
</rules>

返ってきた説明は、コードと見比べて確かめます。この題材で押さえておきたい点は次のとおりです。

見る点 コードから読み取れること
種別 1 と 2 だけに計算がある。それ以外は 0
時間の境目 4 以下と 4 を超える場合で単価が変わる
曜日 6 と 7 のときに2割増し
会員区分 "G" のときに1割引き。小文字の "g" は割引きなし
順番 割増し → 割引き → 上限 5000 の順
不自然に見える点 時間が負の値でも計算され、負の利用料が返る

段階2:現在の挙動を固定するテストを書かせるプロンプト

次に、いまのコードがどう動くかをそのまま記録するテストを用意します。仕様として正しいかどうかではなく、「直す前と同じか」を確かめるためのテストです。

下のコードの「現在の挙動」をそのまま固定するテストを {pytest} で書いてください。
このあとリファクタリングを行うので、動作が変わったことに気づけるようにするのが目的です。

<code>
{対象のコードを貼る}
</code>

<branches>
{段階1で得た分岐の一覧と、不自然に見える挙動の一覧を貼る}
</branches>

<rules>
- 対象のコードは変更しない。
- 公開されている関数({calc})だけを呼ぶ。内部の変数や実装には触れない。
- 分岐ごとに、境目のちょうど・1つ手前・1つ先を入れる。
- 計算の順番で結果が変わる入力({割引きと上限が両方かかる場合})を必ず入れる。
- 不自然に見える挙動も、いまの結果をそのまま期待値にする。直さない。
- 期待値はコードを読んで計算し、確信が持てないものは「要確認」とコメントを付ける。
- 入力と期待値を表の形({pytest.mark.parametrize})で並べる。
</rules>

<output_format>
1. テストコード(コードブロック1つ)
2. 入れたケースの一覧(表:ねらい/入力/期待値)
3. 入れられなかった観点と、その理由
</output_format>

期待値は AI が計算したものなので、直す前のコードで実行して全件が通ることを必ず確かめます。通らないケースは、期待値のほうを実際の結果に合わせます(コードは直しません)。

用意したテストです。

# test_fee.py
import pytest

from fee import calc


@pytest.mark.parametrize(
    "t, h, m, d, expected",
    [
        # 種別1:4時間までと、それを超えた分
        (1, 1, "N", 1, 300),
        (1, 4, "N", 1, 1200),
        (1, 5, "N", 1, 1400),
        # 種別2
        (2, 1, "N", 1, 500),
        (2, 4, "N", 1, 2000),
        (2, 5, "N", 1, 2350),
        # 曜日:5は平日扱い、6と7は2割増し
        (1, 5, "N", 5, 1400),
        (1, 5, "N", 6, 1680),
        (2, 5, "N", 7, 2820),
        # 会員区分 G は1割引き(割増しのあとに引く)
        (1, 5, "G", 1, 1260),
        (1, 5, "G", 6, 1512),
        (1, 5, "g", 1, 1400),
        # 上限 5000(割引きのあとに判定する)
        (2, 12, "N", 1, 4800),
        (2, 13, "N", 1, 5000),
        (2, 12, "N", 6, 5000),
        (2, 12, "G", 6, 5000),
        (2, 14, "G", 1, 4950),
        # いまの挙動をそのまま固定するもの
        (3, 5, "N", 1, 0),
        (1, 0, "N", 1, 0),
        (1, -1, "N", 1, -300),
    ],
)
def test_calc_current_behavior(t, h, m, d, expected):
    assert calc(t, h, m, d) == expected


def test_calc_returns_int():
    assert type(calc(1, 5, "G", 6)) is int

直す前のコードで実行した結果です。

python3 -m pytest -q
.....................                                                    [100%]
21 passed in 0.02s

(1, -1, "N", 1, -300) のように、負の時間で負の利用料が返る挙動も、そのまま期待値にしています。直したくなる挙動を見つけても、リファクタリングとは別の変更として扱うためです。テストが通る状態をコミットしておくと、あとの段階でいつでもここへ戻れます。

段階3:1種類ずつ小さく直させるプロンプト

テストが通る状態から、変更の種類を1つだけ指定して直させます。

下のコードをリファクタリングしてください。今回行う変更は1種類だけです。

<change>
種類: {マジックナンバーを定数にする/変数の改名}
対象: {calc 関数の中の 4、20、10、5000 と、変数 r}
</change>

<code>
{現在のコードを貼る}
</code>

<tests>
{段階2のテストを貼る。全件が通っている状態}
</tests>

<constraints>
- 公開インターフェースを変えない(関数名 {calc}、引数の名前と順番、戻り値の型、例外の種類)。
- 挙動を変えない。入力が同じなら出力も同じにする。計算の順番を入れ替えない。
- <change> に書いた種類以外の変更はしない。ほかに直したい点は「次の候補」として列挙するだけにする。
- テストコードは変更しない。
- 新しいライブラリを追加しない。
</constraints>

<output_format>
1. 変更後のコード(ファイル全体をコードブロック1つで)
2. 変更点の一覧(表:変更前/変更後/理由)
3. 挙動が変わる可能性がある箇所(なければ「なし」)
4. 次の候補(今回は入れなかった改善。1行ずつ)
</output_format>

1回目:定数にする・改名する

数値に名前を付け、変数 r を fee に改名しました。構造はそのままです。

WEEKEND_DAYS = (6, 7)
WEEKEND_SURCHARGE_PERCENT = 20
MEMBER_DISCOUNT_PERCENT = 10
MAX_FEE = 5000
BASE_HOURS = 4


def calc(t, h, m, d):
    fee = 0
    if t == 1:
        if h <= BASE_HOURS:
            fee = h * 300
        else:
            fee = BASE_HOURS * 300 + (h - BASE_HOURS) * 200
        if d in WEEKEND_DAYS:
            fee = fee + fee * WEEKEND_SURCHARGE_PERCENT / 100
    elif t == 2:
…(省略。種別2も同じ形)…
    if m == "G":
        fee = fee - fee * MEMBER_DISCOUNT_PERCENT / 100
    if fee > MAX_FEE:
        fee = MAX_FEE
    return int(fee)
21 passed in 0.02s

通ったので、ここでコミットします。

2回目:重複した計算を関数に切り出す

種別ごとに並んでいた同じ形の計算を、_base_fee にまとめました。公開されている calc の引数はそのままです。

# fee.py(直したあと)
WEEKEND_DAYS = (6, 7)
WEEKEND_SURCHARGE_PERCENT = 20
MEMBER_DISCOUNT_PERCENT = 10
MAX_FEE = 5000
BASE_HOURS = 4

# 種別ごとの単価:(4時間までの1時間あたり, 4時間を超えた分の1時間あたり)
RATES = {
    1: (300, 200),
    2: (500, 350),
}


def _base_fee(item_type, hours):
    if item_type not in RATES:
        return 0
    base_rate, extra_rate = RATES[item_type]
    if hours <= BASE_HOURS:
        return hours * base_rate
    return BASE_HOURS * base_rate + (hours - BASE_HOURS) * extra_rate


def calc(t, h, m, d):
    fee = _base_fee(t, h)
    if d in WEEKEND_DAYS:
        fee = fee + fee * WEEKEND_SURCHARGE_PERCENT / 100
    if m == "G":
        fee = fee - fee * MEMBER_DISCOUNT_PERCENT / 100
    if fee > MAX_FEE:
        fee = MAX_FEE
    return int(fee)
21 passed in 0.02s

コミットの履歴は、変更の種類ごとに1つずつになります。

git log --oneline
304a70b 種別ごとの重複した計算を _base_fee に切り出し
ca4beaf マジックナンバーを定数にし、変数 r を fee に改名
d681bc1 現在の挙動を固定するテストを追加

calc(t, h, m, d) の引数名を分かりやすくしたくなりますが、引数名は公開インターフェースの一部です(Python ではキーワード引数で呼ばれている可能性があります)。呼び出し元の修正を伴う変更は、別の作業として分けます。

動作が変わる変更をテストが見つけた例

3回目として「上限の判定を min で1行にする」変更を入れたところ、行の位置が割引きの前に移っていました。差分は次のとおりです。

@@ -24,8 +24,7 @@ def calc(t, h, m, d):
     fee = _base_fee(t, h)
     if d in WEEKEND_DAYS:
         fee = fee + fee * WEEKEND_SURCHARGE_PERCENT / 100
+    fee = min(fee, MAX_FEE)
     if m == "G":
         fee = fee - fee * MEMBER_DISCOUNT_PERCENT / 100
-    if fee > MAX_FEE:
-        fee = MAX_FEE
     return int(fee)

見た目は整理されていますが、テストを実行すると2件が止まります(出力は末尾の抜粋)。

python3 -m pytest -q --tb=line
E   AssertionError: assert 4500 == 4950
     +  where 4500 = calc(2, 14, 'G', 1)
=========================== short test summary info ============================
FAILED test_fee.py::test_calc_current_behavior[2-12-G-6-5000] - AssertionErro...
FAILED test_fee.py::test_calc_current_behavior[2-14-G-1-4950] - AssertionErro...
2 failed, 19 passed in 0.02s

元のコードは「割引きのあとに上限」でしたが、変更後は「上限のあとに割引き」になり、5000 を超える場合の結果が変わっています。段階2で計算の順番で結果が変わる入力を入れておいたので、ここで気づけました。

テストが落ちたときのプロンプト

テストが落ちたときは、テストを書き換えずに、コードを元の挙動へ戻す方向で頼みます。

直前のリファクタリングのあと、テストが {2件} 落ちました。
テストは「直す前の挙動」を固定したものなので、テストは変更せず、コードを直してください。

<diff>
{直前の変更の差分を貼る}
</diff>

<test_output>
{テストの出力を全文貼る}
</test_output>

<rules>
- まず、どの変更が挙動を変えたのかを1〜2文で説明する。
- 元の挙動に戻す最小の修正を示す。ついでの改善は入れない。
- 修正が難しい場合は、この変更を取り消すことを提案する。
- 「テストの期待値が誤っている」と判断した場合は、コードもテストも変えずに、理由を報告する。
</rules>

<output_format>
1. 原因(1〜2文)
2. 修正後のコード
3. 同じ種類の見落としを防ぐために足すとよいテストケース
</output_format>

対応は次の表で選びます。

状況 対応
落ちた理由がすぐ分かり、修正が小さい 上のプロンプトで直させ、テストを再実行する
2回直しても通らない git checkout -- ファイル名 で直前のコミットに戻し、変更をもっと小さく分けて頼み直す
落ちた挙動のほうが「正しい仕様」に見える リファクタリングとは切り離す。仕様を決められる人に確かめ、別の変更として扱う
テストの期待値が誤っていた 直す前のコードで実行して結果を確かめ、期待値を実際の結果に合わせる

段階4:差分を説明させるプロンプト

テストが通ったら、差分を説明させて、自分で読んだ内容と突き合わせます。テストに含まれていない入力で挙動が変わっていないかを見つけるための段階です。

差分は git で取り出します。

git diff HEAD~2 HEAD            # 2つ前のコミットからの差分
git diff --stat HEAD~2 HEAD     # 変更されたファイルと行数
 fee.py | 50 +++++++++++++++++++++++++++++---------------------
 1 file changed, 29 insertions(+), 21 deletions(-)
下の差分は、動作を変えないことを目的としたリファクタリングです。
レビューする立場で、差分を説明してください。

<diff>
{git diff の出力を貼る}
</diff>

<tests>
{段階2のテストを貼る}
</tests>

<output_format>
1. 変更の要約(3行以内)
2. 変更点の一覧(表:箇所/変更の種類/変更前/変更後)
3. 挙動が変わりうる箇所(表:箇所/どんな入力で変わるか/テストに含まれているか)
   - 計算や判定の順番、型(整数と小数)、境目(以下と未満)、None や空の扱い、例外の種類、
     副作用(ログ、ファイル、DB)の順番の観点で確かめる。
4. 公開インターフェースの変更の有無(関数名、引数、戻り値、例外)
5. テストに足すとよいケース
</output_format>

<rules>
- 差分に書かれていることだけを根拠にする。
- 「変わらない」と言い切れない箇所は、変わりうる箇所として挙げる。
</rules>

「挙動が変わりうる箇所」に挙がった入力は、直す前後のコードで実際に呼んで比べます。テストに含まれていなければ、テストに足します。

Java での例

同じ計算を Java で書いたものです。直す前と、同じ方針で直し終えた形を載せます。

// Fee.java(直す前)
public class Fee {
    public static int calc(int t, int h, String m, int d) {
        int r = 0;
        if (t == 1) {
            if (h <= 4) {
                r = h * 300;
            } else {
                r = 4 * 300 + (h - 4) * 200;
            }
            if (d == 6 || d == 7) {
                r = r + r * 20 / 100;
            }
        } else if (t == 2) {
            if (h <= 4) {
                r = h * 500;
            } else {
                r = 4 * 500 + (h - 4) * 350;
            }
            if (d == 6 || d == 7) {
                r = r + r * 20 / 100;
            }
        } else {
            r = 0;
        }
        if ("G".equals(m)) {
            r = r - r * 10 / 100;
        }
        if (r > 5000) {
            r = 5000;
        }
        return r;
    }
}

現在の挙動を固定するテストです(JUnit 4)。Python と同じ20件に加え、会員区分が null のときの挙動も固定しています。

// FeeTest.java
import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class FeeTest {
    // {種別, 時間, 会員区分, 曜日, 期待値}
    private static final Object[][] CASES = {
        {1, 1, "N", 1, 300}, {1, 4, "N", 1, 1200}, {1, 5, "N", 1, 1400},
        {2, 1, "N", 1, 500}, {2, 4, "N", 1, 2000}, {2, 5, "N", 1, 2350},
        {1, 5, "N", 5, 1400}, {1, 5, "N", 6, 1680}, {2, 5, "N", 7, 2820},
        {1, 5, "G", 1, 1260}, {1, 5, "G", 6, 1512}, {1, 5, "g", 1, 1400},
        {2, 12, "N", 1, 4800}, {2, 13, "N", 1, 5000}, {2, 12, "N", 6, 5000},
        {2, 12, "G", 6, 5000}, {2, 14, "G", 1, 4950},
        {3, 5, "N", 1, 0}, {1, 0, "N", 1, 0}, {1, -1, "N", 1, -300},
    };

    @Test
    public void currentBehavior() {
        for (Object[] c : CASES) {
            int actual = Fee.calc((int) c[0], (int) c[1], (String) c[2], (int) c[3]);
            String label = "calc(" + c[0] + ", " + c[1] + ", " + c[2] + ", " + c[3] + ")";
            assertEquals(label, (int) c[4], actual);
        }
    }

    @Test
    public void nullMemberIsTreatedAsNoDiscount() {
        assertEquals(1400, Fee.calc(1, 5, null, 1));
    }
}
javac -cp junit-4.13.2.jar:hamcrest-core-1.3.jar:. Fee.java FeeTest.java
java -cp junit-4.13.2.jar:hamcrest-core-1.3.jar:. org.junit.runner.JUnitCore FeeTest
JUnit version 4.13.2
..
Time: 0.017
OK (2 tests)

直したあとのコードです。public static int calc(int t, int h, String m, int d) の形は変えていません。

// Fee.java(直したあと)
public class Fee {
    private static final int BASE_HOURS = 4;
    private static final int WEEKEND_SURCHARGE_PERCENT = 20;
    private static final int MEMBER_DISCOUNT_PERCENT = 10;
    private static final int MAX_FEE = 5000;

    public static int calc(int t, int h, String m, int d) {
        int fee = baseFee(t, h);
        if (isWeekend(d)) {
            fee = fee + fee * WEEKEND_SURCHARGE_PERCENT / 100;
        }
        if ("G".equals(m)) {
            fee = fee - fee * MEMBER_DISCOUNT_PERCENT / 100;
        }
        return Math.min(fee, MAX_FEE);
    }

    private static int baseFee(int itemType, int hours) {
        return switch (itemType) {
            case 1 -> tiered(hours, 300, 200);
            case 2 -> tiered(hours, 500, 350);
            default -> 0;
        };
    }

    private static int tiered(int hours, int baseRate, int extraRate) {
        if (hours <= BASE_HOURS) {
            return hours * baseRate;
        }
        return BASE_HOURS * baseRate + (hours - BASE_HOURS) * extraRate;
    }

    private static boolean isWeekend(int dayOfWeek) {
        return dayOfWeek == 6 || dayOfWeek == 7;
    }
}
JUnit version 4.13.2
..
Time: 0.045
OK (2 tests)

Java でも、上限の判定を割引きの前へ移すと、テストが次のように止まりました。ラベルに入力を入れておいたので、どの入力で変わったかが1行で分かります。

JUnit version 4.13.2
..E
Time: 0.027
There was 1 failure:
1) currentBehavior(FeeTest)
java.lang.AssertionError: calc(2, 12, G, 6) expected:<5000> but was:<4500>

変更の種類別の指示文一覧

段階3の <change> に入れる指示文です。1回に1行だけ選びます。上から順に、動作が変わりにくい変更です。

変更の種類 指示文 挙動が変わりやすい点
変数・非公開関数の改名 「{関数名} の中のローカル変数を、役割が分かる名前に改名する。公開されている名前は変えない」 同じ名前の別の変数との取り違え
マジックナンバーの定数化 「数値と文字列の直書きを、意味の分かる名前の定数にする。値は変えない」 同じ値でも意味が違う数値を1つの定数にまとめてしまう
使われていないコードの削除 「どこからも呼ばれていない関数と、使われていない変数・import を削除する。削除したものを一覧にする」 文字列で名前を指定して呼ぶ箇所、外部からの呼び出し
関数の切り出し 「{処理の説明} の部分を非公開の関数に切り出す。計算の順番は変えない」 途中で書き換えていた変数の受け渡し
重複の統合 「{A} と {B} の同じ形の処理を1つにまとめる。違いは引数にする」 似ているが少しだけ違う処理を同じものとして扱う
条件分岐の整理(早期リターン) 「入れ子の if を、条件を満たさない場合に先に return する形にする。判定の順番は変えない」 判定の順番、else に書かれていた処理の抜け
条件式に名前を付ける 「複雑な条件式を、意味の分かる名前の変数か関数にする。式は変えない」 以上と超える、かつ・または の書き換え
ループの書き換え 「for ループを {内包表記/Stream} に書き換える。要素の順番と、途中で止まる条件は変えない」 順番、空のとき、途中で例外が出たときの残りの処理
型の注釈を足す 「引数と戻り値に型の注釈を足す。実行時の動作は変えない」 None を許すかどうかの判断
ファイル・クラスの分割 「{責務} に関する関数を別のファイルに移す。元の場所からも import できるようにする」 import の循環、初期化の順番

同じ表のタブ区切りです。

変更の種類   指示文 挙動が変わりやすい点
変数・非公開関数の改名 {関数名} の中のローカル変数を、役割が分かる名前に改名する。公開されている名前は変えない   同じ名前の別の変数との取り違え
マジックナンバーの定数化    数値と文字列の直書きを、意味の分かる名前の定数にする。値は変えない   同じ値でも意味が違う数値を1つの定数にまとめてしまう
使われていないコードの削除   どこからも呼ばれていない関数と、使われていない変数・import を削除する。削除したものを一覧にする 文字列で名前を指定して呼ぶ箇所、外部からの呼び出し
関数の切り出し {処理の説明} の部分を非公開の関数に切り出す。計算の順番は変えない  途中で書き換えていた変数の受け渡し
重複の統合   {A} と {B} の同じ形の処理を1つにまとめる。違いは引数にする  似ているが少しだけ違う処理を同じものとして扱う
条件分岐の整理(早期リターン) 入れ子の if を、条件を満たさない場合に先に return する形にする。判定の順番は変えない    判定の順番、else に書かれていた処理の抜け
条件式に名前を付ける  複雑な条件式を、意味の分かる名前の変数か関数にする。式は変えない    以上と超える、かつ・または の書き換え
ループの書き換え    for ループを {内包表記/Stream} に書き換える。要素の順番と、途中で止まる条件は変えない  順番、空のとき、途中で例外が出たときの残りの処理
型の注釈を足す 引数と戻り値に型の注釈を足す。実行時の動作は変えない  None を許すかどうかの判断
ファイル・クラスの分割 {責務} に関する関数を別のファイルに移す。元の場所からも import できるようにする   import の循環、初期化の順番

受け入れ前の確認表

AI が直したコードを取り込む前に、次の表で確かめます。

番号 確認すること 確かめ方
1 直す前のコードで、挙動を固定するテストが全件通っていた 直す前のコミットでテストを実行した記録
2 直したあとのコードで、同じテストが全件通る テストを実行する
3 テストコードが変更されていない git diff --stat にテストのファイルが出ていない
4 公開されている関数名・引数・戻り値の型・例外が変わっていない 差分の def、public の行を見る
5 変更が1種類で説明できる コミットのメッセージを1文で書けるか
6 計算と判定の順番が変わっていない 差分で行の移動を見る。段階4の説明と見比べる
7 新しいライブラリや設定が増えていない import と依存の定義の差分を見る
8 削除されたコードが、ほかから呼ばれていない 名前で全体を検索する
9 ログ・ファイル・DB への書き込みの順番と内容が変わっていない 差分を読む。必要ならテストに足す
10 見つけた不自然な挙動を、別の課題として記録した 課題の一覧に1行ずつ書く
11 差分を自分で読み、1行ずつ説明できる 説明できない行は AI に質問するか、取り込まない
番号  確認すること  確かめ方    結果(記入欄)
1   直す前のコードで、挙動を固定するテストが全件通っていた 直す前のコミットでテストを実行した記録 
2   直したあとのコードで、同じテストが全件通る   テストを実行する    
3   テストコードが変更されていない git diff --stat にテストのファイルが出ていない 
4   公開されている関数名・引数・戻り値の型・例外が変わっていない  差分の def、public の行を見る    
5   変更が1種類で説明できる    コミットのメッセージを1文で書けるか  
6   計算と判定の順番が変わっていない    差分で行の移動を見る。段階4の説明と見比べる  
7   新しいライブラリや設定が増えていない  import と依存の定義の差分を見る 
8   削除されたコードが、ほかから呼ばれていない   名前で全体を検索する  
9   ログ・ファイル・DB への書き込みの順番と内容が変わっていない 差分を読む。必要ならテストに足す    
10  見つけた不自然な挙動を、別の課題として記録した 課題の一覧に1行ずつ書く    
11  差分を自分で読み、1行ずつ説明できる  説明できない行は AI に質問するか、取り込まない   

よくある質問

Q. テストがまったくないコードでも、この手順は使えますか? A. 使えます。段階2がそのための工程です。関数の入力と出力だけで確かめられる部分から始め、DB やファイルに触れる部分は、まず計算だけを行う関数に切り出せるかを段階1で AI に尋ねると進めやすくなります。

Q. 長いファイルを丸ごと渡してもよいですか? A. 段階1の説明は丸ごと渡して構いません。段階3で直させるときは、関数1つか、関係する数個の関数に絞ります。範囲が広いと差分が大きくなり、段階4で読み切れなくなります。

Q. 直している途中でバグらしい挙動を見つけたら、どうしますか? A. その場では直さず、挙動を固定するテストにそのまま残し、別の課題として記録します。リファクタリングと動作の変更を同じ差分に入れると、テストが落ちたときに、意図した変更なのか取り違えなのかを見分けられなくなります。

Q. 「1回1種類」はどのくらいの大きさが目安ですか? A. コミットのメッセージを1文で書けて、差分を数分で読み切れる大きさです。「改名と関数の切り出し」のように「と」でつながる場合は、2回に分けます。

まとめ

  • リファクタリングは「説明させる → 挙動を固定するテスト → 1種類ずつ直す → 差分を説明させる」の4段階で頼む
  • 毎回、公開インターフェースを変えない・挙動を変えない・1回1種類の制約を書く
  • テストは直す前のコードで実行して全件通ることを確かめ、不自然に見える挙動もそのまま固定する
  • 計算の順番で結果が変わる入力をテストに入れておくと、行の移動による変化に気づける
  • テストが落ちたら、テストは変えずにコードを戻す。2回直して通らなければコミットへ戻って分け直す
  • テストが通ったあとも、差分を説明させて自分の読みと突き合わせる
  • 取り込む前に確認表で、テスト・インターフェース・順番・依存・削除したコードを確かめる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「生成AIでリファクタリングするプロンプト集|レガシーコード改善の手順」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?