【プログラミングしてみよう64】Javaのリファクタリング入門

Java

前回は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(水) ぱんまる

コメント

タイトルとURLをコピーしました