【6日でできるJava入門】クイズゲームを完成させる(クラス化)


序章

 ここまでのクイズゲームプログラムは、すべての処理がmain()メソッドに直書きされていました。しかし、プログラムが大きくなると保守や拡張が難しくなります。
 ここではリファクタリング(=構造の整理・改良)として、「クラス化」を行い、役割ごとに分担することで、読みやすく・修正しやすく・拡張しやすいプログラムへと進化させます。

また、クラス化には以下のメリットがあります。

項目内容
保守性の向上クラスごとに役割が明確なので、バグ修正や機能追加が容易
再利用性の向上よく使う処理(問題読み込みや出題処理など)はクラスとして他のプロジェクトでも使いやすい。
拡張性の向上新ジャンルの追加や機能追加(例:スコアランキング)も最小限の修正で実装できる。
可読性の向上コードが整理され、全体の流れや各部品の関係が把握しやすくなる。

ここでは、クイズゲームを次のようなクラス構造に分割します。

  • QuizGame(ゲーム全体の管理、mainメソッド担当)
  • QuizManager(問題データの管理、出題処理担当)
  • QuizGenre(ジャンル情報の管理)
  • 必要に応じてユーティリティクラス(例:入力受付)

1.リファクタリングとクラス化

1.1. リファクタリングとは

リファクタリングとは、「動作はそのままで、コードの構造を整理・改善する作業」です。
Javaでは大規模開発や保守のため、クラス化やメソッドの分割によるリファクタリングが不可欠です。

1.2. クラス化のメリット

  • 部品化して再利用可能に
  • 処理の責任範囲が明確になる。
  • 役割ごとに修正や拡張がしやすい。

2.クラス設計と役割

2.1. QuizGameクラス(全体管理・main)

  • プログラムの入口
  • ジャンル選択やチートモード判定
  • ゲーム全体の流れを制御

2.2. QuizManagerクラス(問題読み込み・出題)

  • 問題ファイルの読み込み・シャッフル
  • 問題の出題・解答受付・採点
  • 結果表示

コンストラクタの役割

  • 問題ファイルのパスやジャンル名・問題数などを受け取り、内部でデータを初期化

2.3. QuizGenreクラス(ジャンル情報)

  • 各ジャンルのファイル名・表示タイトル・問題数などを持つ
  • 今後ジャンルを増やす際も管理が楽になる

2.4. 改造のポイント

  • QuizManagerを用意し、QuizGameからジャンルやチートモードを指定して実行
  • 入力や選択肢生成、スコア集計などのロジックを各クラスで分担
  • 問題ごとにArrayListからランダムに出題・選択肢作成
  • QuizGenreをenumで実装するとさらに拡張しやすい

3.プログラム例

ファイル名:QuizGame.java

import java.util.Scanner;

public class QuizGame {
    public static void main(String[] args) throws Exception {
        boolean cheatMode = (args.length > 0 && args[0].equals("cheat"));
        System.out.println("ジャンルを選んでください:");
        System.out.println("1: 県庁所在地クイズ");
        System.out.println("2: 世界史クイズ");
        System.out.println("3: 日本史クイズ");
        System.out.print("==> ");

        Scanner sc = new Scanner(System.in);
        int genre = 0;
        while (true) {
            try {
                genre = Integer.parseInt(sc.nextLine());
                if (genre >= 1 && genre <= 3) break;
                System.out.print("1~3の数字で選んでください。\n==> ");
            } catch (Exception e) {
                System.out.print("数字を入力してください。\n==> ");
            }
        }

        QuizGenre quizGenre = QuizGenre.fromInt(genre);
        QuizManager manager = new QuizManager(
            quizGenre.getFileName(),
            quizGenre.getTitle(),
            quizGenre.getQuestionCount(),
            cheatMode
        );
        manager.startQuiz();
    }
}

ファイル名:QuizGenre.java

public enum QuizGenre {
    PREFECTURE(1, "prefectures.txt", "次の都道府県の県庁所在地はどれ?", 10),
    WORLD_HISTORY(2, "world_history.txt", "世界史クイズ:次の解答群で正しいものはどれ?", 10),
    JAPANESE_HISTORY(3, "japanese_history.txt", "日本史クイズ:次の解答群で正しいものはどれ?", 10);

    private int code;
    private String fileName;
    private String title;
    private int questionCount;

    QuizGenre(int code, String fileName, String title, int questionCount) {
        this.code = code;
        this.fileName = fileName;
        this.title = title;
        this.questionCount = questionCount;
    }
    public String getFileName() { return fileName; }
    public String getTitle() { return title; }
    public int getQuestionCount() { return questionCount; }
    public static QuizGenre fromInt(int code) {
        for (QuizGenre g : QuizGenre.values()) {
            if (g.code == code) return g;
        }
        return PREFECTURE;
    }
}

ファイル名:QuizManager.java

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;

public class QuizManager {
    private String fileName;
    private String title;
    private int questionCount;
    private boolean cheatMode;
    private ArrayList<String> questions = new ArrayList<String>();
    private ArrayList<String> answers = new ArrayList<String>();

    public QuizManager(String fileName, String title, int questionCount, boolean cheatMode) throws Exception {
        this.fileName = fileName;
        this.title = title;
        this.questionCount = questionCount;
        this.cheatMode = cheatMode;
        loadQuestions();
    }

    private void loadQuestions() throws Exception {
        BufferedReader br = new BufferedReader(new FileReader(fileName));
        String line;
        while ((line = br.readLine()) != null) {
            int idx = line.indexOf(',');
            questions.add(line.substring(0, idx));
            answers.add(line.substring(idx + 1));
        }
        br.close();
    }

    public void startQuiz() {
        int score = 0;
        Random rand = new Random();
        Scanner sc = new Scanner(System.in);
        ArrayList<String> qs = new ArrayList<>(questions);
        ArrayList<String> as = new ArrayList<>(answers);
        for (int i = 1; i <= questionCount; i++) {
            if (qs.size() == 0) break;
            int qIdx = rand.nextInt(qs.size());
            String questionText = qs.remove(qIdx);
            String correctAns = as.remove(qIdx);

            ArrayList<String> choices = new ArrayList<>();
            ArrayList<String> backupAnswers = new ArrayList<>(as);
            for (int j = 0; j < 3 && backupAnswers.size() > 0; j++) {
                int cIdx = rand.nextInt(backupAnswers.size());
                choices.add(backupAnswers.remove(cIdx));
            }
            int insertIdx = rand.nextInt(choices.size() + 1);
            choices.add(insertIdx, correctAns);

            System.out.println(i + "問目:" + title);
            System.out.println("「" + questionText + "」");
            for (int j = 0; j < choices.size(); j++) {
                System.out.println((j + 1) + ":" + choices.get(j));
            }

            if (cheatMode) {
                System.out.println("【チートモード】正解は「" + correctAns + "」です。");
            }

            int answer = 0;
            while (true) {
                System.out.print("==> ");
                try {
                    answer = Integer.parseInt(sc.nextLine());
                    if (answer >= 1 && answer <= 4) break;
                    System.out.println("1~4の数字で選んでください。");
                } catch (Exception e) {
                    System.out.println("数字を入力してください。");
                }
            }
            answer--;

            if (answer == insertIdx) {
                score++;
                System.out.println("正解です!");
            } else {
                System.out.println("不正解。正解は「" + correctAns + "」です。");
            }
            System.out.println();
        }
        System.out.println("全" + questionCount + "問終了!あなたの得点は " + score + " 点です。");
    }
}

4.各クラスの詳細解説

  • QuizGame
    ・mainメソッドで全体の流れを管理
    ・ジャンル選択やチートモード判定、QuizManagerの生成・実行を行う
  • QuizGenre(enum)
    ・各ジャンルの設定をまとめて管理
    ・拡張しやすく、ジャンル追加も簡単
  • QuizManager
    ・問題・選択肢の生成と出題、採点、スコア表示
    ・コンストラクタでファイル名・タイトル・問題数・チートモードを受け取る。
    loadQuestions()でデータ読込、startQuiz()で出題・集計

5.コンストラクタについて

QuizManagerのコンストラクタ

  • ファイル名・タイトル・出題数・チートモードをパラメータで受け取り、メンバ変数に保存
  • インスタンス生成時に自動でloadQuestions()を呼び出し、データを初期化

6.改造のポイント

  • mainメソッドの肥大化を避け、責任を分散
  • ジャンル追加時はQuizGenreに追加するだけでOK
  • 入力関連や出題ロジックの整理で、保守・拡張性アップ

7.クイズゲームの実行方法

 今回のサンプルプログラムでは、「cheat」というキーワードをコマンドライン引数として指定すると、チートモードで動作します。

コマンドラインで実行する場合

引数が「cheat」ならチートモード、それ以外は通常モードで動作します。

# チートモード(正解を表示)
java QuizGame cheat

# 通常モード(正解は表示しない)
java QiuizGame

Eclipseで実行する場合

メニューの「実行」→「実行構成」をクリックします。

「引数」のタブで、「プログラムの引数」に「cheat」と入力し、「実行」ボタンをクリックします。

実行結果例

ジャンルを選んでください:
1: 県庁所在地クイズ
2: 世界史クイズ
3: 日本史クイズ
==> 3
1問目:日本史クイズ:次の解答群で正しいものはどれ?
「平安京を建設した天皇」
1:藤原定家
2:桓武天皇
3:寛政の改革
4:ポーツマス条約
【チートモード】正解は「桓武天皇」です。
==> 2
正解です!

2問目:日本史クイズ:次の解答群で正しいものはどれ?
「戦国時代を終結させた大名」
1:徳川家康
2:大化
3:伊藤博文
4:板垣退助
【チートモード】正解は「徳川家康」です。
==> 

まとめ

 このようにクラス設計をしっかり行うことで、「長期間の運用」「機能追加」「バグ修正」などがとてもやりやすくなります。Javaの学習や仕事で複雑なプログラムに出会ったときは、「クラス化して整理する」というアプローチが非常に役立ちます。