refactor について
refactorは、Martin Fowlerのリファクタリングカタログに基づくコード再構成スキルです。構造や保守性を改善する開発者向けに、振る舞いを保ち、小さな変更とテストを重視します。
主な機能
- 01コードの問題に応じて手法を選択
- 02Fowlerの複数のリファクタリング手法を収録
- 03外部から見える振る舞いを維持
- 04小さな変更とテストによる検証を重視
- 05判断カードで根拠と効果を評価
活用シーン
- 01長いメソッドや大きすぎるクラスを分割
- 02重複を除き責務の境界を整理
- 03テストで保護しながら保守しにくいコードを改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ smallnest/ goal-workflow --skill refactorインストール後、Claude Code で/refactorを入力して読み込まれたか確認します。
詳細
リファクタリング — 熟練者向けコードの再構成
Martin Fowler の『』第 2 版のカタログに基づいた、慎重なコードのリファクタリングです。外部の動作を変えずに、構造、可読性、保守性を改善します。大改革ではなく、段階的な進化です。
使用する場面
このスキルは、次の場合に有効になります。
- コードの理解や保守が難しい
- 関数やクラスが大きくなりすぎている
- コードの不吉な臭いが検出された
- 構造が悪いため、機能追加が難しい
- ユーザーがリファクタリング、整理、改善を明示的に依頼している
- ユーザーが次のように言っている:refactor, 重构, clean up, improve code, code smell, extract method, rename, simplify
5 つの原則
以下の 5 つの原則は譲れません。いずれかに違反すると、リファクタリングは無謀な編集になってしまいます。
1. 動作を維持する
変えるのはコードの 仕組み だけで、何をするか は変えません。事前にテストがあった場合は、変更後もテストに合格する必要があります。リファクタリングによって動作が変わるなら、それはリファクタリングではなく、書き直しです。
2. 小さなステップで進める
それぞれの変更は、コンパイルが通り、テストに合格する最小限の変換にします。ステップが失敗した場合、原因となった変更を正確に特定できます。リファクタリングは、小さく安全な変換を積み重ねるものであり、大規模な書き直しを一度に行うものではありません。
3. バージョン管理を活用する
開始前にコミットし、各ステップが成功した後にもコミットします。これにより、いつでも取り消せます。クリーンな状態からブランチを作成すれば、影響を残さずにリファクタリングを中止できます。
4. テストは不可欠
「テストがなければ、リファクタリングではなく、ただの編集です。」対象コードのテストが存在しない場合は、まず特性化テストを書きます。これらのテストで現在の動作を記録し、リグレッションを検出できるようにします。
5. 一度に一つだけ行う
リファクタリングと機能変更を決して混在させてはいけません。無関係なものを同時にリファクタリングしてはいけません。各コミットには、リファクタリングの操作を一つだけ含めてください。
リファクタリングを行わない場面
| 状況 | 対応 |
|---|---|
| コードが動作しており、今後変更される予定もない | そのままにする |
| テストのない、重要な本番環境の処理経路 | まず特性化テストを書く |
| 厳しい期限に追われている | 問題点を記録し、後でリファクタリングする |
| 明確な目的や利点がない | リファクタリング自体を目的にしない |
| コードに根本的な誤りがある | これはリファクタリングではなく、書き直しです |
リファクタリング判断基準
原則は判断の指針であり、コードを書き換える個別の理由ではありません。Fowler の手法を選ぶ前に、次の判断カードを 1 枚記録してください。
| フィールド | 必須の回答 |
|---|---|
| 主なコードの臭い | 1 つの根本原因。原則ごとに項目を設けない |
| 根拠 | 場所、動作、呼び出し元、変更履歴、または計測結果 |
| 主な原則 | 該当する原則のうち、最も具体的なもの |
| 関連する原則 | 説明用のラベルのみ。個別に数えない |
| 期待される効果 | 変更の波及、認知負荷、重複した知識、結合、障害の遅延、または計測された実行時コストのうち、観察可能な削減 |
| 最小限のリファクタリング | 根本原因に対処する、最も侵襲性の低い Fowler の手法 |
| ベースライン/成功条件 | 動作の保持と期待される効果をどのように検証するか |
次の 4 つの判断軸を使ってください。
| 判断軸 | 原則 | 確認する質問 |
|---|---|---|
| 責任と依存関係 | SOLID、SRP、OCP、DIP、関心の分離 | 変更理由が混在していませんか? 実際に新しいバリエーションが加わるたびに、安定したロジックを繰り返し変更していませんか? 高水準のポリシーが具体的な仕組みに依存していませんか? 関心事が境界を越えて漏れていませんか? |
| 再利用と構造 | DRY、コンポジション | 同じ知識が重複していますか、それとも構文が似ているだけですか? 継承よりもコンポジションのほうが、実際の差異を適切に局所化できますか? |
| 単純さとスコープ | KISS、YAGNI | 提案された構造は、現在の問題にとってより単純ですか? すべての抽象化は、既存のバリエーションまたは境界によって裏付けられていますか? |
| 実行時とフィードバック | フェイルファスト、最初に計測 | エラーの意味を変えずに、不正な状態をその発生源に近い場所で失敗させられますか? 問題と結果を証明するベースラインは何ですか? |
SOLID は包括的な原則です。根拠が SRP、OCP、または DIP を支持する場合は、その具体的な判断軸を使い、SOLID の問題を別に作らないでください。LSP と ISP は、それぞれ SOLID/LSP、SOLID/ISP とラベル付けできます。DRY が対象とするのは共有された知識であり、似ているすべてのコードではありません。継承によって実際の結合が生じる場合はコンポジションが優先されますが、常に優先するわけではありません。OCP と DIP は、KISS や YAGNI に反する推測的なレイヤーを正当化するものではありません。
このスキルにおける数値しきい値(行数、パラメーター数、メソッド数、ネストの深さ)は、状況に依存する候補指標です。実行に移す前に、責任の混在、認知コスト、変更の反復、知識の重複、または計測された実行時の影響を確認してください。
コードの臭いカタログ
Fowler の分類に基づいています。リファクタリングの前に、該当するコードの臭いを特定してください。
肥大化
| コードの臭い | 説明 | 主なリファクタリング |
|---|---|---|
| 長いメソッド | 候補:メソッドが 10~15 行超。責任の混在または認知コストを確認してください | メソッドの抽出、一時変数の問い合わせへの置換 |
| 大きなクラス | 候補:フィールド/メソッドが多数。独立した変更理由があるか確認してください | クラスの抽出、サブクラスの抽出 |
| プリミティブ執着 | 小さなオブジェクトの代わりにプリミティブを使っている | データ値をオブジェクトに置換、型コードをクラスに置換 |
| 長いパラメーターリスト | 候補:パラメーターが 3~4 個超。不足している概念や繰り返し現れるデータのまとまりがあるか確認してください | パラメーターオブジェクトの導入、オブジェクト全体の保持 |
| データのまとまり | 同じデータのグループが一緒に現れる | クラスの抽出、パラメーターオブジェクトの導入 |
オブジェクト指向の濫用
| コードの臭い | 説明 | 主なリファクタリング |
|---|---|---|
| switch 文 | 型コードに対する switch/if-else が繰り返される | 条件分岐をポリモーフィズムに置換、型コードをサブクラスに置換 |
| 一時フィールド | 特定の状況でのみ設定されるフィールド | クラスの抽出、ヌルオブジェクトの導入 |
| 継承の拒否 | サブクラスが継承したメンバーを使わない | 継承を委譲に置換、メソッド/フィールドの下位クラスへの移動 |
| インターフェースの異なる代替クラス | 似た処理を異なる名前で行うクラス | メソッド名の変更、メソッドの移動、スーパークラスの抽出 |
変更を妨げる要因
| コードの臭い | 説明 | 主なリファクタリング |
|---|---|---|
| 変更の発散 | 1つのクラスが異なる理由で変更されます | クラスの抽出 |
| 変更の散弾銃 | 1つの変更により、複数のクラスで多くの小さな変更が必要になります | メソッドの移動、フィールドの移動、クラスのインライン化 |
| 並行する継承階層 | ある階層にサブクラスを追加すると、別の階層にも追加が必要になります | メソッドの移動、フィールドの移動 |
不要な要素
| コードの臭い | 説明 | 主なリファクタリング |
|---|---|---|
| コメント | コードが何をするか(なぜではなく)を説明するコメントです | メソッドの抽出、変数名の変更、アサーションの導入 |
| 重複コード | 複数の場所にある同じコード構造です | メソッドの抽出、メソッドの引き上げ、テンプレートメソッドの形成 |
| 怠惰なクラス | 存在を正当化するほどの役割を果たしていないクラスです | クラスのインライン化、階層の縮小 |
| データクラス | フィールドとゲッター/セッターしかないクラスです | メソッドの移動、フィールドのカプセル化、コレクションのカプセル化 |
| デッドコード | 未使用のコード、インポート、コメントアウトされたブロックです | 削除します(Git の履歴に残っています) |
| 投機的一般化 | 「いつか」使うために作られたものの、使われなかったコードです | クラスのインライン化、階層の縮小、パラメーターの削除 |
密結合
| コードの臭い | 説明 | 主なリファクタリング |
|---|---|---|
| 機能への羨望 | メソッドが自クラスのデータより、他のクラスのデータを多く使います | メソッドの移動、メソッドの抽出+メソッドの移動 |
| 不適切な親密さ | クラス同士が互いの内部実装を知りすぎています | メソッドの移動、フィールドの移動、委譲の隠蔽 |
| メッセージチェーン | a.getB().getC().getD().doSomething() | 委譲の隠蔽、メソッドの抽出 |
| 仲介人 | クラスがすべてを別のクラスに委譲します | 仲介人の削除、メソッドのインライン化 |
| 不完全なライブラリークラス | ライブラリーに必要なメソッドがありません | 外部メソッドの導入、ローカル拡張の導入 |
リファクタリング手法カタログ
Fowler のカタログに基づき、カテゴリ別に整理しています。各手法には、具体的な手順が含まれています。
メソッドの構成
メソッドの抽出
コード片を、その目的を説明する名前のメソッドにします。
手順:
- コード片が何をするか(どのようにするかではなく)に基づいて、新しいメソッドに名前を付けます
- 抽出するコードを新しいメソッドにコピーします
- ローカル変数を特定します。読み取り専用のものはパラメーターにし、変更されるものは戻り値にします
- パラメーターを渡し、戻り値を処理します
- 元のコード片を、新しいメソッドの呼び出しに置き換えます
- テストします
変更前:
void printOwing() {
printBanner();
// Print details
System.out.println("name: " + _name);
System.out.println("amount: " + getOutstanding());
}
変更後:
void printOwing() {
printBanner();
printDetails(getOutstanding());
}
void printDetails(double outstanding) {
System.out.println("name: " + _name);
System.out.println("amount: " + outstanding);
}
メソッドのインライン化
メソッド本体が名前と同じくらい明確な場合、メソッド呼び出しをその本体に置き換えます。
手順:
- メソッドがポリモーフィックでないこと(サブクラスでオーバーライドされていないこと)を確認します
- すべての呼び出し元を探します
- 各呼び出しをメソッド本体に置き換えます
- メソッド定義を削除します
- テストします
変数の抽出
式(またはその一部)の結果を、自己説明的な変数に格納します。
変更前:
if (platform.toUpperCase().indexOf("MAC") > -1 &&
browser.toUpperCase().indexOf("IE") > -1 &&
wasInitialized() && resize > 0) {
// ...
}
変更後:
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
// ...
}
一時変数のインライン化
一時変数が1回しか使われず、式が明確な場合、その一時変数を式に置き換えます。
一時変数をクエリーに置き換える
式をメソッドに抽出します。一度計算して再利用される一時変数を、メソッド呼び出しに置き換えます。
一時変数の分割
複数回代入される一時変数(ループや値の収集に使うものを除く)は、責務ごとに別の変数に分割します。
パラメーターへの代入の削除
パラメーターには代入しないでください。代わりにローカル変数を使います。
メソッドをメソッドオブジェクトに置き換える
長いメソッドが多くのローカル変数を使っていて、メソッドの抽出が難しい場合、そのメソッドを独立したクラスにし、ローカル変数をフィールドにします。
アルゴリズムの置き換え
アルゴリズムを、より明確なものに置き換えます。
オブジェクト間で機能を移動する
メソッドの移動
メソッドを最もよく使用するクラスへ移動します。
手順:
- 現在のクラスで、そのメソッドが使用している機能をすべて確認します
- ポリモーフィズム(サブクラス/スーパークラスのメソッド)を確認します
- 必要に応じて調整しながら、対象クラスにメソッドを作成します
- ソースから対象オブジェクトを参照します
- ソースのメソッドを委譲するメソッドにするか、削除します
- テストします
フィールドの移動
フィールドを最もよく使用するクラスへ移動します。
クラスの抽出
1つのクラスが2つの役割を担っている場合は、分割します。新しいクラスを作成し、関連するフィールドとメソッドを移動します。
クラスのインライン化
クラスがほとんど何もしていない場合は、そのクラスを最もよく使用するクラスに統合します。
委譲の隠蔽
サーバーにメソッドを作成して、委譲の連鎖を隠します。manager = person.getDepartment().getManager() → manager = person.getManager()。
ミドルマンの削除
クラスが委譲を多くしすぎている場合は、委譲先を直接呼び出します。
外部メソッドの導入
サーバークラスにメソッドを追加する必要があるものの、そのクラスを変更できない場合は、サーバーインスタンスを第1引数に取るメソッドをクライアントに作成します。
ローカル拡張の導入
複数の外部メソッドが必要な場合は、拡張クラス(サブクラスまたはラッパー)を作成します。
データの整理
フィールドの自己カプセル化
所有クラス内でも、フィールドにはゲッターとセッターを通じてアクセスします。
データ値をオブジェクトに置き換える
データ項目に追加のデータや振る舞いが必要な場合は、オブジェクトに変換します。
変更前:
class Order {
private String customer; // Just a string
}
変更後:
class Order {
private Customer customer; // Rich object with name, address, credit rating
}
値から参照への変更
複数の場所でオブジェクトの同じインスタンスを共有する必要がある場合に使用します。
参照から値への変更
参照オブジェクトが小さく、不変であり、値セマンティクスが必要な場合に使用します。
配列をオブジェクトに置き換える
配列に異種のデータ(String[] row = new String[3] — 名前、スコア、勝利数)が格納されている場合は、オブジェクトに置き換えます。
観察対象データの複製
ドメインデータが GUI コントロールに存在する一方で、ドメインロジックでも必要な場合は、データをドメインオブジェクトにコピーし、オブザーバーを設定して両者を同期させます(オブザーバーパターン)。プレゼンテーションとドメインを分離することで、それぞれを独立して進化させられます。
単方向関連を双方向関連に変更
2つのクラスがお互いの機能を必要としているのに、一方しか参照を保持していない場合は、逆参照を追加し、両端の変更処理でリンクの整合性を保ちます。本当に必要な場合にのみ参照を追加してください。双方向リンクは結合度を高め、不整合のリスクをもたらします。
双方向関連を単方向関連に変更
双方向リンクが存在するものの、一方がもう一方を使わなくなった場合は、不要な方向を削除します。結合度を下げ、ライフサイクル管理を簡素化し、古い逆参照だけによって生き続ける「ゾンビ」オブジェクトを防ぎます。
マジックナンバーをシンボリック定数に置き換える
リテラルの数値や文字列を、名前付き定数に置き換えます。
フィールドのカプセル化
public フィールドを private にし、アクセサーを用意します。
コレクションのカプセル化
生のコレクションを返してはいけません。読み取り専用のビューを返し、追加/削除メソッドを用意します。
型コードをクラスに置き換える
数値または文字列の型コードを、意味のある振る舞いを持つクラスに置き換えます。
型コードをサブクラスに置き換える
型コードが振る舞いに影響する場合は、条件分岐ではなくポリモーフィズムを使用します。
型コードを状態/ストラテジーに置き換える
サブクラスを使う方法に似ていますが、実行時に型を変更できる場合にコンポジションを使用します。
サブクラスをフィールドに置き換える
サブクラス間で異なるのが定数データだけの場合は、単一のクラスにあるフィールドに置き換えます。
条件式の簡素化
条件の分解
条件、then 部、else 部をそれぞれ別のメソッドに抽出します。
変更前:
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * _winterRate + _winterServiceCharge;
} else {
charge = quantity * _summerRate;
}
変更後:
if (isSummer(date)) {
charge = summerCharge(quantity);
} else {
charge = winterCharge(quantity);
}
条件式の統合
同じ結果になる複数の条件式をまとめます。
重複する条件分岐の断片の統合
すべての分岐にあるコードを、条件分岐の外に移動します。
制御フラグの削除
制御フラグを break、continue、または return に置き換えます。
ネストした条件分岐をガード節に置き換える
深いネストを使う代わりに、特殊なケースでは早期リターンを使います。
変更前(アローコード):
double getPayAmount() {
double result;
if (_isDead) {
result = deadAmount();
} else {
if (_isSeparated) {
result = separatedAmount();
} else {
if (_isRetired) {
result = retiredAmount();
} else {
result = normalPayAmount();
}
}
}
return result;
}
変更後:
double getPayAmount() {
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();
}
条件分岐をポリモーフィズムに置き換える
オブジェクトの型に応じて条件分岐で異なる動作を選択している場合は、サブクラスを使います。
Null オブジェクトを導入する
null チェックを、デフォルトの動作を提供する Null オブジェクトに置き換えます。
アサーションを導入する
アサーションで前提条件を明示します。
メソッド呼び出しをシンプルにする
メソッド名を変更する
メソッド名から、そのメソッドが何をするのか分かるようにします。適切な名前を思いつけない場合、そのメソッドは複数の責務を担っている可能性があります。
パラメーターを追加する/削除する
メソッドにより多くの情報が必要な場合は、パラメーターを追加します。別の方法で情報を取得できる場合は、パラメーターを削除します。
クエリと変更処理を分離する
メソッドは値を返すか、状態を変更するかのどちらか一方だけにし、両方を行わないようにします。
メソッドをパラメーター化する
異なる値を使って同様の処理を行う複数のメソッドを、パラメーターを持つ1つのメソッドにまとめます。
パラメーターを明示的なメソッドに置き換える
逆のアプローチです。パラメーターによって実質的に異なる動作が選択される場合は、個別のメソッドを作成します。
オブジェクト全体を保持する
オブジェクトから個々のフィールドを取り出すのではなく、オブジェクト全体を渡します。
パラメーターをメソッドに置き換える
(refactoring.guru:パラメーターをメソッド呼び出しに置き換える) オブジェクトがすでに持つデータからパラメーターを計算できる場合は、パラメーターを削除し、メソッド内でクエリを呼び出します。
パラメーターオブジェクトを導入する
自然に関連するパラメーターをオブジェクトにまとめます。
セッターメソッドを削除する
セッターを削除し、コンストラクターで値を設定して、フィールドを不変にします。
メソッドを隠す
クラスの外部で使われないメソッドは、private にします。
コンストラクターをファクトリーメソッドに置き換える
単純なコンストラクター呼び出しより柔軟性が必要な場合に使います。
エラーコードを例外に置き換える
エラーコードを返す代わりに、例外をスローします。
例外をテストに置き換える
例外を捕捉する代わりに、先に条件を確認します。
汎化への対応
フィールド/メソッド/コンストラクター本体を引き上げる
サブクラスに共通するフィールド、メソッド、コンストラクターのコードをスーパークラスに移動します。
メソッド/フィールドを引き下げる
スーパークラスの動作を、それを使うサブクラスだけに移動します。
サブクラスを抽出する
一部のインスタンスで使われる機能のまとまりに対して、サブクラスを作成します。
スーパークラスを抽出する
類似するクラス間で共通する機能をまとめるスーパークラスを作成します。
インターフェースを抽出する
クラスの public メソッドの一部からインターフェースを作成します。
階層を統合する
スーパークラスとサブクラスに十分な違いがない場合は、両者を統合します。
テンプレートメソッドを作成する
スーパークラスでアルゴリズムを汎化し、サブクラスで詳細を実装できるようにします。
継承を委譲に置き換える
サブクラスがスーパークラスの一部しか使わない場合は、コンポジションを使います。
委譲を継承に置き換える
委譲するクラスが、委譲先のすべての動作にアクセスする必要がある場合に使います。
リファクタリングのプロセス
フェーズ 1:準備
- 存在しない場合は、特性テストを作成します。これらのテストは現在の動作を記録するものです。洗練されている必要はありませんが、回帰を検出できる程度に網羅的にします。
- 対象となる問題のベースラインを記録します。代表的な動作、変更の広がり、関連する呼び出し元、またはパフォーマンス改善の場合はベンチマーク/プロファイル/クエリ数を記録します。パフォーマンスのベースラインがない場合、パフォーマンスに関するアイデアは、測定済みの改善ではなく候補として説明します。
- 現在の状態をコミットします。作業ツリーがクリーンな状態から始めます。
- リファクタリング用のブランチを作成します。機能開発とは分けて作業します。
フェーズ 2:特定
- コードの臭いを嗅ぎ取ります。 上記のコードの臭いのカタログを使って問題を分類します。しきい値は候補となるシグナルとして扱ってください。
- コードを理解します。 コードを十分に読み、関連する呼び出し元と依存関係を把握し、具体的な障害または変更シナリオを特定します。
- 主要な原則を選びます。 関連する原則は説明としてのみ付記し、1 つの根本原因を複数の指摘に分割しないでください。
- 反対方向の圧力を確認します。 DRY によって誤った抽象化が生じないこと、OCP/DIP が推測に基づくものでないこと、SRP によって受け渡しだけの断片が生まれないこと、コンポジションによって実際の継承の結合が解消されることを確認します。
- 適切なリファクタリングを選びます。 カタログから最小限の手法を選び、ベースラインと動作の確認方法を定義します。
フェーズ 3:リファクタリング(小さなステップで)
各ステップで、以下を行います。
- 小さな変更を 1 つ行います。 主要なコードの臭いのうち、検証可能な部分を 1 つ解消します。無関係な原則に基づくクリーンアップをまとめて行わないでください。
- コンパイルします。 変更のたびにコードがコンパイルできる状態にします。
- テストを実行します。 すべてのテストが成功する必要があります。成功しない場合は、動作を変更しています。
- 接続点を確認します。 新しいインターフェース、アダプター、または間接化によって実際の呼び出し元の複雑さが解消されるか、既存のバリエーションがサポートされる必要があります。そうでなければ、よりシンプルな設計を維持します。
- コミットします。
refactor: extract validateEmail methodのようなメッセージでコミットを作成します。
コードの臭いが解消されるまで繰り返します。
フェーズ 4:検証
- 動作。 すべてのテスト、型チェック、コンパイル、手動のスモークテストが成功することを確認します。
- 構造。 依存関係の方向、関連する呼び出し元、重複した知識、または変更の広がりを判断カードと比較します。
- 早期失敗のセマンティクス。 検証を早い段階に移した場合は、エラーの種類/コード、集約、再試行、トランザクション、クリーンアップの動作を維持します。
- パフォーマンス。 パフォーマンスが主張に含まれていた場合は、同じワークロードと環境で再実行します。結果と測定値のばらつきを報告してください。ベースラインがない場合は、明らかな性能低下は見られなかったことのみを報告します。
- 差分レビュー。 意図しない変更がないか確認し、成功条件が満たされていることを確認します。
フェーズ 5:クリーンアップ
- 古くなったコメントを削除します。 リファクタリングによって自明になったコメントは削除します。
- デッドコードを確認します。 リファクタリング後に未使用コードが見つかることがあります。
- 推測に基づく接続点を削除します。 現時点で役に立たない単一実装のインターフェース、受け渡しだけのモジュール、未使用の拡張ポイントを削除します。
- 最終コミット。 リファクタリングの手順を要約します。
リファクタリングのチェックリスト
コード品質
- 対処しているのは主要なコードの臭い/根本原因 1 つであり、関連する原則を別個の作業として数えていない
- 根拠、期待される影響、ベースライン、成功条件が記録されている
- 関数のサイズがそれぞれのコンテキストに適切である(行数は候補となるシグナルであり、上限ではない)
- 責任の境界を改善する場合、各関数が 1 つのことだけを行っている(SRP)
- 独立して変更されるコードを統合することなく、知識の重複がない(DRY)
- 名前が方法ではなく内容を表している
- マジックナンバーやマジック文字列がない
- デッドコードが削除されている
構造
- 関連するコードがまとめられ、意図した境界を越えて関心事が漏れていない
- モジュールの境界が明確である
- 依存関係が一方向に流れている(循環がない)。DIP の抽象化が実際の境界に対応している
- OCP/DIP による変更で、KISS/YAGNI に反する推測的なレイヤーが導入されていない
- 実際の継承の結合を減らす場合にのみ、コンポジションが優先されている
- 循環依存がない
条件分岐
- 深いネストがガード節に置き換えられている
- 複雑な条件が名前付きメソッドに抽出されている
- 型の切り替えを行う条件分岐がポリモーフィズムに置き換えられている
- 適切な場合に Null Object パターンが使われている
型安全性(型付き言語)
- すべての公開 API に型が定義されている
- 理由を示さずに
anyを使用していない - Nullable 型が明示されている
- 型コードがクラス/列挙型に置き換えられている
テスト
- リファクタリングしたコードをテストします
- エッジケースをカバーします
- 適切な場合、無効な入力や状態は発生源の近くで失敗するようにします(フェイルファスト)
- エラー、再試行、トランザクション、クリーンアップのセマンティクスを変更しないようにします
- 各ステップの後にすべてのテストが成功することを確認します
- リファクタリング前の動作を特性テストで記録します
- パフォーマンスに関する主張には、比較可能な変更前後の測定値を添えます
言語別のガイダンス
Java
- 変更しないローカル変数には
finalを優先します - 機械的な手順には IDE の自動リファクタリング機能(Eclipse/IntelliJ)を使用します
- 型システムを活用します。列挙型、レコード(Java 14+)、シールドクラス(Java 17+)などを利用します
JavaScript/TypeScript
- 分割代入を使ってパラメーター数を減らします
- 不変のバインディングには
letよりconstを優先します - 型コードではなく TypeScript のユニオン型を使用します
- Null 合体演算子(
??)とオプショナルチェーン(?.)を使うと、煩雑な null チェックをなくせます
Python
- リファクタリング中の意図の明示には型ヒントを使用します
- タプルやデータクラスのパターンを置き換えるには
dataclassesを使用します - ゲッターを置き換えるには
@propertyを使用します - リソースのクリーンアップにはコンテキストマネージャーを使用します
Go
- 小さなインターフェースを優先します。インターフェースを受け取り、構造体を返します
- 明確さが増す場合は名前付き戻り値を使用します
- テーブル駆動テストはリファクタリングと相性が良いです
- 早期リターンを使って深いネストを避けます
Rust
- エラーコードや null の代わりに
ResultとOptionを使用します - パターンマッチングで if-else の連鎖を置き換えます
Fromトレイトの実装を使うと、型変換を簡潔にできます- derive マクロを使って定型コードを減らします
よくあるリファクタリングの手順
メソッド抽出の手順
- 意図を表す名前で新しいメソッドを作成します
- コードの断片を新しいメソッドにコピーします
- ローカル変数を特定し、パラメーターや戻り値にします
- 元の場所から新しいメソッドを呼び出します
- テストします
条件分岐をポリモーフィズムで置き換える手順
- 各バリエーションに対応するサブクラスを作成します
- 適切なサブクラスを返すファクトリーメソッドを作成します
- 条件分岐の本体を、該当するサブクラスのメソッドに移動します
- 条件分岐を削除します
クラス抽出の手順
- まとまりのあるフィールドとメソッドの一部を特定します
- 新しいクラスを作成します
- 元のクラスからインスタンスを作成します
- フィールドとメソッドを一つずつ移動します
- 元のクラス内の参照を更新します
- 移動するたびにテストします
クラスインライン化の手順
- 対象クラスの呼び出し元をすべて特定します
- すべてのメソッドとフィールドを統合先のクラスに移動します
- すべての参照を統合先のクラスに付け替えます
- 空になったクラスを削除します
- テストします
安全プロトコル
作業を始める前に
1. Characterization tests → capture what the code does now
2. Git commit → save a known-good state
3. Branch → isolate refactoring from other work
各ステップで必ず
1. One change → one refactoring technique
2. Compile → must compile clean
3. Tests → every test must pass
4. Commit → message: "refactor: <technique> <what>"
テストに失敗した場合
1. Undo the last change
2. Understand what broke and why
3. Try a smaller step
4. If the test was wrong and behavior was correct, fix the test FIRST, then retry
完了時
1. Full test suite → all tests pass
2. Manual smoke test → quick sanity check
3. Self-review diff → catch unintended changes
4. Final commit → describe the overall transformation
リファクタリングにおけるデザインパターン
Strategy パターン
アルゴリズムを選択する条件分岐を置き換えます。コードの臭い: 型コードに対する switch で、分岐ごとに動作が異なります。手法: 条件分岐をポリモーフィズムで置き換える + メソッド抽出。
Template Method
共通のアルゴリズムの骨組みをスーパークラスに抽出し、バリエーションはサブクラスに実装させます。コードの臭い: わずかな違いしかない重複コードです。手法: テンプレートメソッドの形成。
State パターン
状態に基づく条件分岐を置き換えるため、各状態の動作をクラスに抽出します。コードの臭い: ステータスフィールドに対する switch で、動作が変化します。手法: 型コードを State/Strategy で置き換えます。
Composite パターン
個々のオブジェクトとグループを同様に扱います。コードの臭い: クライアントコードが単体とコレクションを特別に扱います。手法: インターフェース抽出 + Composite の作成。
Decorator パターン
オブジェクトをラップして動作を動的に追加します。コードの臭い: オプションの動作に対する条件ロジックです。手法: クラス抽出 + コンポジションの使用。
Null Object パターン
null チェックをデフォルトオブジェクトで置き換えます。コードの臭い: if (x == null) のチェックが繰り返されています。手法: Null Object の導入。
エッジケースと注意点
| シナリオ | 対応 |
|---|---|
| テストが存在しない | まず特性テストを書きます。さまざまな入力でコードを実行し、出力を記録します。これらが安全網になります。 |
| リファクタリングによって離れた箇所のテストが失敗する | まず、なぜ失敗したのかを理解します。テストが実装の詳細に依存していた可能性があります。その場合は、実装ではなく振る舞いをテストするように修正してから、作業を再開します。 |
| ユーザーが振る舞いの変更とリファクタリングを同時に求めている | 断ります。別々に行います。まずリファクタリングして振る舞いの変更を容易にし、コミットしてから、振る舞いを変更します。 |
| メソッドが複雑すぎて手順を追えない | メソッドオブジェクトによるメソッドの置き換えを使います。メソッド全体をクラスにして、各手順を抽出できるようにします。 |
| 大規模なコードベース全体でリファクタリングする | まずマイクロサービスまたはモジュールの境界を抽出します。その後、その境界内でリファクタリングします。「リファクタリングが多すぎる場合を除けば、あらゆるものにリファクタリングがあります。」 |
| IDE に自動リファクタリング機能がある | それを使います。最新の IDE では、安全に名前変更、メソッド抽出、変数導入などを行えます。手動で行うのは、IDE でできない場合だけにします。 |
| 元に戻す必要がある | git stash または git reset --hard で最後のコミットまで戻します。小さなコミットにしておけば、簡単に元に戻せます。 |
リソース
- Martin Fowler 著、『Refactoring: Improving the Design of Existing Code』第2版(2018年)
- refactoring.com — Fowler のオンラインカタログ
- refactoring.guru — 図解付きのリファクタリングパターン
- refactoring.guru/refactoring/catalog — 技法の完全なカタログ(6カテゴリ、66技法)と、このスキルが踏襲するコードの臭いの分類体系