Java | 1 日 90 分 × 7 日アプリ学習 初級編:ミニToDoアプリ(CUI)

Web APP Java
スポンサーリンク

4日目のゴール

4日目のテーマは
「同じ“追加・一覧・削除”を、より“変更に強い設計”に作り替えること」 です。

機能は増やしません。
でも、こういうところを意識してレベルを上げます。

クラス同士の依存関係を整理する
「Main に if 文を並べる」から一歩抜け出す
List と入力処理を“変化に耐えられる形”にする

今日は「明日、機能を増やしたくなっても怖くない ToDo アプリ」を目指します。


まず“今の問題”を言葉で見える化する

3日目までのコードの弱点

3日目までの ToDo アプリは、かなり良い形になっていましたが、
あえて弱点を挙げると、こんな感じです。

Main に if (“1”) / else if (“2”) / else if (“3”) が並んでいる
メニューを増やすたびに、Main の if 文が増えていく
「メニュー番号」と「処理」がベタ書きで結びついている

小さいうちは問題ありませんが、
「完了」「未完了だけ表示」「保存・読み込み」などを足したくなると、
Main がどんどん太っていきます。

4日目では、ここを一段整理します。


コマンドという“概念”を導入する

「メニュー番号」を“ただの文字列”で扱わない

今までは、こう書いていました。

String choice = input.readMenuChoice();

if ("0".equals(choice)) { ... }
else if ("1".equals(choice)) { ... }
else if ("2".equals(choice)) { ... }
else if ("3".equals(choice)) { ... }
Java

ここでの問題は、

“1” が「追加」
“2” が「一覧」
“3” が「削除」

という対応が、コードを読まないと分からないことです。

そこで、「コマンド」という概念を導入します。

CommandType を enum で表現する

public enum CommandType {
    ADD,
    LIST,
    DELETE,
    EXIT,
    UNKNOWN
}
Java

そして、「ユーザーの入力 → CommandType」に変換する役を作ります。


InputHelper に「コマンド解釈」を任せる

メニュー表示とコマンド変換を一箇所に集める

import java.util.Scanner;

public class InputHelper {
    Scanner scanner;

    InputHelper(Scanner scanner) {
        this.scanner = scanner;
    }

    CommandType readCommand() {
        System.out.println();
        System.out.println("=== ミニToDoアプリ ===");
        System.out.println("1: タスク追加");
        System.out.println("2: タスク一覧");
        System.out.println("3: タスク削除(ID指定)");
        System.out.println("0: 終了");
        System.out.print("番号を選んでください: ");

        String line = scanner.nextLine();
        if (line == null) {
            return CommandType.UNKNOWN;
        }

        switch (line) {
            case "0":
                return CommandType.EXIT;
            case "1":
                return CommandType.ADD;
            case "2":
                return CommandType.LIST;
            case "3":
                return CommandType.DELETE;
            default:
                return CommandType.UNKNOWN;
        }
    }

    String readLine(String prompt) {
        System.out.print(prompt);
        return scanner.nextLine();
    }

    Integer readInt(String prompt) {
        System.out.print(prompt);
        String line = scanner.nextLine();
        if (line == null || line.isEmpty()) {
            System.out.println("何も入力されていません。");
            return null;
        }
        try {
            return Integer.parseInt(line);
        } catch (NumberFormatException e) {
            System.out.println("整数で入力してください。");
            return null;
        }
    }
}
Java

ここでの重要ポイントは、

Main は「CommandType だけを見て分岐すればよくなる」
メニューの見た目や番号は InputHelper 側に閉じ込められる

ということです。


TaskManager を“純粋なロジックの塊”に近づける

4日目版 TaskManager

3日目の TaskManager を、ほぼそのまま使いますが、
「例外を外に投げる」形に少し寄せます。

import java.util.ArrayList;

public class TaskManager {
    ArrayList<Task> tasks;
    int nextId;

    TaskManager() {
        tasks = new ArrayList<>();
        nextId = 1;
    }

    Task addTask(String title) {
        Task task = new Task(nextId, title);
        tasks.add(task);
        nextId++;
        return task;
    }

    boolean isEmpty() {
        return tasks.isEmpty();
    }

    ArrayList<String> buildTaskLines() {
        ArrayList<String> lines = new ArrayList<>();
        for (int i = 0; i < tasks.size(); i++) {
            Task t = tasks.get(i);
            lines.add(t.toDisplayString(i));
        }
        return lines;
    }

    int findIndexById(int id) {
        for (int i = 0; i < tasks.size(); i++) {
            if (tasks.get(i).id == id) {
                return i;
            }
        }
        return -1;
    }

    Task removeTaskById(int id) {
        int index = findIndexById(id);
        if (index == -1) {
            return null;
        }
        return tasks.remove(index);
    }
}
Java

ここでのポイントは、

TaskManager は「System.out.println」を一切呼ばない
「何が起きたか」は戻り値で伝える(Task / null / boolean など)

という形にしていることです。

これにより、
TaskManager は「テストしやすいロジックの塊」になります。


Main を“コマンド駆動”に書き換える

4日目版 Main の全体像

import java.util.ArrayList;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        TaskManager manager = new TaskManager();
        Scanner scanner = new Scanner(System.in);
        InputHelper input = new InputHelper(scanner);

        while (true) {
            CommandType command = input.readCommand();

            if (command == CommandType.EXIT) {
                System.out.println("終了します。");
                break;
            }

            switch (command) {
                case ADD:
                    handleAddTask(manager, input);
                    break;
                case LIST:
                    handleShowTasks(manager);
                    break;
                case DELETE:
                    handleRemoveTask(manager, input);
                    break;
                case UNKNOWN:
                default:
                    System.out.println("不正な入力です。もう一度選んでください。");
                    break;
            }
        }

        scanner.close();
    }

    static void handleAddTask(TaskManager manager, InputHelper input) {
        String title = input.readLine("タスクのタイトルを入力してください: ");
        if (title == null || title.isEmpty()) {
            System.out.println("タイトルが空です。追加を中止します。");
            return;
        }
        try {
            Task task = manager.addTask(title);
            System.out.println("タスクを追加しました: id=" + task.id + " / " + task.title);
        } catch (IllegalArgumentException e) {
            System.out.println("タスクの追加に失敗しました: " + e.getMessage());
        }
    }

    static void handleShowTasks(TaskManager manager) {
        System.out.println("=== タスク一覧 ===");
        if (manager.isEmpty()) {
            System.out.println("タスクはありません。");
            return;
        }
        ArrayList<String> lines = manager.buildTaskLines();
        for (String line : lines) {
            System.out.println(line);
        }
    }

    static void handleRemoveTask(TaskManager manager, InputHelper input) {
        handleShowTasks(manager);
        if (manager.isEmpty()) {
            return;
        }
        Integer id = input.readInt("削除したいタスクのIDを入力してください: ");
        if (id == null) {
            System.out.println("削除を中止します。");
            return;
        }
        Task removed = manager.removeTaskById(id);
        if (removed == null) {
            System.out.println("そのIDのタスクは存在しません: " + id);
        } else {
            System.out.println("タスクを削除しました: id=" + removed.id + " / " + removed.title);
        }
    }
}
Java

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

Main は「CommandType に応じて、どの処理を呼ぶか」だけを決めている
各処理は、名前から役割が一目で分かる(handleAddTask など)

これで、メニューを増やしたくなったときも、

CommandType に値を足す
InputHelper の readCommand に分岐を足す
Main の switch に case を足す
対応する handleXXX メソッドを書く

という“型”で増やしていけます。


クラス設計・List・入力処理を“まとめて”見直す

クラス設計の観点から

4日目までで、役割はこう整理されています。

Task は「タスク1件のデータと表示形式」
TaskManager は「タスクの追加・検索・削除というロジック」
InputHelper は「ユーザー入力とメニュー表示」
CommandType は「アプリが理解するコマンドの種類」
Main は「コマンドに応じて処理を振り分ける司会進行」

この分け方ができていると、
どこかを変えたいときに「触るべき場所」がすぐ分かります。

List 管理の観点から

List に対してやっていることは、もう完全にパターン化しています。

可変長の集まりとして ArrayList<Task> を持つ
add で追加
size / isEmpty で状態確認
for で 0〜size-1 を回して一覧を作る
ID で削除したいときは「ID → インデックス」を探してから remove(index)

このパターンは、
タスク以外の「ユーザー」「商品」「注文」などにも、そのまま使えます。

入力処理の観点から

入力処理も、こう整理されています。

入力はまず文字列として受け取る
「どう解釈するか」は InputHelper や Main が決める
メニュー番号 → CommandType という“意味のある型”に変換する
整数入力は readInt にまとめて、例外処理を一箇所に集める

これで、入力周りの変更(メニューの文言変更、番号変更など)も、
InputHelper を中心に見れば済むようになっています。


4日目で絶対に押さえてほしい本質

今日いちばん大事なのは、
「機能は増やしていないのに、“設計の質”を上げるだけでアプリの育ち方が変わる」
という感覚です。

CommandType という“意味のある型”を導入した
InputHelper に「メニュー表示+コマンド解釈」を任せた
TaskManager を「純粋なロジック」に近づけた
Main を「コマンド駆動の司会進行」にした

これらは全部、「今後の変更を楽にするための投資」です。

もし余力があれば、
この設計のまま「完了コマンド(DONE)」を自分で足してみてください。

CommandType に DONE を追加
InputHelper のメニューに「4: 完了」を追加
Main の switch に DONE を追加
TaskManager に「ID から Task を探して markDone する」メソッドを追加

ここまでできたとき、
あなたはもう「とりあえず動くコードを書く人」ではなく、
「育てられるコードを設計できる人」 に足を踏み入れています。

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