4人チームでクイズアプリを作る(Gitチーム開発発展)

2026年9月16日

広告

Gitチーム開発シリーズ:なぜGitを使うのかWinFormsで2人開発を体験するコンフリクトを体験して解消するブランチを体験するPull Requestを体験するよくある失敗とFAQ|4人チームでクイズアプリを作る(今ここ)|[目次へ]


前提:以下の記事を読んでいるとスムーズです。


はじめに

チーム開発の練習題材として最適なのが「クイズアプリ」です。

本記事では、GitHub Desktopを使った最小運用ルールと、4〜5人での役割分担を取り入れながら、WinFormsでクイズアプリを完成させるチュートリアルを紹介します。役割は基本的に「リーダー+担当A〜D」の5役で構成し、4人チームの場合はリーダーが担当A〜Dのいずれか1つを兼任します。

このチュートリアルを通じて、以下のスキルを習得できます:

  • WinForms の基本操作(ボタンやラベルなどのUI部品の扱い)
  • 複数クラスによる責務分担(問題管理・判定処理・スコア管理・UI制御の分離)
  • チーム開発の基本プロセス(役割ごとの実装と統合、GitHub Desktopを使ったバージョン管理)
  • 設計原則の基礎理解(単一責任の原則やUIとロジックの分離)

Step 1. プロジェクト準備

リーダーが行う作業

  1. 新規 WinForms アプリを作成
    • プロジェクト名:TeamQuizApp
    • テンプレートは「Windows フォーム アプリ」を選びます。
    – 注意:
    「Windows フォーム アプリケーション (.NET Framework)」という似た名前のテンプレートもありますが、
    今回は選びません。 今回の教材では、GitHub Desktop を使ったチーム開発の流れを学ぶため、
    .csproj の変更が起きにくい現在の .NET 形式を使用します。
  2. フォームに配置するUI
コントロール名前
LabelquestionLabel
Button ×4answerButton1 ~ answerButton4
ListBoxlogListBox

※名前は「意味+型」の順(例:questionLabel)で、型を接頭辞にする記法(lblQuestion など)は避けています。

3. リポジトリ作成と共有

  • GitHub Desktopでリポジトリを作成し、初期Push。(01-WinFormsで2人開発を体験するの「プロジェクト作成とGit初期化」を参考に)
  • このとき、「Keep this code private」のチェックを外し、public で作成します(練習用のためpublicで問題ありません)。
  • main ブランチを保護しておきます。設定内容はGitHub Rulesetsでmainブランチを保護する を参考に、次の3つだけ設定します。
    • Restrict deletions → チェックボックスをオンにするだけ
    • Require a pull request before merging → チェックボックスをオンにすると追加設定が表示されるので、Required approvals の欄に1 を入力する
    • Block force pushes → チェックボックスをオンにするだけ
  • メンバー全員がCloneして開発スタート

Step 2. CSVファイルの用意

リーダーが行う作業

チーム全員が同じ問題データを使えるように、リーダーが最初にquestions.csvを用意します。

プロジェクト直下にquestions.csvを作成し、プロパティで「出力ディレクトリにコピー → 常にコピー」に設定します。

担当Aは、このCSVを読み込むQuestionLoaderを実装します。

なぜ「出力ディレクトリにコピー → 常にコピー」に設定するのか?

プログラムを実行すると、アプリはプロジェクト直下ではなく、出力フォルダから動きます。

今回のコードでは questions.csv というファイル名だけで読み込むため、実行ファイルと同じ場所に questions.csv が必要です。

そこで、questions.csv のプロパティで
「出力ディレクトリにコピー → 常にコピー」
に設定します。

これにより、CSVを編集した内容が実行時のフォルダへ確実にコピーされ、ファイルが見つからないエラーを防げます。

questions.csv のサンプル

question,choice1,choice2,choice3,choice4,correctIndex
日本の首都はどこ?,大阪,東京,名古屋,札幌,1
C#で文字列を扱う型は?,int,string,bool,char,1
Unityでシーン切り替えに使うメソッドは?,LoadScene,ChangeScene,OpenScene,StartScene,0

注意:選択肢にカンマを含めると Split(',') で正しく分割できません。現状のサンプルでは問題ありませんが、データ作成時はカンマを含めないようにしてください。


Step 3. チームでの役割分担

図の見方
図では役割を分かりやすくするため、「リーダー」と「担当A〜D」を別々に描いています。
4人チームでは、リーダーが担当A〜Dのいずれかを兼任します。

ブランチ命名

ブランチ名担当・内容
feature/question-loaderCSVから問題読み込み(担当A)
feature/answer-checker解答判定(担当B)
feature/score-managerスコア集計(担当C)
feature/ui-updaterUI更新(担当D)
feature/home-updaterリーダー:ホーム位置更新・統合

役割別担当

役割担当内容難易度
リーダー(L)プロジェクト作成・UI配置Form1 のイベントハンドラは空実装、各メンバーのコードをレビュー&統合
担当A問題読み込みQuestion.cs と QuestionLoader.cs を作成、CSVから問題を読み込む処理を実装
担当B解答判定AnswerChecker.cs を作成、正解/不正解を判定する処理を実装
担当Cスコア管理ScoreManager.cs を作成、正解数や正答率を管理
担当DUI更新UiUpdater.cs を作成、問題文・選択肢の表示や、ログ出力を担当

役割分担の目安

難易度の目安は、担当B(低)→ 担当C・D(中)→ 担当A・リーダー(高)です。理解度に応じて役割を決めると、スムーズに進められます。

基本は5人チームです。

  • リーダー:プロジェクト作成・UI配置・統合レビュー
  • 担当A〜D:それぞれの担当クラスを実装

4人チームでは、リーダーが担当A〜Dのいずれかを兼任します。
例:リーダー+担当A、担当B、担当C、担当D

3人チームでは、さらに1人が2つの担当を兼任します。
例:担当B+担当C

同時作業を安全に進めるコツ

担当するクラスとブランチが分かれているため、基本的には全員が同時に作業できます。

次の2点を守りましょう。

  1. 作業を始める前に、最新のmainを自分の作業ブランチへ取り込む次の順番で操作します。
    1. mainに切り替えます。
    2. 「Fetch origin」で更新を確認し、「Pull origin」が表示されたらクリックします。
    3. 自分の作業ブランチに戻ります。
    4. 最新のmainの変更を、自分の作業ブランチへ取り込みます(マージ)。
    5. 取り込みが完了したら、作業を再開します。
    新しく作業ブランチを作る場合は、最新にしたmainから作成します。自分の作業ブランチでPullするだけでは、mainに統合された他のメンバーの変更は取り込まれません。詳しい操作はStep 5で確認しましょう。
  2. 自分の担当ファイル以外は触らない同じファイルを複数人で変更すると、競合(Conflict)が発生しやすくなります。

なお、リーダーが行うForm1.csの統合作業は、各担当のPull Requestがレビューされ、mainにMergeされたあとに行います。チーム全員の担当作業がmainに反映されたことを確認してから進めましょう。


Step 4. コード実装

クラス構成

  • モデル:Question
  • データ取得:QuestionLoader
  • 判定:AnswerChecker
  • スコア管理:ScoreManager
  • 画面更新:UiUpdater
  • 統括制御:Form1

動作確認について

実務では、担当したクラスを画面と結合する前に、単体テストやデバッグ実行で確認することがあります。

ただし、この教材では Git の変更差分を分かりやすくするため、各担当者は確認用のFormや別プロジェクトを追加しません。

各担当の処理は、リーダーが Form1 に接続したあと、画面操作でまとめて確認します。


QuestionLoader.cs(担当A)

担当Aは、次のQuestionクラス(データモデル)とQuestionLoaderクラス(CSV読み込み)の両方を作成します。

Question クラス

  • 役割:1問分のデータを保持するためのモデル。
  • 内容:問題文 (Text)、選択肢 (Choices)、正解インデックス (CorrectIndex) をプロパティとして持つ。
  • ポイント:クイズ問題をオブジェクトとして扱えるようになる。

QuestionLoader クラス

  • 役割:CSVファイルから問題を読み込み、ランダムに1問を取り出す処理を提供。
  • 内容:List に問題を保持し、GetRandomQuestion() でランダムに返す。
  • ポイント:データの取得とアプリ本体の処理を分離している。
using System;
using System.Collections.Generic;
using System.IO;

namespace TeamQuizApp
{
    public class Question
    {
        public string Text { get; set; } = "";
        public string[] Choices { get; set; } = new string[4];
        public int CorrectIndex { get; set; }
    }

    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];
        }
    }
}

パス指定について

上記コードでは、AppContext.BaseDirectory を使って、実行ファイルのあるフォルダを起点に questions.csv を探しています。

そのため、questions.csv「出力ディレクトリにコピー → 常にコピー」 にしておけば、Visual Studio から実行したときもファイルを見つけやすくなります。

エラー処理について

このサンプルでは、学習しやすさを優先して、CSVの列数不足や点数に変換できない値への対応は省略しています。

実際のアプリでは、CSVの内容が正しいかを確認し、不正な行は読み飛ばすなどの処理を追加すると安全です。

余裕があるチームは、エラー処理にも挑戦してみましょう。


AnswerChecker.cs(担当B)

  • 役割:ユーザーが選んだ答えが正解かどうかを判定する。
  • 内容:CheckAnswer(Question q, int index) メソッドで比較して true/false を返す。
  • ポイント:判定処理を独立させることで、UIに依存しないロジックになる。
namespace TeamQuizApp
{
    public class AnswerChecker
    {
        public bool CheckAnswer(Question q, int selectedIndex)
        {
            return q.CorrectIndex == selectedIndex;
        }
    }
}

ScoreManager.cs(担当C)

  • 役割:正解数や解答数を記録・集計する。
  • 内容:CorrectCount・TotalCount をカウントし、GetResult() で結果を文字列に整形。
  • ポイント:成績を管理する責務を一元化している。
namespace TeamQuizApp
{
    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}%)";
        }
    }
}

UiUpdater.cs(担当D)

  • 役割:画面表示の更新を担当。
  • 内容:問題文をラベルに表示、ボタンに選択肢をセット、リストボックスにログを追加する処理を持つ。
  • ポイント:UI更新をまとめて管理することで、Form本体のコードがシンプルになる。
using System.Windows.Forms;

namespace TeamQuizApp
{
    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);
        }
    }
}

Form1.cs(リーダー統合)

  • 役割:全体の流れを制御する中核。
  • 内容:ロード時にQuestionLoaderで問題を読み込み、ボタンイベントで回答チェックを実行、次の問題へ進む。
  • ポイント:各部品(Loader/Checker/Score/UI)を組み合わせ、イベント駆動でアプリを動かす。
using System;
using System.Windows.Forms;

namespace TeamQuizApp
{
    public partial class Form1 : Form
    {
        private readonly QuestionLoader loader;
        private readonly AnswerChecker checker;
        private readonly ScoreManager score;
        private readonly UiUpdater ui;

        private Question current;

        public Form1()
        {
            InitializeComponent();

            loader = new QuestionLoader();
            checker = new AnswerChecker();
            score = new ScoreManager();
            ui = new UiUpdater(questionLabel,
                new[] { answerButton1, answerButton2, answerButton3, answerButton4 },
                logListBox);

            LoadNextQuestion();
        }

        private void answerButton_Click(object sender, EventArgs e)
        {
            var btn = sender as Button;
            int index = Array.IndexOf(new[] { answerButton1, answerButton2, answerButton3, answerButton4 }, btn);

            bool result = checker.CheckAnswer(current, index);
            score.Record(result);

            ui.LogResult(result ? "正解!" : "不正解...");
            ui.LogResult(score.GetResult());

            LoadNextQuestion();
        }

        private void LoadNextQuestion()
        {
            current = loader.GetRandomQuestion();
            ui.ShowQuestion(current);
        }
    }
}

重要:すべての回答ボタン(answerButton1~4)の Click イベントを answerButton_Click に設定しておきましょう。

設定手順

  1. Visual Studioのデザイナー画面で、answerButton1を選択します。
  2. プロパティウィンドウ上部の「⚡イベント」(稲妻マーク)を開きます。
  3. Clickの欄にanswerButton_Clickと入力し、Enterキーを押します。すでに作成済みの場合は、右側のドロップダウンから選択します。
  4. answerButton2answerButton4にも、同じanswerButton_Clickを設定します。

これで、4つのボタンすべてが同じイベントハンドラを共有します。どのボタンを押しても、answerButton_Clickが呼び出されます。


Step 5. GitHub Desktopでの運用ルール

基本の流れ
mainを最新化 → 作業ブランチへ反映 → 編集 → Commit → Push → PR

特に重要なのが、誰かのPRがmainにマージされた後の同期です。以下で詳しく確認します。

誰かのPRがmainにマージされたら

他のメンバーの変更を自分の作業にも反映するため、次の順番で操作しましょう。

① 全員:mainを最新の状態にする

  1. GitHub Desktopで、ブランチをmainに切り替えます。
  2. Fetch originをクリックして、GitHub上の更新を確認します。
  3. Pull originが表示されたらクリックし、自分のパソコンのmainに変更を取り込みます。更新がなければPullは不要です。

② 作業中のブランチがある人:mainの変更を取り込んで再開する

  1. 自分の作業ブランチに戻ります。
  2. Branch」メニューから「Update from main」を実行し、最新のmainの変更を作業ブランチへ取り込みます(マージ)
  3. 競合が発生した場合は、画面の指示に従って解消します。
  4. 取り込みが完了したら、作業を再開します。

③ 新しい作業を始める人:mainからブランチを作る

最新にしたmainから新しい作業ブランチを作成し、そのブランチで作業を始めます。

注意:自分の作業ブランチでPullするだけでは、mainに統合された他のメンバーの変更は取り込まれません。

覚える順番は「mainを最新化 → 作業ブランチへ反映 → 作業再開」です。下の7ステップの同期フロー図を見ながら操作してみましょう。

誰かがmainにマージした後の同期フロー詳細手順(GitHub Desktop)

※GitHub Desktopのバージョンや状態によって表示名が異なる場合があります。Update from main が表示されない場合は、作業ブランチ上で main を取り込む操作を行います。

Pull Requestはどこで作る?
GitHub Desktopで 「Preview Pull Request」→「Create Pull Request」 と進むとブラウザが開きます。
その後、GitHub上でタイトル・説明を入力してPull Requestを作成します。

GitHub Desktop:ブランチ操作・Commit・Push・PR作成の開始
GitHub(ブラウザ):PR作成の確定・レビュー・Approve・Merge

Pushしたら、PRを作成してレビューを受け、mainへ統合します。PRがすでにある場合は、同じブランチへのPushで更新されます。

Commit前に確認すること

  • クラスファイルを追加したら、すべて保存(Ctrl+Shift+S)してから Commit する。プロジェクトファイル(.csproj)の更新も含めて保存される
  • 自分の担当クラス以外は触らない
  • コミットメッセージは、日本語で簡潔に「何を変更したか」が分かるように書きます。
    コミットメッセージ例:
    • QuestionLoader.csを追加(担当A)
    • AnswerChecker.csを実装(担当B)

これでコンフリクトは最小限に抑えられます。


Step 6. Rulesetsを試してみる(体験)

Step 1で設定したmain ブランチのRulesetsが、実際にどう働くのかをチームで体験してみましょう。次の2つは、ぜひ全員で試してみてください。

実験1:mainへ直接pushしてみる

誰か1人が、わざとmain ブランチのままファイルを1行変更し、Commit → Pushしてみます。

1. mainブランチのままファイルを編集する
2. Commitする
3. Pushする
→ Pushが拒否され、Pull Requestの作成を促すメッセージが表示される

「mainには直接pushできない」ことを、実際に体験できます。

実験2:承認なしでMergeしてみる

誰かがPull Requestを作成した直後、まだ誰も承認していない状態で、GitHub上のMerge pull request ボタンを押してみます。

1. Pull Requestを作成する(まだ誰もApproveしていない)
2. Merge pull requestボタンを押してみる
→ ボタンが押せない、または「At least 1 approving review is required」と表示される
3. 別のメンバーがApproveする
→ ボタンが押せるようになる

承認前後でボタンの状態が変わる様子を見ることで、「レビューが仕組みとして機能している」ことが実感できます。


まとめ

  • データは CSVファイル、コードは クラスごとに分担 → 競合を回避できる
  • GitHub Desktopの最小運用ルールを守れば、初心者でも安全にチーム開発可能
  • 各クラスが責務を持つため、オブジェクト指向の理解にもつながる

参考(設計パターン)

このクイズアプリの構成では、以下の設計原則やパターンが関連しています。

単一責任の原則 (SRP)

各クラスがそれぞれ「問題読み込み」「解答判定」「スコア管理」「UI更新」という明確な責務に集中しており、SRP に基づいて設計されていると言えます。

ファサード (Facade)

Form1.cs が各担当クラスの機能をまとめて統合している様子から、複雑な内部構造を簡潔に扱うという点で ファサードパターン の考え方に似ています。

ロジックとUIの分離

処理ロジック(QuestionLoader, AnswerChecker, ScoreManager)と UI更新(UiUpdater, Form1)が明確に分離されており、MVP 的な分離の考え方に近しい設計とも見なせます。


訪問数 340 回, 今日の訪問数 102回

広告