基礎から学ぶC#入門 90日コース | アプリを作りながら学ぶ - Day 80:リファクタリング

Web APP 90日で身につけるC#
スポンサーリンク
スポンサーリンク

Day 80 のゴールと全体像

Day 80 では、ここまで育ててきたタスク管理アプリを 「きれいに片づける日」 にします。 コードそのものの機能はほとんど変えずに、

  • フォルダ構成
  • クラスの置き場所
  • 名前空間の整理

を通して、読みやすく・育てやすいプロジェクト構成に整えていきます。

今回のターゲット構成は次のとおりです。

  • Models
  • Services
  • Repositories
  • Utilities
  • Program.cs(ルート)

「家の中のものを、ちゃんと棚にしまう」ようなイメージで、 コードを気持ちよく並べ替えていきましょう。

なぜフォルダ構成を整理するのか

小さなアプリでも「整理整頓」は効いてくる

最初は、1ファイルに全部書いてしまっても動きます。 しかし、機能が増えてくると、

  • どこに何が書いてあるのか分からない
  • 似たような処理があちこちに散らばる
  • 修正したいときに、探すだけで疲れる

という状態になりがちです。

そこで、役割ごとにフォルダを分けることで、

  • 「データの形」は Models
  • 「データの保存・取得」は Repositories
  • 「アプリの振る舞い」は Services
  • 「共通の小さな便利機能」は Utilities
  • 「アプリの入口」は Program.cs

というように、頭の中の地図とプロジェクトの構造を一致させていきます。

フォルダ構成の全体像

目指すプロジェクト構成

最終的に、プロジェクトはこんな感じの見た目になります。

TaskApp/
 ├─ Models/
 │   └─ TaskItem.cs
 ├─ Repositories/
 │   └─ TaskRepository.cs
 ├─ Services/
 │   └─ TaskService.cs
 ├─ Utilities/
 │   └─ InputHelper.cs
 └─ Program.cs

ここからは、フォルダごとに「何を置くか」「どう書くか」を、 順番に見ていきます。

Models:データの「形」を表す場所

TaskItem を Models に移動する

まずは、タスクを表す TaskItem クラスを Models フォルダに移します。

Models/TaskItem.cs

namespace TaskApp.Models
{
    // タスクを表すモデルクラス
    public class TaskItem
    {
        public int Id { get; set; }              // 一意なID
        public string Title { get; set; }        // タスク名
        public string Description { get; set; }  // 詳細メモ
        public int Priority { get; set; }        // 優先度 (1:高, 2:中, 3:低)
        public DateTime? DueDate { get; set; }   // 期限(nullなら期限なし)
        public bool IsCompleted { get; set; }    // 完了フラグ
        public DateTime CreatedAt { get; set; }  // 作成日時
    }
}
C#

ここで意識したいこと

  • Models には「データの形」だけを置きます。
  • ロジック(保存・検索・表示など)は書きません。
  • こうしておくと、 「タスクってどんな情報を持っているんだっけ?」 と確認したいときに、迷わず Models を見に行けます。

Repositories:データの保存・取得を担当する場所

TaskRepository を Repositories に移動する

次に、タスク一覧を管理する TaskRepositoryRepositories フォルダに移します。

Repositories/TaskRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.IO;
using TaskApp.Models; // TaskItem を使うために Models を参照

namespace TaskApp.Repositories
{
    // タスクの保存・取得を担当するクラス
    public class TaskRepository
    {
        private readonly List<TaskItem> _tasks = new List<TaskItem>();
        private int _nextId = 1;

        // タスク追加
        public TaskItem Add(string title, string description, int priority, DateTime? dueDate)
        {
            var task = new TaskItem
            {
                Id = _nextId++,
                Title = title,
                Description = description,
                Priority = priority,
                DueDate = dueDate,
                IsCompleted = false,
                CreatedAt = DateTime.Now
            };

            _tasks.Add(task);
            return task;
        }

        // 全タスク取得
        public IEnumerable<TaskItem> GetAll()
        {
            return _tasks;
        }

        // IDで検索
        public TaskItem? FindById(int id)
        {
            return _tasks.FirstOrDefault(t => t.Id == id);
        }

        // 削除
        public bool Remove(int id)
        {
            var task = FindById(id);
            if (task == null)
            {
                return false;
            }

            _tasks.Remove(task);
            return true;
        }

        // JSONファイルに保存
        public void SaveToFile(string filePath)
        {
            try
            {
                var options = new JsonSerializerOptions
                {
                    WriteIndented = true
                };

                string json = JsonSerializer.Serialize(_tasks, options);

                File.WriteAllText(filePath, json);
            }
            catch (Exception ex)
            {
                Console.WriteLine("タスクの保存中にエラーが発生しました。");
                Console.WriteLine($"詳細: {ex.Message}");
            }
        }

        // JSONファイルから読み込み
        public void LoadFromFile(string filePath)
        {
            try
            {
                if (!File.Exists(filePath))
                {
                    return;
                }

                string json = File.ReadAllText(filePath);

                var tasks = JsonSerializer.Deserialize<List<TaskItem>>(json);

                if (tasks == null)
                {
                    Console.WriteLine("タスクデータの読み込みに失敗しました。");
                    return;
                }

                _tasks.Clear();
                _tasks.AddRange(tasks);

                if (_tasks.Any())
                {
                    _nextId = _tasks.Max(t => t.Id) + 1;
                }
                else
                {
                    _nextId = 1;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("タスクの読み込み中にエラーが発生しました。");
                Console.WriteLine($"詳細: {ex.Message}");
            }
        }
    }
}
C#

ここで意識したいこと

  • Repositories は「どこに・どうやって保存するか」を担当します。
  • コンソール表示やユーザー入力のことは考えません。
  • 将来、JSONではなくデータベースに変えたくなったときも、 Repository を差し替えるだけで済むような構造を目指します。

Utilities:共通の小さな便利機能をまとめる場所

入力チェックを Utilities に切り出す

Day 79 で作った ReadIntReadDateOrNull のようなメソッドは、 いろいろな場面で使える「小さな便利機能」です。

これを Utilities/InputHelper.cs にまとめてみます。

Utilities/InputHelper.cs

namespace TaskApp.Utilities
{
    // コンソール入力を安全に受け取るためのヘルパークラス
    public static class InputHelper
    {
        // 数値入力を安全に受け取る
        public static int ReadInt(string label)
        {
            while (true)
            {
                Console.Write($"{label}を入力してください: ");
                string text = Console.ReadLine();

                if (string.IsNullOrWhiteSpace(text))
                {
                    Console.WriteLine("入力が空です。数値を入力してください。");
                    continue;
                }

                if (int.TryParse(text, out int value))
                {
                    return value;
                }

                Console.WriteLine("数値として認識できませんでした。もう一度入力してください。");
            }
        }

        // 日付入力を安全に受け取る(未入力ならnull)
        public static DateTime? ReadDateOrNull(string label)
        {
            while (true)
            {
                Console.Write($"{label}を入力してください (yyyy-MM-dd、未入力なら期限なし): ");
                string text = Console.ReadLine();

                if (string.IsNullOrWhiteSpace(text))
                {
                    return null;
                }

                if (DateTime.TryParse(text, out DateTime date))
                {
                    return date;
                }

                Console.WriteLine("日付の形式が正しくありません。例: 2026-09-10 のように入力してください。");
            }
        }
    }
}
C#

ここで意識したいこと

  • Utilities には「どの機能からも呼び出せる、共通の小さな部品」を置きます。
  • static クラスにしておくと、「道具箱」っぽく使えて分かりやすいです。

Services:アプリの振る舞いをまとめる場所

TaskService で「アプリの操作」をまとめる

ここまで、Program が直接 Repository を呼んでいましたが、 少し整理して、アプリの操作をまとめるサービスクラスを作ってみます。

Services/TaskService.cs

using TaskApp.Models;
using TaskApp.Repositories;
using TaskApp.Utilities;

namespace TaskApp.Services
{
    // タスク管理アプリの「振る舞い」をまとめるサービスクラス
    public class TaskService
    {
        private readonly TaskRepository _repository;

        public TaskService(TaskRepository repository)
        {
            _repository = repository;
        }

        // タスク追加の一連の流れ
        public void CreateTask()
        {
            Console.WriteLine("=== タスク追加 ===");

            Console.Write("タスク名を入力してください: ");
            string? title = Console.ReadLine();
            if (string.IsNullOrWhiteSpace(title))
            {
                Console.WriteLine("タスク名は必須です。空では登録できません。");
                return;
            }

            Console.Write("詳細(メモ)を入力してください(空でも構いません): ");
            string? description = Console.ReadLine();

            int priority = InputHelper.ReadInt("優先度 (1: 高, 2: 中, 3: 低)");

            DateTime? dueDate = InputHelper.ReadDateOrNull("期限");

            var task = _repository.Add(
                title,
                description ?? string.Empty,
                priority,
                dueDate
            );

            Console.WriteLine($"タスクを追加しました。ID: {task.Id}");
        }

        // タスク一覧表示
        public void ShowTasks()
        {
            Console.WriteLine("=== タスク一覧 ===");

            var tasks = _repository.GetAll().ToList();

            if (!tasks.Any())
            {
                Console.WriteLine("タスクが登録されていません。");
                return;
            }

            foreach (var t in tasks)
            {
                string status = t.IsCompleted ? "[完了]" : "[未完了]";
                string priorityText = t.Priority switch
                {
                    1 => "高",
                    2 => "中",
                    3 => "低",
                    _ => "不明"
                };
                string dueText = t.DueDate.HasValue
                    ? t.DueDate.Value.ToString("yyyy-MM-dd")
                    : "期限なし";

                Console.WriteLine($"ID: {t.Id}");
                Console.WriteLine($"  状態   : {status}");
                Console.WriteLine($"  優先度 : {priorityText}");
                Console.WriteLine($"  期限   : {dueText}");
                Console.WriteLine($"  タイトル: {t.Title}");
                Console.WriteLine($"  詳細   : {t.Description}");
                Console.WriteLine();
            }
        }

        // ここに編集・削除・完了切り替えなども追加していける
    }
}
C#

ここで意識したいこと

  • Services は「ユーザーが行う操作」をまとめる場所です。
  • Repository に直接触るのではなく、 Service 経由で操作することで、 ロジックの変更や拡張がしやすくなります。

Program.cs:アプリの入口だけに集中させる

Main をすっきりさせる

最後に、Program.cs を「アプリの入口」に専念させます。

Program.cs

using TaskApp.Repositories;
using TaskApp.Services;

namespace TaskApp
{
    internal class Program
    {
        const string TaskFilePath = "tasks.json";

        static void Main()
        {
            var repository = new TaskRepository();
            var service = new TaskService(repository);

            try
            {
                // 起動時にタスクを読み込む
                repository.LoadFromFile(TaskFilePath);

                // メインループ
                Run(service);

                // 終了時にタスクを保存する
                repository.SaveToFile(TaskFilePath);
            }
            catch (Exception ex)
            {
                Console.WriteLine("予期しないエラーが発生しました。アプリを終了します。");
                Console.WriteLine($"詳細: {ex.Message}");
            }
        }

        static void Run(TaskService service)
        {
            while (true)
            {
                Console.WriteLine("=== タスク管理アプリ ===");
                Console.WriteLine("1: タスク追加");
                Console.WriteLine("2: タスク一覧");
                Console.WriteLine("0: 終了");
                Console.Write("選択してください: ");

                string? input = Console.ReadLine();

                if (string.IsNullOrWhiteSpace(input))
                {
                    Console.WriteLine("入力が空です。メニュー番号を入力してください。");
                    Console.WriteLine();
                    continue;
                }

                switch (input)
                {
                    case "1":
                        service.CreateTask();
                        break;
                    case "2":
                        service.ShowTasks();
                        break;
                    case "0":
                        Console.WriteLine("終了します。");
                        return;
                    default:
                        Console.WriteLine("不正な入力です。メニューに表示されている番号を入力してください。");
                        break;
                }

                Console.WriteLine();
            }
        }
    }
}
C#

ここで意識したいこと

  • Program.cs は「アプリの入口」と「メニューのループ」だけに集中させます。
  • 実際の処理は Service に任せることで、 Main がすっきりして読みやすくなります。

Day 80 のまとめ

Day 80 では、タスク管理アプリの フォルダ構成と責務の整理 を行いました。

  • Models に「データの形」をまとめ
  • Repositories に「保存・取得のロジック」を集め
  • Utilities に「共通の小さな便利機能」を置き
  • Services に「アプリの振る舞い」を整理し
  • Program.cs を「入口とメニュー」に専念させました。

コードの中身は大きく変えていないのに、 プロジェクト全体の「見通しの良さ」はかなり変わってきたはずです。

「どこに何があるか分かる」 「どこを直せばいいかすぐに見つかる」

そんな状態は、プログラミングを続けていくうえで、 とても大きな安心感につながります。

ぜひ、フォルダを行ったり来たりしながら、 自分のアプリが「ちゃんとした構造」を持ち始めた感覚を味わってみてください。 その一歩一歩が、みなさんのコードを、 より美しく、より頼もしいものにしていきます。

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