基礎から学ぶC#入門 90日コース | 実践力を高める - Day 70:リファクタリング演習

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

Day 70 のゴールと全体像

Day 70 は、いよいよ リファクタリング演習の日です。 ここまで学んできた「単一責任」「DRY」「クラス分割」「責務分離」を、 実際の“悪いコード”にぶつけて、読みやすいコードへ育てていく練習をしていきます。

流れはこうです。

  1. 長いコードを用意する
  2. 問題点を発見する
  3. メソッド分割でスッキリさせる
  4. クラス分割で責務を分ける
  5. 最終的に「読みやすいコード」に仕上げる

ひとつの例をじっくり育てていきますので、 「コードって、あとからこんなふうに整えていくんだ」という感覚を ぜひ味わってみてください。

スタート地点:悪いコード(長いコード)

例題:簡易家計簿を一気に書いてしまったコード

まずは、よくある「勢いで全部 Main に書いてしまった」コードから始めます。

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

class Program
{
    static void Main()
    {
        // 収支データを入れるリスト
        List<string> records = new List<string>();

        while (true)
        {
            Console.WriteLine("1: 収入登録, 2: 支出登録, 3: 一覧表示, 4: 合計表示, 5: 保存, 6: 読み込み, 0: 終了");
            Console.Write("選択してください: ");
            string input = Console.ReadLine();

            if (input == "1")
            {
                Console.Write("収入の金額を入力してください: ");
                string amountText = Console.ReadLine();
                int amount = int.Parse(amountText);

                Console.Write("メモを入力してください: ");
                string memo = Console.ReadLine();

                string record = $"IN,{amount},{memo}";
                records.Add(record);

                Console.WriteLine("収入を登録しました。");
            }
            else if (input == "2")
            {
                Console.Write("支出の金額を入力してください: ");
                string amountText = Console.ReadLine();
                int amount = int.Parse(amountText);

                Console.Write("メモを入力してください: ");
                string memo = Console.ReadLine();

                string record = $"OUT,{amount},{memo}";
                records.Add(record);

                Console.WriteLine("支出を登録しました。");
            }
            else if (input == "3")
            {
                Console.WriteLine("一覧表示:");
                foreach (var r in records)
                {
                    string[] parts = r.Split(',');
                    string type = parts[0];
                    int amount = int.Parse(parts[1]);
                    string memo = parts[2];

                    Console.WriteLine($"{type} {amount}円 {memo}");
                }
            }
            else if (input == "4")
            {
                int totalIncome = 0;
                int totalExpense = 0;

                foreach (var r in records)
                {
                    string[] parts = r.Split(',');
                    string type = parts[0];
                    int amount = int.Parse(parts[1]);

                    if (type == "IN")
                    {
                        totalIncome += amount;
                    }
                    else if (type == "OUT")
                    {
                        totalExpense += amount;
                    }
                }

                Console.WriteLine($"収入合計: {totalIncome}円");
                Console.WriteLine($"支出合計: {totalExpense}円");
                Console.WriteLine($"差引: {totalIncome - totalExpense}円");
            }
            else if (input == "5")
            {
                Console.Write("保存するファイル名を入力してください: ");
                string path = Console.ReadLine();

                File.WriteAllLines(path, records);
                Console.WriteLine("保存しました。");
            }
            else if (input == "6")
            {
                Console.Write("読み込むファイル名を入力してください: ");
                string path = Console.ReadLine();

                if (File.Exists(path))
                {
                    records = new List<string>(File.ReadAllLines(path));
                    Console.WriteLine("読み込みました。");
                }
                else
                {
                    Console.WriteLine("ファイルが見つかりません。");
                }
            }
            else if (input == "0")
            {
                Console.WriteLine("終了します。");
                break;
            }
            else
            {
                Console.WriteLine("不正な入力です。");
            }

            Console.WriteLine();
        }
    }
}
C#

一見すると「ちゃんと動きそう」ですし、 最初に書いたときは「おお、家計簿できた!」と嬉しくなるタイプのコードです。

でも、落ち着いて眺めてみると、

  • Main が長すぎる
  • 収入登録・支出登録・一覧表示・合計計算・保存・読み込み… すべてが一箇所に詰め込まれている
  • データの形式がただの string で、意味が分かりにくい

など、いろいろな“モヤモヤ”が見えてきます。

ここから、少しずつ整えていきます。

ステップ1:問題を発見する

どこがつらいかを言葉にしてみる

リファクタリングの第一歩は、 「どこがつらいのか」を言葉にしてみることです。

このコードの問題点を、いくつか挙げてみます。

  • Main が長い
    • 何をしているのか、一気に理解するのが難しい
  • 単一責任が守られていない
    • 入力、表示、計算、保存、読み込み…すべてを Main が担当している
  • データがただの文字列
    • "IN,1000,ランチ" のような文字列で管理していて、 「これは何のデータなのか」がコードから読み取りづらい
  • DRY が守られていない部分がある
    • 収入登録と支出登録の処理がほぼ同じなのに、 コピペで書かれている

こうして問題を言葉にしておくと、 「どこから手をつければいいか」が見えやすくなります。

ステップ2:メソッド分割でスッキリさせる

まずは「Main の責任を減らす」

最初のリファクタリングとして、 「Main の中の処理をメソッドに切り出す」ことから始めます。

収入登録と支出登録をメソッドにする

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

class Program
{
    static void Main()
    {
        List<string> records = new List<string>();

        while (true)
        {
            Console.WriteLine("1: 収入登録, 2: 支出登録, 3: 一覧表示, 4: 合計表示, 5: 保存, 6: 読み込み, 0: 終了");
            Console.Write("選択してください: ");
            string input = Console.ReadLine();

            if (input == "1")
            {
                AddIncome(records);
            }
            else if (input == "2")
            {
                AddExpense(records);
            }
            else if (input == "3")
            {
                PrintRecords(records);
            }
            else if (input == "4")
            {
                PrintSummary(records);
            }
            else if (input == "5")
            {
                Save(records);
            }
            else if (input == "6")
            {
                records = Load();
            }
            else if (input == "0")
            {
                Console.WriteLine("終了します。");
                break;
            }
            else
            {
                Console.WriteLine("不正な入力です。");
            }

            Console.WriteLine();
        }
    }

    // 収入登録
    static void AddIncome(List<string> records)
    {
        Console.Write("収入の金額を入力してください: ");
        string amountText = Console.ReadLine();
        int amount = int.Parse(amountText);

        Console.Write("メモを入力してください: ");
        string memo = Console.ReadLine();

        string record = $"IN,{amount},{memo}";
        records.Add(record);

        Console.WriteLine("収入を登録しました。");
    }

    // 支出登録
    static void AddExpense(List<string> records)
    {
        Console.Write("支出の金額を入力してください: ");
        string amountText = Console.ReadLine();
        int amount = int.Parse(amountText);

        Console.Write("メモを入力してください: ");
        string memo = Console.ReadLine();

        string record = $"OUT,{amount},{memo}";
        records.Add(record);

        Console.WriteLine("支出を登録しました。");
    }

    // 一覧表示
    static void PrintRecords(List<string> records)
    {
        Console.WriteLine("一覧表示:");
        foreach (var r in records)
        {
            string[] parts = r.Split(',');
            string type = parts[0];
            int amount = int.Parse(parts[1]);
            string memo = parts[2];

            Console.WriteLine($"{type} {amount}円 {memo}");
        }
    }

    // 合計表示
    static void PrintSummary(List<string> records)
    {
        int totalIncome = 0;
        int totalExpense = 0;

        foreach (var r in records)
        {
            string[] parts = r.Split(',');
            string type = parts[0];
            int amount = int.Parse(parts[1]);

            if (type == "IN")
            {
                totalIncome += amount;
            }
            else if (type == "OUT")
            {
                totalExpense += amount;
            }
        }

        Console.WriteLine($"収入合計: {totalIncome}円");
        Console.WriteLine($"支出合計: {totalExpense}円");
        Console.WriteLine($"差引: {totalIncome - totalExpense}円");
    }

    // 保存
    static void Save(List<string> records)
    {
        Console.Write("保存するファイル名を入力してください: ");
        string path = Console.ReadLine();

        File.WriteAllLines(path, records);
        Console.WriteLine("保存しました。");
    }

    // 読み込み
    static List<string> Load()
    {
        Console.Write("読み込むファイル名を入力してください: ");
        string path = Console.ReadLine();

        if (File.Exists(path))
        {
            Console.WriteLine("読み込みました。");
            return new List<string>(File.ReadAllLines(path));
        }
        else
        {
            Console.WriteLine("ファイルが見つかりません。");
            return new List<string>();
        }
    }
}
C#

この段階で、

  • Main は「メニューの制御」だけを担当
  • 実際の処理は、それぞれのメソッドに分かれている

という状態になり、 だいぶ読みやすくなってきました。

ただ、まだ

  • データが string のまま
  • 収入登録と支出登録がほぼ同じロジック
  • ファイル保存・読み込みの責務が Program にべったり

という課題が残っています。

ステップ3:クラス分割で責務を分ける

「家計簿」という概念をクラスにする

次のステップとして、 「家計簿そのものをクラスにする」ことを考えます。

まず、データの形を string ではなく、 きちんとしたクラスにしてしまいましょう。

// 1件分の収支を表すクラス
class Record
{
    public string Type { get; set; }   // "IN" or "OUT"
    public int Amount { get; set; }    // 金額
    public string Memo { get; set; }   // メモ
}
C#

次に、「家計簿全体」を管理するクラスを作ります。

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

class Ledger
{
    private readonly List<Record> _records = new List<Record>();

    // 収入登録
    public void AddIncome(int amount, string memo)
    {
        _records.Add(new Record
        {
            Type = "IN",
            Amount = amount,
            Memo = memo
        });
    }

    // 支出登録
    public void AddExpense(int amount, string memo)
    {
        _records.Add(new Record
        {
            Type = "OUT",
            Amount = amount,
            Memo = memo
        });
    }

    // 一覧取得
    public IEnumerable<Record> GetAll()
    {
        return _records;
    }

    // 合計計算
    public (int income, int expense) GetSummary()
    {
        int totalIncome = 0;
        int totalExpense = 0;

        foreach (var r in _records)
        {
            if (r.Type == "IN")
            {
                totalIncome += r.Amount;
            }
            else if (r.Type == "OUT")
            {
                totalExpense += r.Amount;
            }
        }

        return (totalIncome, totalExpense);
    }

    // 保存
    public void Save(string path)
    {
        var lines = new List<string>();
        foreach (var r in _records)
        {
            lines.Add($"{r.Type},{r.Amount},{r.Memo}");
        }

        File.WriteAllLines(path, lines);
    }

    // 読み込み
    public void Load(string path)
    {
        _records.Clear();

        if (!File.Exists(path))
        {
            return;
        }

        var lines = File.ReadAllLines(path);
        foreach (var line in lines)
        {
            var parts = line.Split(',');
            var record = new Record
            {
                Type = parts[0],
                Amount = int.Parse(parts[1]),
                Memo = parts[2]
            };
            _records.Add(record);
        }
    }
}
C#

この Ledger クラスは、

  • 収支の登録
  • 一覧の管理
  • 合計の計算
  • 保存・読み込み

といった「家計簿のロジック」を担当するクラスです。

Program は「UIの制御」に専念させる

Program 側は、 Ledger を使ってユーザーインターフェイスを制御する役割に絞ります。

using System;

class Program
{
    static void Main()
    {
        var ledger = new Ledger();

        while (true)
        {
            Console.WriteLine("1: 収入登録, 2: 支出登録, 3: 一覧表示, 4: 合計表示, 5: 保存, 6: 読み込み, 0: 終了");
            Console.Write("選択してください: ");
            string input = Console.ReadLine();

            if (input == "1")
            {
                AddIncomeUI(ledger);
            }
            else if (input == "2")
            {
                AddExpenseUI(ledger);
            }
            else if (input == "3")
            {
                PrintRecordsUI(ledger);
            }
            else if (input == "4")
            {
                PrintSummaryUI(ledger);
            }
            else if (input == "5")
            {
                SaveUI(ledger);
            }
            else if (input == "6")
            {
                LoadUI(ledger);
            }
            else if (input == "0")
            {
                Console.WriteLine("終了します。");
                break;
            }
            else
            {
                Console.WriteLine("不正な入力です。");
            }

            Console.WriteLine();
        }
    }

    static void AddIncomeUI(Ledger ledger)
    {
        Console.Write("収入の金額を入力してください: ");
        int amount = int.Parse(Console.ReadLine());

        Console.Write("メモを入力してください: ");
        string memo = Console.ReadLine();

        ledger.AddIncome(amount, memo);
        Console.WriteLine("収入を登録しました。");
    }

    static void AddExpenseUI(Ledger ledger)
    {
        Console.Write("支出の金額を入力してください: ");
        int amount = int.Parse(Console.ReadLine());

        Console.Write("メモを入力してください: ");
        string memo = Console.ReadLine();

        ledger.AddExpense(amount, memo);
        Console.WriteLine("支出を登録しました。");
    }

    static void PrintRecordsUI(Ledger ledger)
    {
        Console.WriteLine("一覧表示:");
        foreach (var r in ledger.GetAll())
        {
            Console.WriteLine($"{r.Type} {r.Amount}円 {r.Memo}");
        }
    }

    static void PrintSummaryUI(Ledger ledger)
    {
        var (income, expense) = ledger.GetSummary();
        Console.WriteLine($"収入合計: {income}円");
        Console.WriteLine($"支出合計: {expense}円");
        Console.WriteLine($"差引: {income - expense}円");
    }

    static void SaveUI(Ledger ledger)
    {
        Console.Write("保存するファイル名を入力してください: ");
        string path = Console.ReadLine();

        ledger.Save(path);
        Console.WriteLine("保存しました。");
    }

    static void LoadUI(Ledger ledger)
    {
        Console.Write("読み込むファイル名を入力してください: ");
        string path = Console.ReadLine();

        ledger.Load(path);
        Console.WriteLine("読み込みました。");
    }
}
C#

この段階で、

  • Ledger が「家計簿のロジック」を担当
  • Program が「コンソールUIの制御」を担当

という、単一責任 + クラス分割 の状態に近づいています。

ステップ4:読みやすさをもう一段階上げる

名前空間とフォルダで整理する

さらに一歩進めて、 フォルダと名前空間で整理してみます。

HouseholdLedger
 ├─ Models
 │   └─ Record.cs
 ├─ Domain
 │   └─ Ledger.cs
 └─ UI
     └─ ConsoleApp.cs
// Models/Record.cs
namespace HouseholdLedger.Models
{
    // 1件分の収支を表すモデル
    class Record
    {
        public string Type { get; set; }   // "IN" or "OUT"
        public int Amount { get; set; }    // 金額
        public string Memo { get; set; }   // メモ
    }
}
C#
// Domain/Ledger.cs
using System.Collections.Generic;
using System.IO;
using HouseholdLedger.Models;

namespace HouseholdLedger.Domain
{
    // 家計簿のロジックを担当するクラス
    class Ledger
    {
        private readonly List<Record> _records = new List<Record>();

        public void AddIncome(int amount, string memo)
        {
            _records.Add(new Record { Type = "IN", Amount = amount, Memo = memo });
        }

        public void AddExpense(int amount, string memo)
        {
            _records.Add(new Record { Type = "OUT", Amount = amount, Memo = memo });
        }

        public IEnumerable<Record> GetAll() => _records;

        public (int income, int expense) GetSummary()
        {
            int income = 0;
            int expense = 0;

            foreach (var r in _records)
            {
                if (r.Type == "IN") income += r.Amount;
                else if (r.Type == "OUT") expense += r.Amount;
            }

            return (income, expense);
        }

        public void Save(string path)
        {
            var lines = new List<string>();
            foreach (var r in _records)
            {
                lines.Add($"{r.Type},{r.Amount},{r.Memo}");
            }
            File.WriteAllLines(path, lines);
        }

        public void Load(string path)
        {
            _records.Clear();

            if (!File.Exists(path)) return;

            var lines = File.ReadAllLines(path);
            foreach (var line in lines)
            {
                var parts = line.Split(',');
                _records.Add(new Record
                {
                    Type = parts[0],
                    Amount = int.Parse(parts[1]),
                    Memo = parts[2]
                });
            }
        }
    }
}
C#
// UI/ConsoleApp.cs
using System;
using HouseholdLedger.Domain;

namespace HouseholdLedger.UI
{
    class ConsoleApp
    {
        private readonly Ledger _ledger = new Ledger();

        public void Run()
        {
            while (true)
            {
                Console.WriteLine("1: 収入登録, 2: 支出登録, 3: 一覧表示, 4: 合計表示, 5: 保存, 6: 読み込み, 0: 終了");
                Console.Write("選択してください: ");
                string input = Console.ReadLine();

                switch (input)
                {
                    case "1":
                        AddIncome();
                        break;
                    case "2":
                        AddExpense();
                        break;
                    case "3":
                        PrintRecords();
                        break;
                    case "4":
                        PrintSummary();
                        break;
                    case "5":
                        Save();
                        break;
                    case "6":
                        Load();
                        break;
                    case "0":
                        Console.WriteLine("終了します。");
                        return;
                    default:
                        Console.WriteLine("不正な入力です。");
                        break;
                }

                Console.WriteLine();
            }
        }

        private void AddIncome()
        {
            Console.Write("収入の金額を入力してください: ");
            int amount = int.Parse(Console.ReadLine());

            Console.Write("メモを入力してください: ");
            string memo = Console.ReadLine();

            _ledger.AddIncome(amount, memo);
            Console.WriteLine("収入を登録しました。");
        }

        private void AddExpense()
        {
            Console.Write("支出の金額を入力してください: ");
            int amount = int.Parse(Console.ReadLine());

            Console.Write("メモを入力してください: ");
            string memo = Console.ReadLine();

            _ledger.AddExpense(amount, memo);
            Console.WriteLine("支出を登録しました。");
        }

        private void PrintRecords()
        {
            Console.WriteLine("一覧表示:");
            foreach (var r in _ledger.GetAll())
            {
                Console.WriteLine($"{r.Type} {r.Amount}円 {r.Memo}");
            }
        }

        private void PrintSummary()
        {
            var (income, expense) = _ledger.GetSummary();
            Console.WriteLine($"収入合計: {income}円");
            Console.WriteLine($"支出合計: {expense}円");
            Console.WriteLine($"差引: {income - expense}円");
        }

        private void Save()
        {
            Console.Write("保存するファイル名を入力してください: ");
            string path = Console.ReadLine();

            _ledger.Save(path);
            Console.WriteLine("保存しました。");
        }

        private void Load()
        {
            Console.Write("読み込むファイル名を入力してください: ");
            string path = Console.ReadLine();

            _ledger.Load(path);
            Console.WriteLine("読み込みました。");
        }
    }
}
C#
// Program.cs
using HouseholdLedger.UI;

class Program
{
    static void Main()
    {
        var app = new ConsoleApp();
        app.Run();
    }
}
C#

ここまで来ると、

  • 「どのクラスが何を担当しているか」がはっきりしている
  • データの形が Record として明示されている
  • 家計簿のロジックは Ledger に集約されている
  • UI の制御は ConsoleApp にまとまっている

という、読みやすくて育てやすいコードにかなり近づいています。

Day 70 用ミニテンプレート

リファクタリングの思考テンプレート

  1. まず動くコードを書く
    • 最初は「全部 Main に書いてしまう」でもOK
  2. 問題を言葉にする
    • 長すぎる
    • 責務がごちゃ混ぜ
    • 同じ処理を何度も書いている
  3. メソッド分割する
    • 「この部分、ひとつのメソッドにできないかな?」と考える
  4. クラス分割する
    • 「このメソッドたち、ひとつの概念(家計簿など)としてまとめられないかな?」
  5. 名前空間・フォルダで整理する
    • Models / Domain / UI などに分けてみる

この流れを、 小さなアプリで何度か繰り返してみると、 リファクタリングの感覚が少しずつ手になじんでいきます。

Day 70 のまとめ

Day 70 では、

  • 長くてごちゃごちゃしたコードからスタートし
  • 問題点を言葉にして
  • メソッド分割で Main の責任を減らし
  • クラス分割で「家計簿」という概念を形にし
  • 名前空間とフォルダで整理して
  • 最終的に「読みやすいコード」へ育てていく

という、リファクタリングの一連の流れを体験しました。

リファクタリングは、 「一度で完璧にきれいにする魔法」ではなく、 少しずつ、段階的に整えていく“育てる作業”です。

だからこそ、

  • 動くコードを書いたあとに「もう一歩きれいにできないかな?」と考えてみる
  • 長くなってきたら「メソッドに分けようかな」と手を入れてみる
  • 責務が増えすぎたら「クラスに分けようかな」と勇気を出してみる

という小さな一歩が、 コードを「自分にとって気持ちのいい相棒」に育てていく力になります。

ぜひ、Day 70 のリファクタリング演習を、 今後のコードづくりの“癖づけ”として、 少しずつ生活のように取り入れていっていただければうれしいです。

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