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 に発展させるときのイメージを持つ
といった方向に進めます。
