リファクタリング
外部の振る舞いを変えずにコードの内部構造を改善し、保守性と可読性を向上させる手法
リファクタリングとは
リファクタリングは、外部から観測できる振る舞いを変えずにコードの内部構造を改善し、保守性と可読性を高める手法である。Martin Fowler が 1999 年の著書『リファクタリング』で体系化し、2018 年に刊行された第 2 版で手順の名称とコード例が改められた。手順名を調べるときは、手元の版がどちらかを意識する必要がある。
「振る舞いを変えない」が指すのは返り値だけではない。呼び出し側から見える例外、外部への副作用、永続化される状態まで含めた約束である。その約束が守れているかを機械的に判定できるのはテストだけなので、テストが無いコードへ手を入れる作業はリファクタリングではなく書き直しに近い。名前を変えるだけの一歩でも、変更のたびにテストを通して同一性を確かめる。この刻み幅の細かさが、リファクタリングと大掛かりな改修を分ける実質的な境目である。
リファクタリング vs 機能追加
機能追加と混ぜて進めると、テストが落ちたときに原因が構造の変更なのか新しい仕様なのか切り分けられなくなる。同じ作業時間の中で両者を行き来してもよいが、コミットは必ず分ける。
| 観点 | リファクタリング | 機能追加 |
|---|---|---|
| 外部の振る舞い | 変わらない | 変わる |
| テスト | 全テストが通る | 新しいテストを追加 |
| 目的 | 内部構造の改善 | 新機能の実装 |
代表的なリファクタリング
Fowler のカタログには手順ごとに名前が付いている。原名を覚えておくと、実装例や議論を探すときに迷わない。
長い関数を小さな関数へ切り出す手順は、現行のカタログでは Extract Function の名で載っており、第 1 版では Extract Method と呼ばれていた。意図が伝わらない識別子を改める手順は Rename Variable と Rename Function、入れ子の条件式を早期リターンへ倒す手順は Replace Nested Conditional with Guard Clauses、まとめて渡している引数群を 1 つの型にする手順は Introduce Parameter Object である。重複の除去はこれ単独の手順ではなく、抽出と移動を積み重ねた結果として現れる。
関数の抽出
関数の抽出は、処理の塊に名前を付けて意図を表に出す作業である。次の例では、入力の解析・検証・計算・保存という 4 つの責務が 1 つの関数に同居しているため、どこを直せば何が変わるのかが読み取れない。
// ❌ 長いメソッド
async function processOrder(event: any) {
const body = JSON.parse(event.body);
if (!body.productId) throw new Error('Missing productId');
if (!body.quantity || body.quantity < 1) throw new Error('Invalid quantity');
const product = await db.get({ TableName: 'products', Key: { id: body.productId } });
const total = product.price * body.quantity;
await db.put({ TableName: 'orders', Item: { id: uuid(), total, ...body } });
return { statusCode: 201, body: JSON.stringify({ total }) };
}
// ✅ 抽出後
async function processOrder(event: any) {
const input = parseInput(event);
const validated = validateOrder(input);
const total = await calculateTotal(validated);
const order = await saveOrder(validated, total);
return formatResponse(201, order);
}
早期リターン
入れ子の条件式は、成立する経路を読み手が頭の中で積み上げないと読めない。前提を満たさない場合を先に片付けてしまえば、後には本筋だけが残る。
// ❌ ネストが深い
function process(user: User | null) {
if (user) {
if (user.active) {
if (user.verified) {
return doSomething(user);
}
}
}
return null;
}
// ✅ 早期リターン
function process(user: User | null) {
if (!user) return null;
if (!user.active) return null;
if (!user.verified) return null;
return doSomething(user);
}
リファクタリングの安全網
条件を反転させる過程で否定の付け方を誤れば、それだけで振る舞いが変わる。振る舞いを変えていないことを目視で保証するのは現実的でないため、手を動かす前に安全網を用意する。
| 安全網 | 何を担保するか |
|---|---|
| テスト | 変更前に通っていたテストが変更後も通ることで、観測できる振る舞いの同一性を確かめる |
| 型チェック | 名前やシグネチャの変更が波及した箇所を機械的に洗い出す。ただし型が通っても計算の中身が同じとは限らない |
| 小さなステップ | 1 手ごとにテストを回し、壊れた原因を直前の 1 手に限定する |
| バージョン管理 | 手順ごとにコミットを分け、迷ったら直前の状態へ戻る |
いつリファクタリングするか
独立した作業として計画すると、期限のある仕事に押し出されて後回しになる。日々の変更に紐付けたほうが続く。
| タイミング | 説明 |
|---|---|
| 機能追加の前 | コードを理解しやすくしてから追加 |
| バグ修正の前 | 原因を見つけやすくする |
| コードレビューで指摘 | レビューの指摘を反映 |
| ボーイスカウトルール | 触った範囲だけを少し綺麗にして戻す。範囲を広げると差分が読めなくなる |
リファクタリングについては関連書籍でも詳しく扱われている。
この記事は役に立ちましたか?