前回はSOLID原則を学び、変更に強い設計の考え方を理解しました。 今回は「リファクタリング」について解説します。 リファクタリングとは「外から見た動作を変えずに、コードの内部構造を改善すること」です。以前学んだクリーンコードやSOLID原則を実際のコードに適用するための具体的な手法を学びましょう。
リファクタリングとは
「リファクタリング(Refactoring)」はマーチン・ファウラーが体系化した概念で、動作を変えずにコードの設計を改善することを指します。
リファクタリングが必要なサインを「コードの臭い(Code Smell)」と呼びます。代表的なものは以下の通りです。
重複したコード // 同じような処理が複数箇所に書かれている
長すぎるメソッド // 1つのメソッドがあまりにも多くのことをしている
長すぎるクラス // クラスが大きくなりすぎている
マジックナンバー // 意味のわからない数値が直接書かれている
深すぎるネスト // if文やループが何重にも入れ子になっている
コメントが多すぎる // コードが読めないためコメントで補っている
よく使うリファクタリング手法
メソッドの抽出 // 長いメソッドの一部を別のメソッドに切り出す
変数の抽出 // 複雑な式に名前をつけて変数にする
条件の単純化 // 複雑な条件式をわかりやすく書き直す
クラスの抽出 // 1つのクラスが持ちすぎた責任を別クラスに分ける
パラメータオブジェクト // 引数が多いメソッドの引数をクラスにまとめる
ガード節の導入 // ネストを減らすために早期リターンを使う
サンプルコード(リファクタリングの実践)
ファイル作成
「Java52」フォルダに RefactoringExample.java という名前でファイルを作成します

コード入力
以下のコードを入力します。
import java.util.List;
import java.util.stream.Collectors;
public class RefactoringExample {
// ===== Step 1: リファクタリング前のコード =====
static class Before {
// 問題: メソッドが長く、マジックナンバーがあり、ネストが深い
static String generateReport(List<int[]> data) {
StringBuilder sb = new StringBuilder();
sb.append("=== レポート ===\n");
int total = 0;
int count = 0;
for (int[] row : data) {
if (row != null && row.length == 3) {
if (row[2] >= 0) {
total += row[2];
count++;
if (row[2] >= 80) {
sb.append("ID:" + row[0] + " 名前:" + row[1] + " 点数:" + row[2] + " [優秀]\n");
} else if (row[2] >= 60) {
sb.append("ID:" + row[0] + " 名前:" + row[1] + " 点数:" + row[2] + " [合格]\n");
} else {
sb.append("ID:" + row[0] + " 名前:" + row[1] + " 点数:" + row[2] + " [不合格]\n");
}
}
}
}
if (count > 0) {
sb.append("平均点: " + (total / count) + "\n");
}
return sb.toString();
}
}
// ===== Step 2: リファクタリング後のコード =====
// 手法1: パラメータオブジェクト(引数をまとめる)
record StudentScore(int id, String name, int score) {
static final int EXCELLENT_THRESHOLD = 80;
static final int PASSING_THRESHOLD = 60;
// 手法2: 変数の抽出・条件の単純化
String grade() {
if (score >= EXCELLENT_THRESHOLD) return "優秀";
if (score >= PASSING_THRESHOLD) return "合格";
return "不合格";
}
boolean isValid() {
return score >= 0;
}
}
static class After {
// 手法3: メソッドの抽出(長いメソッドを小さなメソッドに分割)
static String generateReport(List<StudentScore> students) {
var validStudents = filterValidStudents(students);
return buildReport(validStudents);
}
private static List<StudentScore> filterValidStudents(List<StudentScore> students) {
return students.stream()
.filter(StudentScore::isValid)
.collect(Collectors.toList());
}
private static String buildReport(List<StudentScore> students) {
var sb = new StringBuilder("=== レポート ===\n");
students.forEach(s -> sb.append(formatStudentLine(s)).append("\n"));
appendAverageScore(sb, students);
return sb.toString();
}
// 手法4: メソッドの抽出(フォーマット処理を切り出す)
private static String formatStudentLine(StudentScore student) {
return String.format("ID:%d 名前:%s 点数:%d [%s]",
student.id(), student.name(), student.score(), student.grade());
}
private static void appendAverageScore(StringBuilder sb, List<StudentScore> students) {
if (students.isEmpty()) return; // 手法5: ガード節の導入
double average = students.stream()
.mapToInt(StudentScore::score)
.average()
.orElse(0);
sb.append(String.format("平均点: %.1f\n", average));
}
}
// ===== Step 3: さらにリファクタリング(クラスの抽出) =====
// 手法6: クラスの抽出(レポート生成を専用クラスに移す)
static class ReportGenerator {
private final List<StudentScore> students;
ReportGenerator(List<StudentScore> students) {
this.students = students.stream()
.filter(StudentScore::isValid)
.collect(Collectors.toList());
}
String generate() {
return header() + body() + footer();
}
private String header() {
return "=== レポート ===\n";
}
private String body() {
return students.stream()
.map(this::formatLine)
.collect(Collectors.joining("\n", "", "\n"));
}
private String formatLine(StudentScore s) {
return String.format("ID:%d 名前:%s 点数:%d [%s]",
s.id(), s.name(), s.score(), s.grade());
}
private String footer() {
return students.stream()
.mapToInt(StudentScore::score)
.average()
.stream()
.mapToObj(avg -> String.format("平均点: %.1f\n", avg))
.findFirst()
.orElse("");
}
}
public static void main(String[] args) {
// Beforeのテストデータ(int配列)
List<int[]> rawData = List.of(
new int[]{1, 90, 90}, // 便宜上name列もintで
new int[]{2, 75, 75},
new int[]{3, 55, 55},
new int[]{4, -1, -1} // 無効データ
);
// Afterのテストデータ(recordを使う)
List<StudentScore> students = List.of(
new StudentScore(1, "ぱんまる", 90),
new StudentScore(2, "たろう", 75),
new StudentScore(3, "はなこ", 55),
new StudentScore(4, "けんじ", -1) // 無効データ
);
System.out.println("=== After(メソッド抽出) ===");
System.out.print(After.generateReport(students));
System.out.println("\n=== After(クラス抽出) ===");
System.out.print(new ReportGenerator(students).generate());
}
}

コンパイル
javac RefactoringExample.java

実行
java RefactoringExample
実行結果は次のようになります。
=== After(メソッド抽出) ===
=== レポート ===
ID:1 名前:ぱんまる 点数:90 [優秀]
ID:2 名前:たろう 点数:75 [合格]
ID:3 名前:はなこ 点数:55 [不合格]
平均点: 73.3
=== After(クラス抽出) ===
=== レポート ===
ID:1 名前:ぱんまる 点数:90 [優秀]
ID:2 名前:たろう 点数:75 [合格]
ID:3 名前:はなこ 点数:55 [不合格]
平均点: 73.3

リファクタリングの進め方
リファクタリングは以下のステップで進めると安全です。
1. テストを書く(または確認する)
→ 動作が変わっていないことを確認するため
→ 前回学んだJUnitが役立つ
2. 小さな変更を少しずつ行う
→ 一度に大きく変えると何が原因でバグが起きたかわからなくなる
3. テストを実行して動作を確認する
→ 毎回のリファクタリング後にテストを実行する
4. コミットする
→ 動作する状態でバージョン管理に保存する
5. 1〜4を繰り返す
リファクタリングをするべきでない状況もあります。
・デッドラインが迫っているとき(後でやる)
・テストがないとき(先にテストを書く)
・動作がそもそも正しくないとき(先にバグを直す)
コードの解説
Before.generateReport は1つのメソッドの中にバリデーション・集計・フォーマット・出力のすべてが詰め込まれており、非常に読みにくい状態です。
After では「メソッドの抽出」によって filterValidStudents・buildReport・formatStudentLine・appendAverageScore に分割しました。それぞれのメソッドが1つのことだけをするため、読みやすくなっています。また int[] の代わりに StudentScore recordを使う「パラメータオブジェクト」により、row[0]・row[1]・row[2] という意味のわからないアクセスが id()・name()・score() という意図の明確なアクセスに変わっています。
ReportGenerator では「クラスの抽出」を行い、レポート生成のロジックを専用クラスに移しました。header()・body()・footer() というメソッドに分かれており、コードを読むだけでレポートの構造が把握できます。
if (students.isEmpty()) return; は「ガード節の導入」です。ネストを増やさず、早期リターンで処理を分岐させることでコードが読みやすくなります。
まとめ
リファクタリングは「メソッドの抽出」「変数の抽出」「パラメータオブジェクト」「クラスの抽出」「ガード節の導入」などの手法を使って、動作を変えずにコードを改善する作業です。リファクタリングは一度に大きく変えるのではなく、テストで動作を確認しながら少しずつ進めることが重要です。クリーンコード・SOLID原則・デザインパターンで学んだ知識は、リファクタリングの目標地点を示してくれるものです。コードを書き続ける中で、少しずつ改善する習慣を身につけていきましょう。
2026/08/12(水) ぱんまる


コメント