WinFormsクイズアプリで学ぶクラス分担とチーム開発
はじめに
チーム開発では、全員が同じファイルを同時に編集すると、作業がぶつかりやすくなります。
特に WinForms アプリでは、画面を担当する Form1.cs に処理をすべて書いてしまうと、次のような問題が起こりやすくなります。
- どこに何の処理があるのか分かりにくい
- 複数人で同じファイルを編集してコンフリクトしやすい
- 後から修正するときに影響範囲が広くなる
- UIの処理と計算処理が混ざって読みにくくなる
そこで今回は、クイズアプリを題材にして、処理をいくつかのクラスに分けて実装します。
目的は、単にクイズアプリを完成させることではありません。
「1つのアプリを、役割ごとに分けて作る」
という考え方を体験することが目的です。
今回作るアプリの概要
今回作るのは、4択クイズアプリです。
アプリの基本的な流れは次の通りです。
- CSVファイルから問題を読み込む
- 問題文と4つの選択肢を画面に表示する
- ユーザーが答えのボタンをクリックする
- 正解か不正解かを判定する
- 正解数と解答数を記録する
- 結果を画面に表示する
- 次の問題を表示する
一見すると単純なアプリですが、処理をよく見ると、いくつかの役割に分けられます。
クラス構成
今回のアプリでは、次のようにクラスを分けます。
| クラス名 | 役割 |
|---|---|
Question | 1問分の問題データを表す |
QuestionLoader | CSVファイルから問題を読み込む |
AnswerChecker | 選んだ答えが正しいか判定する |
ScoreManager | 正解数・解答数を管理する |
UiUpdater | 画面表示を更新する |
Form1 | 全体の流れをまとめる |
ポイントは、1つのクラスに何でも書かないことです。
たとえば、CSVを読む処理、答えを判定する処理、スコアを計算する処理、画面を更新する処理をすべて Form1.cs に書くこともできます。
しかし、それでは Form1.cs が大きくなりすぎます。
チーム開発では、処理を分けることで、各担当者が自分の担当範囲を理解しやすくなります。
Questionクラス:1問分のデータを表す
まず、クイズの1問分を表す Question クラスを作ります。
public class Question
{
public string Text { get; set; } = "";
public string[] Choices { get; set; } = new string[4];
public int CorrectIndex { get; set; }
}
このクラスは、1問分の情報をまとめて持つためのクラスです。
Text には問題文を入れます。
Choices には4つの選択肢を入れます。
CorrectIndex には正解の番号を入れます。
たとえば、次のような問題があるとします。
日本の首都はどこ?
大阪
東京
名古屋
札幌
正解は2番目
この情報をプログラムの中で扱いやすくするために、Question クラスを使います。
QuestionLoaderクラス:問題を読み込む
QuestionLoader クラスは、CSVファイルから問題を読み込む担当です。
public class QuestionLoader
{
private readonly List<Question> _questions = new List<Question>();
private readonly Random _rand = new Random();
public QuestionLoader(string path = "questions.csv")
{
string csvPath = Path.Combine(AppContext.BaseDirectory, path);
foreach (var line in File.ReadAllLines(csvPath))
{
if (line.StartsWith("question")) continue;
var cols = line.Split(',');
_questions.Add(new Question
{
Text = cols[0],
Choices = new[] { cols[1], cols[2], cols[3], cols[4] },
CorrectIndex = int.Parse(cols[5])
});
}
}
public Question GetRandomQuestion()
{
int idx = _rand.Next(_questions.Count);
return _questions[idx];
}
}
このクラスの役割は、問題データを用意することです。
Form1 が直接CSVファイルを読むのではなく、QuestionLoader に任せます。
これにより、Form1 は次のように考えればよくなります。
問題が必要になったら QuestionLoader からもらう
CSVファイルの読み込み方法を変更したい場合も、基本的には QuestionLoader を修正すれば済みます。
CSV読み込みで注意すること
このサンプルでは、次のようなCSVを想定しています。
question,choice1,choice2,choice3,choice4,correctIndex
日本の首都はどこ?,大阪,東京,名古屋,札幌,1
C#で文字列を扱う型は?,int,string,bool,char,1
Unityでシーン切り替えに使うメソッドは?,LoadScene,ChangeScene,OpenScene,StartScene,0
correctIndex は、0から数える番号です。
つまり、次のようになります。
| 値 | 意味 |
|---|---|
| 0 | 1つ目の選択肢 |
| 1 | 2つ目の選択肢 |
| 2 | 3つ目の選択肢 |
| 3 | 4つ目の選択肢 |
この点は初学者が間違えやすいところです。
画面上では「1番、2番、3番、4番」と見えていても、配列では 0, 1, 2, 3 で管理します。
AnswerCheckerクラス:答えを判定する
AnswerChecker クラスは、ユーザーが選んだ答えが正しいかどうかを判定します。
public class AnswerChecker
{
public bool CheckAnswer(Question q, int selectedIndex)
{
return q.CorrectIndex == selectedIndex;
}
}
このクラスはとても短いですが、重要な役割を持っています。
答えの判定を Form1 に直接書くのではなく、専用のクラスに分けています。
これにより、判定処理だけを見たいときに、AnswerChecker を見ればよくなります。
また、将来的に次のような変更をしたい場合にも対応しやすくなります。
- 部分点をつける
- 複数正解を認める
- 制限時間によって点数を変える
- 大文字小文字を無視して判定する
今は単純な比較だけですが、処理を分けておくことで、あとから拡張しやすくなります。
ScoreManagerクラス:成績を管理する
ScoreManager クラスは、正解数と解答数を管理します。
public class ScoreManager
{
public int CorrectCount { get; private set; }
public int TotalCount { get; private set; }
public void Record(bool isCorrect)
{
TotalCount++;
if (isCorrect)
{
CorrectCount++;
}
}
public string GetResult()
{
if (TotalCount == 0)
{
return "正解数: 0 / 0 (正答率 0.0%)";
}
return $"正解数: {CorrectCount} / {TotalCount} (正答率 {((double)CorrectCount / TotalCount * 100):F1}%)";
}
}
Record メソッドでは、1回解答するたびに TotalCount を増やします。
正解していれば、CorrectCount も増やします。
GetResult メソッドでは、現在の成績を文字列として返します。
ここで大事なのは、スコア管理を Form1 に直接書かないことです。
スコアの管理を専用クラスに分けることで、Form1 は次のように使えます。
score.Record(isCorrect);
string result = score.GetResult();
Form1 は「記録する」「結果を取得する」という操作だけを知っていればよくなります。
UiUpdaterクラス:画面表示を更新する
UiUpdater クラスは、画面表示を更新するためのクラスです。
public class UiUpdater
{
private readonly Label _questionLabel;
private readonly Button[] _buttons;
private readonly ListBox _log;
public UiUpdater(Label questionLabel, Button[] buttons, ListBox log)
{
_questionLabel = questionLabel;
_buttons = buttons;
_log = log;
}
public void ShowQuestion(Question q)
{
_questionLabel.Text = q.Text;
for (int i = 0; i < 4; i++)
{
_buttons[i].Text = q.Choices[i];
}
}
public void LogResult(string msg)
{
_log.Items.Add(msg);
}
}
このクラスは、問題文をラベルに表示したり、ボタンに選択肢を表示したりします。
画面の更新処理は、どうしても Label や Button などの部品に依存します。
そのため、完全にUIから切り離すことはできません。
しかし、Form1 に画面更新の細かい処理をすべて書くよりも、UiUpdater にまとめた方が読みやすくなります。
Form1 から見ると、次のように使えます。
ui.ShowQuestion(currentQuestion);
ui.LogResult("正解です");
細かい表示処理を UiUpdater に任せることで、Form1 の役割が分かりやすくなります。
Form1クラス:全体の流れをまとめる
Form1 は、アプリ全体の流れを担当します。
ただし、すべての処理を自分で行うのではなく、各クラスを組み合わせて動かします。
イメージとしては、次のような役割です。
Form1
├─ QuestionLoader に問題をもらう
├─ UiUpdater に画面表示を依頼する
├─ AnswerChecker に正誤判定を依頼する
├─ ScoreManager に結果を記録する
└─ UiUpdater に結果表示を依頼する
Form1 は「司令塔」のような役割です。
実際の処理そのものは、それぞれの専門クラスに任せます。
これにより、Form1.cs が極端に大きくなることを防げます。
ボタンがクリックされたときの流れ
ユーザーが答えのボタンをクリックしたとき、内部では次のような流れになります。
- どのボタンが押されたかを確認する
- 選択された番号を取得する
AnswerCheckerで正解かどうかを判定するScoreManagerに結果を記録するUiUpdaterでログを表示する- 次の問題を取得する
- 次の問題を画面に表示する
この流れを文章にすると長く見えますが、クラスを分けておくとコードは読みやすくなります。
たとえば、判定処理は AnswerChecker、スコア処理は ScoreManager、画面表示は UiUpdater というように、担当がはっきりします。
なぜクラスを分けるのか
クラスを分ける理由は、コードをきれいに見せるためだけではありません。
チーム開発では、次のようなメリットがあります。
1. 担当範囲が分かりやすい
担当Aは QuestionLoader、担当Bは AnswerChecker、担当Cは ScoreManager、担当Dは UiUpdater というように、作業範囲を分けることができます。
自分がどのファイルを編集すればよいかが明確になります。
2. 同時作業しやすい
全員が Form1.cs を編集すると、変更がぶつかりやすくなります。
しかし、担当ごとに別ファイルを編集すれば、同時に作業しても衝突しにくくなります。
3. 修正箇所を見つけやすい
たとえば、正答率の表示がおかしい場合は、ScoreManager を確認すればよいと分かります。
問題が読み込めない場合は、QuestionLoader を確認します。
このように、問題が起きたときに調べる場所を絞りやすくなります。
4. 役割ごとに理解できる
初学者にとって、アプリ全体を一度に理解するのは大変です。
しかし、クラスごとに役割が分かれていれば、まずは自分の担当部分から理解できます。
初学者が注意したいポイント
配列の番号は0から始まる
選択肢が4つある場合、配列の番号は次のようになります。
0 → 1つ目の選択肢
1 → 2つ目の選択肢
2 → 3つ目の選択肢
3 → 4つ目の選択肢
CSVの correctIndex も、この番号に合わせる必要があります。
画面上の「1番」と、配列の 0 を混同しないようにしましょう。
CSVにカンマを含めない
今回のサンプルでは、CSVを Split(',') で分割しています。
そのため、問題文や選択肢の中にカンマを入れると、正しく読み込めなくなる場合があります。
たとえば、次のようなデータは注意が必要です。
C#で使う型はどれ?,int,string,bool,char,1
この程度であれば問題ありません。
しかし、選択肢の中にカンマを含めると、列数がずれてしまいます。
学習用のサンプルでは、問題文や選択肢にカンマを入れないようにしましょう。
questions.csv のコピー設定を忘れない
アプリを実行するとき、プログラムはプロジェクトのフォルダではなく、出力フォルダから動きます。
そのため、questions.csv が出力フォルダにコピーされていないと、実行時にファイルが見つからないエラーになることがあります。
Visual Studioで questions.csv を選択し、プロパティから次の設定を確認しましょう。
出力ディレクトリにコピー:常にコピー
または、教材の指示に合わせて適切なコピー設定にします。
自分の担当外のファイルをむやみに変更しない
チーム開発では、自分の担当外のファイルを変更すると、他の人の作業とぶつかることがあります。
特に Form1.cs は統合で使う重要なファイルです。
担当者は、基本的に自分の担当クラスを中心に作業しましょう。
実務との違い
実務では、各クラスを作ったあと、単体テストやデバッグ用コードで動作確認することがあります。
しかし、学習用のチーム開発では、確認用のフォームや別プロジェクトを追加すると、Gitの変更差分が分かりにくくなることがあります。
そのため、この教材では、各担当者が作成したクラスをリーダーが Form1 に接続し、画面操作でまとめて確認する流れにしています。
これは、実務のやり方をすべて再現するためではなく、まずは次のことを学ぶためです。
- 担当ごとにクラスを作る
- 自分の作業範囲を意識する
- 他の人のコードと統合する
- 1つのアプリとして動かす
最初の学習段階では、すべてを完璧にするよりも、役割分担と統合の流れを体験することが大切です。
この課題で学べること
このクイズアプリ制作では、C#の文法だけでなく、開発の考え方も学べます。
特に重要なのは、次の3つです。
1. クラスには役割がある
クラスは、ただファイルを分けるためのものではありません。
「何を担当するのか」を明確にするために使います。
QuestionLoader は問題を読み込む。
AnswerChecker は答えを判定する。
ScoreManager は点数を管理する。
このように役割を分けることで、コード全体が理解しやすくなります。
2. Form1にすべてを書かない
WinFormsでは、つい Form1.cs にすべての処理を書きたくなります。
しかし、それを続けると、アプリが大きくなるほど管理が難しくなります。
画面の処理、データの処理、判定の処理、集計の処理を分けることが大切です。
3. チーム開発では「動くこと」だけでなく「分担しやすいこと」も大切
1人で作る場合は、多少コードがまとまっていなくても動けばよい場面があります。
しかし、チーム開発ではそうはいきません。
他の人が読めること。
担当範囲が分かること。
統合しやすいこと。
変更がぶつかりにくいこと。
これらも重要な品質です。
発展課題
余裕がある場合は、次のような改良にも挑戦できます。
- 問題を重複しないように出題する
- 最後に総合結果を表示する
- 正解・不正解で表示色を変える
- CSVの列数チェックを追加する
- 不正なCSVデータを読み飛ばす
- 問題カテゴリを追加する
- 制限時間を追加する
- 結果をファイルに保存する
ただし、最初から発展課題を入れすぎると、チーム開発の目的がぼやけます。
まずは基本形を完成させることを優先しましょう。
まとめ
今回のクイズアプリでは、1つのアプリを複数のクラスに分けて実装しました。
重要なのは、次の考え方です。
Question → 問題データ
QuestionLoader → 問題の読み込み
AnswerChecker → 正誤判定
ScoreManager → 成績管理
UiUpdater → 画面更新
Form1 → 全体の流れ
このように分けることで、コードの見通しがよくなり、チームでも作業しやすくなります。
チーム開発では、単に「自分のコードが動く」だけでは不十分です。
他の人のコードと組み合わせて、1つのアプリとして動かす必要があります。
そのためには、クラスの役割を意識し、自分の担当範囲を守りながら作業することが大切です。
この課題を通して、C#の文法だけでなく、実際の開発で必要になる「分担」「統合」「責務の分離」の考え方を体験していきましょう。












ディスカッション
コメント一覧
まだ、コメントがありません