C# | 2週間で身につくアプリを作りながら学ぶC#の基本 - 12日目

C# C#
スポンサーリンク

12日目のゴールとテーマ

12日目のテーマは「クラス同士に“役割分担”をさせて、アプリ全体を整理する」です。
10〜11日目で、Result クラスと List<Result>、LINQ を使って「データをオブジェクトとして扱う」感覚をつかみました。

今日はそこから一歩進めて、

  • 「結果を表すクラス」と「アプリ全体を管理するクラス」を分ける
  • 処理のまとまりをクラス単位で整理する
  • Main を“司令塔”だけにして、ロジックをクラスに任せる

という「オブジェクト指向らしい構造」に近づけていきます。


クラスに“役割”を持たせるという発想

なんでも Program に書くと何がつらいか

今までのコードは、ほとんどが Program クラス(Main のあるクラス)に集まっていました。

  • ログを読む
  • 結果を表示する
  • 集計する
  • 新しい結果を保存する

全部が 1 クラスに詰め込まれていると、
コードが増えるほど「どこで何をしているか」が分かりにくくなります。

クラスごとに“責任”を分ける

そこで、クラスに「役割(責任)」を持たせます。

例えば、こういう分け方ができます。

  • Result … 1 回分の診断結果を表すクラス
  • ResultRepository … 結果をファイルから読み書きするクラス
  • ResultService … 結果の集計や分析を行うクラス

こうしておくと、

  • 「保存の話」は ResultRepository を見ればいい
  • 「集計の話」は ResultService を見ればいい
  • Main は「どの順番で呼ぶか」だけを書く

という構造になり、アプリが大きくなっても整理しやすくなります。


結果の保存・読み込みを担当する ResultRepository

クラスの骨組みを作る

まずは「結果の保存と読み込み」を担当するクラスを作ります。

using System;
using System.Collections.Generic;
using System.IO;

class ResultRepository
{
    private readonly string _logFileName;

    public ResultRepository(string logFileName)
    {
        _logFileName = logFileName;
    }

    public List<Result> LoadAll()
    {
        List<Result> list = new List<Result>();

        if (!File.Exists(_logFileName))
        {
            return list;
        }

        string[] lines = File.ReadAllLines(_logFileName);

        foreach (string line in lines)
        {
            Result r = Result.ParseFromLogLine(line);
            if (r != null)
            {
                list.Add(r);
            }
        }

        return list;
    }

    public void Append(Result result)
    {
        string line = result.ToLogLine();
        File.AppendAllText(_logFileName, line + Environment.NewLine);
    }
}
C#

ここでの重要ポイントを深掘りします。

コンストラクタで「どのファイルを使うか」を決める

public ResultRepository(string logFileName) はコンストラクタです。
このクラスを new するときに、ログファイル名を渡します。

var repo = new ResultRepository("log.txt");
C#

こうしておくと、クラスの中では _logFileName を使えばよく、
「どのファイルを使うか」を外から差し替えられます。

LoadAll は「全部読み込んで List<Result> を返す」

  • ファイルがなければ空の List を返す
  • 1 行ずつ読み込んで Result に変換する
  • 変換できたものだけ List に追加する

という流れを、1 つのメソッドにまとめています。

Append は「1 件分を 1 行として追記する」

Result 側に ToLogLine() を用意しておけば、
Repository 側は「それをファイルに書く」ことだけに集中できます。


Result クラスに「自分をログ形式に変換する」責任を持たせる

文字列変換を Result 側に寄せる

10日目では、ログ 1 行を Result に変換する ParseLogLine を Program 側に書きました。
これを Result クラスの中に移動させます。

using System;

class Result
{
    public DateTime Timestamp;
    public int YesCount;
    public int Total;
    public string TypeCode;

    public string GetTypeLabel()
    {
        switch (TypeCode)
        {
            case "ACTIVE":
                return "超アクティブタイプ";
            case "BALANCE":
                return "バランスタイプ";
            case "INDOOR":
                return "インドアタイプ";
            default:
                return "不明なタイプ";
        }
    }

    public string ToLogLine()
    {
        string timestamp = Timestamp.ToString("yyyy-MM-dd HH:mm");
        return timestamp + "," + YesCount + "/" + Total + "," + TypeCode;
    }

    public static Result ParseFromLogLine(string line)
    {
        string[] parts = line.Split(',');

        if (parts.Length != 3)
        {
            return null;
        }

        string timestampText = parts[0];
        string scorePart = parts[1];
        string typeCode = parts[2];

        string[] scoreParts = scorePart.Split('/');

        if (scoreParts.Length != 2)
        {
            return null;
        }

        if (!int.TryParse(scoreParts[0], out int yesCount))
        {
            return null;
        }

        if (!int.TryParse(scoreParts[1], out int total))
        {
            return null;
        }

        if (!DateTime.TryParse(timestampText, out DateTime timestamp))
        {
            return null;
        }

        Result r = new Result();
        r.Timestamp = timestamp;
        r.YesCount = yesCount;
        r.Total = total;
        r.TypeCode = typeCode;

        return r;
    }
}
C#

ここでの重要ポイントはこうです。

  • ToLogLine は「自分自身をログ1行の文字列に変換する」
  • ParseFromLogLine は「ログ1行から Result を作る“工場”」
  • どちらも Result に強く関係する処理なので、Result クラスの中に置くと自然

これで、「ログ形式 ↔ Result オブジェクト」の変換は
Result クラスに任せられるようになりました。


集計や分析を担当する ResultService

Result の List を受け取って“分析”するクラス

次に、「集計・分析」を担当するクラスを作ります。

using System;
using System.Collections.Generic;
using System.Linq;

class ResultService
{
    private readonly List<Result> _results;

    public ResultService(List<Result> results)
    {
        _results = results;
    }

    public void ShowRecent(int count)
    {
        var latest = _results
            .OrderByDescending(r => r.Timestamp)
            .Take(count);

        Console.WriteLine("=== 最近 " + count + " 件の履歴 ===");

        foreach (var r in latest)
        {
            Console.WriteLine(
                r.Timestamp.ToString("yyyy-MM-dd HH:mm")
                + "  "
                + r.YesCount + "/" + r.Total
                + "  "
                + r.GetTypeLabel()
            );
        }

        Console.WriteLine();
    }

    public void ShowTypeSummary()
    {
        var groups = _results
            .GroupBy(r => r.TypeCode)
            .OrderByDescending(g => g.Count());

        Console.WriteLine("=== タイプ別件数 ===");

        foreach (var g in groups)
        {
            string label = ConvertTypeCodeToLabel(g.Key);
            Console.WriteLine(label + ": " + g.Count() + " 件");
        }

        Console.WriteLine();
    }

    private string ConvertTypeCodeToLabel(string typeCode)
    {
        switch (typeCode)
        {
            case "ACTIVE":
                return "超アクティブタイプ";
            case "BALANCE":
                return "バランスタイプ";
            case "INDOOR":
                return "インドアタイプ";
            default:
                return "不明なタイプ";
        }
    }
}
C#

ここでのポイントを整理します。

  • コンストラクタで List<Result> を受け取り、内部に保持する
  • ShowRecent は「最近 n 件」を表示する
  • ShowTypeSummary は「タイプごとの件数」を表示する
  • LINQ を使って、データ処理を短く書いている

「結果をどう見せるか」「どう集計するか」という“ロジック”を
ResultService に閉じ込めているイメージです。


Main を“司令塔”だけにする

Program.Main の役割をシンプルにする

ここまでクラスを分けたら、Main はかなりスッキリできます。

using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        var repo = new ResultRepository("log.txt");

        List<Result> results = repo.LoadAll();

        var service = new ResultService(results);

        service.ShowRecent(5);
        service.ShowTypeSummary();

        Console.WriteLine("=== 新しい診断を行います ===");

        int yesCount = RunDiagnosis();

        Result newResult = CreateResultFromYesCount(yesCount, 5);

        repo.Append(newResult);

        Console.WriteLine("診断結果を保存しました。");
    }

    static int RunDiagnosis()
    {
        string[] questions =
        {
            "休日は家で過ごすことが多いですか?",
            "一人でいる時間が好きですか?",
            "初対面の人と話すのは少し緊張しますか?",
            "新しい場所に行くより、行き慣れた場所が好きですか?",
            "大人数の飲み会より、少人数で話す方が好きですか?"
        };

        int yesCount = 0;

        for (int i = 0; i < questions.Length; i++)
        {
            Console.WriteLine("第 " + (i + 1) + " 問");
            bool yes = AskYesNo(questions[i]);

            if (yes)
            {
                yesCount++;
            }
        }

        return yesCount;
    }

    static Result CreateResultFromYesCount(int yesCount, int total)
    {
        string typeCode = GetTypeCode(yesCount, total);

        Result r = new Result();
        r.Timestamp = DateTime.Now;
        r.YesCount = yesCount;
        r.Total = total;
        r.TypeCode = typeCode;

        return r;
    }

    static bool AskYesNo(string question)
    {
        while (true)
        {
            Console.WriteLine(question);
            Console.WriteLine("はいなら y、いいえなら n を入力してください:");
            string input = Console.ReadLine();

            if (input == null)
            {
                continue;
            }

            string answer = input.Trim().ToLower();

            if (answer == "y")
            {
                Console.WriteLine("→ はい");
                Console.WriteLine();
                return true;
            }
            else if (answer == "n")
            {
                Console.WriteLine("→ いいえ");
                Console.WriteLine();
                return false;
            }
            else
            {
                Console.WriteLine("y か n で答えてください。");
                Console.WriteLine();
            }
        }
    }

    static string GetTypeCode(int yesCount, int total)
    {
        if (yesCount <= 1)
        {
            return "ACTIVE";
        }
        else if (yesCount <= 3)
        {
            return "BALANCE";
        }
        else
        {
            return "INDOOR";
        }
    }
}
C#

ここで注目してほしいのは、

  • Main は「リポジトリを作る」「結果を読み込む」「サービスを作る」「診断を実行する」「保存する」という“流れ”だけを書いている
  • 保存の詳細は ResultRepository に任せている
  • 集計や表示の詳細は ResultService に任せている
  • Result は「1 件分のデータ」と「ログ変換」を担当している

という「役割分担」がはっきりしていることです。


12日目のまとめ

今日のキーワードを整理します。

役割ごとのクラス分割
Result(データ)、ResultRepository(保存・読み込み)、ResultService(集計・表示)、Program(司令塔)。

コンストラクタで依存を渡す
ResultRepository にファイル名を渡す、ResultService に List<Result> を渡す。
「何を使うか」を外から決められるようにする。

クラスに責任を集約する
ログ変換は Result に、ファイル操作は Repository に、
集計は Service に集めることで、コードの見通しが良くなる。

Main を細くする
Main は「どのクラスをどう組み合わせるか」だけを書く。
細かい処理はクラスに任せる。

ここまで来ると、
「1 ファイルに全部書いたコード」から
「役割ごとにクラスが分かれたアプリ」へと進化しています。


次へのつながり

13日目以降では、

  • 例外処理(try / catch)でエラーに強いコードにする
  • ユーザーにメニューを出して「履歴を見る」「診断する」を選べるようにする
  • ここまでのコンソールアプリを、将来 GUI や Web に発展させるときのイメージを持つ

といった方向に進めます。

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