【プログラミングしてみよう62】Javaでクリーンコードを書くためのベストプラクティス

Java

前回はObserver・Strategy・Decoratorのデザインパターンを学びました。

今回は「クリーンコード」について解説します。 動くコードを書けるようになった次のステップとして、「読みやすく・変更しやすく・バグが起きにくい」コードを書くための考え方とテクニックをまとめます。

クリーンコードとは

「クリーンコード」とは、他の人(未来の自分を含む)が読んで理解しやすいコードのことです。ロバート・C・マーチン(Uncle Bob)が著書「Clean Code」で提唱した概念で、以下の特徴を持ちます。

・意図が明確に伝わる命名
・1つのことだけをするメソッド
・適切なコメント
・重複のないコード(DRY原則)
・適切な抽象化

命名のベストプラクティス

変数・メソッド・クラスの名前はコードの読みやすさに直結します。

// 悪い例: 何を表しているかわからない
int d = 7;
List<String> lst = new ArrayList<>();
void proc(List<String> l) { }

// 良い例: 意図が伝わる
int daysUntilExpiration = 7;
List<String> userNames = new ArrayList<>();
void sendEmailToAllUsers(List<String> userNames) { }
// 悪い例: 略語・略称
int cnt;
String usr;
boolean flg;

// 良い例: 略さない
int count;
String userName;
boolean isLoggedIn;
// boolean変数・メソッドは is/has/can で始める
boolean isValid = true;
boolean hasPermission = false;
boolean canEdit() { return true; }

// コレクションは複数形
List<User> users;
Map<String, Order> ordersByUserId;

メソッドの設計原則

// 悪い例: 1つのメソッドが多くのことをしている
void processOrder(Order order) {
    // バリデーション
    if (order == null) throw new IllegalArgumentException();
    if (order.items().isEmpty()) throw new IllegalStateException();
    // 在庫確認
    for (var item : order.items()) { ... }
    // 合計計算
    int total = 0;
    for (var item : order.items()) { total += item.price(); }
    // メール送信
    sendEmail(order.user(), total);
    // DB保存
    repository.save(order);
}

// 良い例: 1つのことだけする小さなメソッドに分割
void processOrder(Order order) {
    validateOrder(order);
    checkStock(order);
    int total = calculateTotal(order);
    notifyUser(order.user(), total);
    saveOrder(order);
}

サンプルコード(Before/After比較)

ファイル作成

「Java50」フォルダに CleanCodeExample.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

import java.util.List;
import java.util.stream.Collectors;

public class CleanCodeExample {

    // ===== Before: 読みにくいコード =====
    static class Before {
        static List<String> get(List<String> l, int n) {
            List<String> r = new java.util.ArrayList<>();
            for (String s : l) {
                if (s != null && !s.isEmpty() && s.length() > n) {
                    r.add(s.toUpperCase());
                }
            }
            return r;
        }
    }

    // ===== After: クリーンなコード =====
    static class After {
        static List<String> getLongWordsUpperCased(List<String> words, int minLength) {
            return words.stream()
                        .filter(word -> isValidWord(word, minLength))
                        .map(String::toUpperCase)
                        .collect(Collectors.toList());
        }

        private static boolean isValidWord(String word, int minLength) {
            return word != null && !word.isEmpty() && word.length() > minLength;
        }
    }

    // ===== Before: マジックナンバー =====
    static class BeforeNumbers {
        static String getStatus(int code) {
            if (code == 1) return "処理中";
            if (code == 2) return "完了";
            if (code == 3) return "エラー";
            return "不明";
        }
    }

    // ===== After: 定数・enumを使う =====
    static class AfterNumbers {
        enum OrderStatus {
            PROCESSING(1, "処理中"),
            COMPLETED(2, "完了"),
            ERROR(3, "エラー");

            final int code;
            final String label;

            OrderStatus(int code, String label) {
                this.code = code;
                this.label = label;
            }

            static OrderStatus fromCode(int code) {
                for (OrderStatus status : values()) {
                    if (status.code == code) return status;
                }
                throw new IllegalArgumentException("不明なコード: " + code);
            }
        }

        static String getStatus(int code) {
            return OrderStatus.fromCode(code).label;
        }
    }

    // ===== Before: ネストが深い =====
    static class BeforeNested {
        static String process(String input) {
            if (input != null) {
                if (!input.isEmpty()) {
                    if (input.length() <= 100) {
                        return input.trim().toUpperCase();
                    } else {
                        return "長すぎます";
                    }
                } else {
                    return "空です";
                }
            } else {
                return "nullです";
            }
        }
    }

    // ===== After: 早期リターンでネストを減らす =====
    static class AfterNested {
        static String process(String input) {
            if (input == null)        return "nullです";
            if (input.isEmpty())      return "空です";
            if (input.length() > 100) return "長すぎます";
            return input.trim().toUpperCase();
        }
    }

    public static void main(String[] args) {
        List<String> words = List.of("hi", "hello", "java", "programming", "a");

        System.out.println("--- Before vs After ---");
        System.out.println("Before: " + Before.get(words, 3));
        System.out.println("After:  " + After.getLongWordsUpperCased(words, 3));

        System.out.println("\n--- マジックナンバー ---");
        System.out.println("Before: " + BeforeNumbers.getStatus(2));
        System.out.println("After:  " + AfterNumbers.getStatus(2));

        System.out.println("\n--- ネストの解消 ---");
        String[] inputs = {null, "", "hello", "a".repeat(101)};
        for (String input : inputs) {
            String before = BeforeNested.process(input);
            String after  = AfterNested.process(input);
            System.out.println("Before: " + before + " / After: " + after);
        }
    }
}

コンパイル

javac CleanCodeExample.java

実行

java CleanCodeExample

実行結果は次のようになります。

--- Before vs After ---
Before: [HELLO, JAVA, PROGRAMMING]
After:  [HELLO, JAVA, PROGRAMMING]

--- マジックナンバー ---
Before: 完了
After:  完了

--- ネストの解消 ---
Before: nullです / After: nullです
Before: 空です / After: 空です
Before: HELLO / After: HELLO
Before: 長すぎます / After: 長すぎます

その他のベストプラクティス

コメントの書き方

// 悪い例: コードの繰り返し(コードを読めばわかる)
// iに1を足す
i++;

// 良い例: 「なぜ」そうするかを説明する
// うるう年の計算では2月を28日として扱う必要があるため+1する
i++;

例外の扱い方

// 悪い例: 例外を握りつぶす
try {
    doSomething();
} catch (Exception e) {
    // 何もしない
}

// 良い例: 例外を適切に処理するかログに残す
try {
    doSomething();
} catch (IOException e) {
    logger.warning("ファイル操作に失敗しました: " + e.getMessage());
    throw new RuntimeException("処理に失敗しました", e);
}

定数の扱い方

// 悪い例: マジックナンバー
if (score >= 60) { ... }
Thread.sleep(1000);

// 良い例: 定数として名前をつける
static final int PASSING_SCORE = 60;
static final long ONE_SECOND_MS = 1000L;

if (score >= PASSING_SCORE) { ... }
Thread.sleep(ONE_SECOND_MS);

コードの重複を避ける(DRY原則)

// 悪い例: 同じロジックが複数箇所に
double calcCircleArea(double r)    { return 3.14159 * r * r; }
double calcCirclePerimeter(double r) { return 2 * 3.14159 * r; }

// 良い例: 定数として1か所に
static final double PI = Math.PI;
double calcCircleArea(double r)    { return PI * r * r; }
double calcCirclePerimeter(double r) { return 2 * PI * r; }

まとめ

クリーンコードの基本は「意図が伝わる命名」「1つのことだけするメソッド」「マジックナンバーを使わない」「ネストを減らす早期リターン」「DRY原則(重複をなくす)」の5つです。どれも「動くコード」を「読めるコード」に変えるための工夫です。コードは書く時間より読まれる時間の方がはるかに長いため、読みやすさへの投資は必ず報われます。今日からコードレビューやリファクタリングの際にこれらの観点を意識してみてください。

2026/08/10(月) ぱんまる

コメント

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