Claude Media
Claude Codeで巨大関数を分割する — 抽出単位の決め方と1コミット1抽出

Claude Codeで巨大関数を分割する — 抽出単位の決め方と1コミット1抽出

数千行の関数をClaude Codeで分割する手順です。radonで対象を選び、副作用の境界と引数の束で抽出単位を決め、1コミット1抽出で進めます。

巨大関数の分割で人が決めるべきなのは「どこで切るか」です。Claude Codeには、決めた1ブロックを抜き出してテストを回す作業を、1回ずつ頼みます。切る位置の基準は2つです。副作用がどこで起きるかと、そのブロックが外側の変数をいくつ必要とするか(引数の束)です。

全体の進め方(特性テストの作り方、読ませる範囲の絞り方)はClaude Codeリファクタリングを安全に進める手順にあります。この記事は、その手順のうち「1つの関数をどう割るか」だけを掘り下げます。Python向けの複雑度ツールradonで対象を選び、抽出ごとにテストとコミットを挟む流れです。

分割する関数はradonの複雑度で選ぶ

数千行のファイルには関数が山ほどあり、全部に手を付けると終わりません。最初に「分岐が最も重い関数」を機械的に拾います。

radonはPythonのコードメトリクスを計算するツールで、サイクロマティック複雑度(McCabe数)も出せます。複雑度は、ブロック内の分岐の数に1を足した値です。radon cc はこれをA(良好)からF(最悪)までのランクにして並べます。

pip install radon
radon cc -s -o SCORE -n C src/

-s で点数を表示し、-o SCORE で複雑度の降順に並べ、-n C でCランク未満を隠します。ランクと点数の対応は次のとおりです。

数字

radon ccのランク境界

  • A

    1〜5

    低い・単純なブロック

  • C

    11〜20

    やや複雑

  • E

    31〜40

    複雑で警戒域

  • F

    41以上

    エラーを生みやすい

radon公式ドキュメントのcc節より

手元で動かした出力を載せます。radon 6.0.1で、分岐を詰め込んだ模擬の注文処理関数(約26行)を解析したものです。

src/billing.py
    F 1:0 process_order - C (13)
    F 28:0 helper - A (1)

F は「関数」の種別で、ランクのFとは別の記号です。続く 1:0 が行番号と桁、C (13) がランクと点数です。種別の記号はF(関数)、M(メソッド)、C(クラス)の3つで、ランクの文字と同じ字面になるため、慣れるまで読み違えます。

複雑度が低い巨大関数は拾えない

数千行あっても、分岐が少なく手続きを順に並べただけの関数は複雑度が低く出ます。その場合は -o LINES で行番号順に並べたうえで、行数の大きい関数を目で拾います。複雑度は「割る価値」の目安であって、長さの指標ではありません。

radonはPython用です。他の言語は、同じ種類の指標を出す別のツールで代用します。以降の抽出基準(副作用と引数の束)は、言語を問わず使えます。

Claudeに対象を選ばせるなら、JSONで渡すのが扱いやすい形です。

radon cc -j -n C src/ > cc.json

出力には rank、complexity、lineno、endline、name が含まれます。このJSONを渡して「複雑度の高い順に3件、行範囲と名前を表にして」と頼めば、着手順の叩き台になります。

抽出単位は「副作用の境界」と「引数の束」で決める

対象が決まったら、関数の中身をブロックに分けます。ここがこの手順の肝で、Claudeに任せきりにしない部分です。見出しや空行で区切られたブロックを、次の3列で仕分けます。

  • 外側から読む変数(抽出後は引数になる)
  • ブロック内で書き換え、後続で使う変数(抽出後は戻り値になる)
  • 外部への副作用(DB書き込み、メール送信、ファイル出力、ログ)の有無

この仕分けを、コードを触る前にClaudeへ出させます。plan modeなら、承認するまで何も編集されません。

claude --permission-mode plan
src/billing.py の process_order を、意味の切れ目ごとのブロックに分けてください。
各ブロックについて「読む外側の変数」「書いて後続で使う変数」
「DB・メール・ファイル等の副作用の有無」を表にしてください。
コードはまだ変えないでください。

副作用のないブロックから抜く

模擬関数のブロックを仕分けると、次のようになります。

ブロック読む変数後続へ渡す値副作用抜く順
明細1行の金額計算(プロモ割引)読む変数item、単価後続へ渡す値行の金額副作用なし(単価の取得は呼び出し側)抜く順1番目
クーポン適用読む変数total、coupon後続へ渡す値割引後の合計副作用なし抜く順2番目
税率の適用読む変数total、country後続へ渡す値税込み合計副作用なし抜く順3番目
保存とメール送信読む変数合計、注文ID、メール後続へ渡す値なし副作用DB書き込み、メール送信抜く順抜かない

判断の軸は単純です。副作用のないブロックは入力と出力だけで決まるため、単体でテストでき、順序の入れ替えにも強い。逆に、DB書き込みやメール送信を含むブロックを先に抜くと、テストに偽物(モック)が要り、処理順の前提も崩れやすくなります。保存と通知は、元の関数に「最後の数行」として残します。残った関数は、計算を組み合わせて外へ出す指揮役になります。

ループは「中身を抜き、ループ自体は残す」のが無難です。ループごと抜くと、途中で continue や break が外側の制御に影響する場合に、戻り値の設計が一気に複雑になります。

引数が4つを超えたら、切り方を疑う

ブロックが読む外側の変数を数えます。1〜3個なら、そのまま引数にします。4個以上になるなら、次のどちらかです。

  • 関連する値をまとめる(dataclassや辞書にして、1つの引数として渡す)
  • 切り方が悪い(2つの責務が1ブロックに混ざっている)ので、さらに割る

戻り値が複数になる場合も同様です。「書き換えて後続で使う変数が2つ以上」あるブロックは、そもそも1つの責務ではないことが多く、抽出する前に切り直します。数千行の関数では、この「引数の束」が太いブロックほど、実は別の関数が絡み合っているサインです。

引数の束の太さを測っておくと、着手順が決まります。束が細い(引数1〜2個、戻り値1個)ブロックから抜き、太いブロックは周囲を抜いたあとに、細くなった姿を見て判断します。

1コミット1抽出で進める

抽出は1回の依頼で1ブロックだけにします。公式のワークフロー集も、リファクタリングは「小さく、テスト可能な増分」で行うよう勧めています。1コミット1抽出にすると、失敗したときの戻し先が1つに決まります。

手順

抽出1回あたりの流れ

  1. 1

    抽出前にテストを通しておく

    特性テスト(いまの出力を固定するテスト)が緑であることを確認します。作り方はリファクタリング全体の手順の手順2が詳しいので、ここでは省きます。

  2. 2

    1ブロックだけ抽出させる

    仕分け表の「抜く順」どおりに、1つだけ頼みます。シグネチャは呼び出し元を変えない形にします。

  3. 3

    テストと複雑度を測る

    テストを回し、radon cc -s で数字の動きを見ます。

  4. 4

    緑になった時点でコミットする

    区切りのよさではなく、検証が通った時点が境界です。

1回の依頼は、たとえば次の形です。

process_order の税率を適用する部分だけを apply_tax(total, country) として
同じファイルに抽出してください。process_order のシグネチャと戻り値は変えず、
他のブロックには触らないでください。
抽出後に pytest tests/test_billing.py を実行し、通ったら
radon cc -s src/billing.py の結果を報告してください。

コミットメッセージは refactor: process_order から apply_tax を抽出 のように、動詞と対象を決めた形にします。コミットの打ち方そのものを揃えたいなら、commit-commandsプラグインでコミットまでを1コマンドにできます。メッセージ形式を機械的に縛るならcommitlintの設定が使えます。

毎回同じ指示を書かないよう、規約は CLAUDE.md に置きます。

## 巨大関数の分割
- 1回の依頼で抽出するのは1ブロックだけ。複数の抽出を同時に行わない
- 呼び出し元から見えるシグネチャと戻り値は変えない
- DB・メール・ファイル出力を含むブロックは抽出しない
- 抽出後に pytest を実行し、通ったら radon cc -s の結果を報告する

分割の前後で数字がどう動くか

模擬関数を、副作用のない3ブロックの順で手作業で抽出した結果が次のとおりです。コードは筆者が手で書いた例で、Claudeの実出力ではありません。

PROMO_RATE = {"HALF": 0.5, "TEN": 0.9}
 
 
def line_total(item, unit_price):
    return unit_price * PROMO_RATE.get(item.get("promo"), 1) * item["qty"]
 
 
def apply_coupon(total, coupon):
    if coupon == "A" and total > 1000:
        return total - 100
    if coupon == "B":
        return total - 50
    return total
 
 
def process_order(order, db, mailer):
    if not order.get("items"):
        raise ValueError("empty")
    total = sum(
        line_total(i, db.price(i["sku"])) for i in order["items"] if i["qty"] > 0
    )
    total = apply_tax(apply_coupon(total, order.get("coupon")), order["country"])
    db.save(order["id"], total)
    if order.get("email"):
        mailer.send(order["email"], total)
    return total

apply_tax は税率の適用を同様に抜いた関数です。同じradon 6.0.1で測ると、結果は次のとおりでした。

F 24:0 process_order - A (5)
F  8:0 apply_coupon - A (4)
F 16:0 apply_tax - A (3)
F  4:0 line_total - A (1)

process_order はC(13)からA(5)に下がりました。一方、4関数の合計は5+4+3+1で13です。分岐は消えたのではなく、名前の付いた小さな関数に移りました。分割で得るのは複雑度の総量の減少ではありません。1つの関数に抱え込んでいた分岐を、単体でテストできる単位に分けたことです。数字が下がらないのに分割を失敗と判断しないための目安として、この点は覚えておくと役に立ちます。

途中でつまずいたときの戻し方

2回訂正しても直らないときは、3回目の訂正を足さず、/clear して、分かったことを織り込んだ指示を書き直します。公式のベストプラクティスにある運用で、失敗した試行が積もった会話をClaudeが読み続けるのを避ける狙いです。1コミット1抽出にしておけば、/clear の前に失敗した抽出をコミット単位で捨てられます。

抽出のあとに緑が崩れたときは、差分で次の3点を見ます。

  • 抽出したブロックの外へ処理の順序が移っていないか(特に、読み取りと書き込みの前後関係)
  • 元の関数が暗黙に参照していた外側の変数(クロージャやグローバル)が、引数に含まれず漏れていないか
  • 例外の送出位置が変わり、呼び出し元の try で拾える範囲が変わっていないか

セッションを捨てたくない場合は、/compact 抽出済みのブロック名とテストコマンドを残して のように焦点を指定して要約させる方法もあります。大規模な変換を多数のファイルへ並列にかける場合はコードモダナイゼーションの進め方が参考になります。

まとめ

巨大関数の分割は、切る位置の基準を先に決めたほうが、Claudeへの指示が短くなります。副作用のないブロックから抜き、引数の束が細い順に進め、1抽出ごとにテストとコミットを挟む。この3点を CLAUDE.md に書けば、毎回の依頼は「このブロックを抜いて」の一文で済みます。複雑度の合計は減らない前提で、目的は単体でテストできる単位を増やすことに置きます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/common-workflows
  2. code.claude.com/docs/en/best-practices
  3. radon.readthedocs.io/en/latest/commandline.html
  4. radon.readthedocs.io/en/latest/intro.html
この記事を共有:XはてブLinkedIn