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

Web APP Java
スポンサーリンク

6日目のゴール

6日目のテーマは
「“追加・一覧・削除”という同じ機能を、テストしやすく・壊れにくく・読み返しやすくする」 ことです。

機能は増やしません。
でも、視点を変えます。

クラスを「テストできるか」という目で見る
List の操作を「パターン」として再確認する
入力処理を「失敗前提」で設計する

今日は、明日以降「完了機能」や「保存機能」を足しても、
土台が揺れないように、芯を固める日です。


クラス設計を「テストできるか」という目で見る

Task を“外から見て分かりやすい”形にする

まずは Task から。

public class Task {
    private final int id;
    private final String title;
    private boolean done;

    public Task(int id, String title) {
        if (title == null || title.isEmpty()) {
            throw new IllegalArgumentException("タイトルは必須です。");
        }
        this.id = id;
        this.title = title;
        this.done = false;
    }

    public int getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public boolean isDone() {
        return done;
    }

    public void markDone() {
        this.done = true;
    }

    public String toDisplayString(int index) {
        String status = done ? "[x]" : "[ ]";
        return index + ": " + status + " (id=" + id + ") " + title;
    }
}
Java

ここでのポイントは、
「外から見えるメソッドだけで状態が分かる」ことです。

id
title
done

が getter で取れるので、
もしテストを書くなら、こういうことができます。

Task t = new Task(1, “勉強する”);
t.markDone();
t.isDone() が true か確認する

つまり、
「中身を直接触らなくても、メソッド経由で状態を確認できる」
という形になっています。

TaskManager を“テストしやすいロジックの塊”として見る

TaskManager も、
「戻り値で結果が分かる」ようにしておきます。

import java.util.ArrayList;

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

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

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

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

    public 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;
    }

    public Task findById(int id) {
        for (Task t : tasks) {
            if (t.getId() == id) {
                return t;
            }
        }
        return null;
    }

    public Task removeTaskById(int id) {
        for (int i = 0; i < tasks.size(); i++) {
            Task t = tasks.get(i);
            if (t.getId() == id) {
                return tasks.remove(i);
            }
        }
        return null;
    }

    public int size() {
        return tasks.size();
    }
}
Java

ここでの重要ポイントは、
System.out.println が一切出てこないことです。

TaskManager は「何が起きたか」を
Task や boolean や null で返してくれます。

だから、もしテストを書くなら、こうできます。

TaskManager m = new TaskManager();
m.addTask(“A”);
m.addTask(“B”);
m.removeTaskById(1);
m.size() が 1 か確認する

「入出力に依存しないロジック」 になっているので、
テストしやすく、壊れにくいクラスになっています。


List 管理を“自分の中の定番パターン”として固める

追加のパターン

List への追加は、いつも同じです。

Task を作る
List に add する
必要なら ID を進める

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

ここで大事なのは、
「Task を作る責任」と「List に入れる責任」を
TaskManager に集めていることです。

Main は「タイトルを渡すだけ」で済みます。

検索のパターン

List の検索も、いつも同じです。

for で回す
条件に合ったら返す
なければ null

public Task findById(int id) {
    for (Task t : tasks) {
        if (t.getId() == id) {
            return t;
        }
    }
    return null;
}
Java

ID 以外でも、
「タイトルで探す」「完了だけ探す」など、
条件を変えるだけで同じパターンが使えます。

削除のパターン

削除は「検索+remove」です。

public Task removeTaskById(int id) {
    for (int i = 0; i < tasks.size(); i++) {
        Task t = tasks.get(i);
        if (t.getId() == id) {
            return tasks.remove(i);
        }
    }
    return null;
}
Java

インデックスを見つけてから remove する
見つからなければ null

この形は、
List を使う限り、何度も何度も出てきます。

表示用変換のパターン

List を「表示用の List」に変換するのも、
よく出てくるパターンです。

public 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;
}
Java

ここでのポイントは、
「表示のための加工」を TaskManager に任せていることです。

Main は「行を受け取って println するだけ」で済みます。


入力処理を“失敗前提”で設計する

InputHelper を「失敗も含めて扱うクラス」として見る

InputHelper は、
「ユーザー入力がうまくいかないことも前提にした窓口」です。

import java.util.Scanner;

public class InputHelper {
    private final Scanner scanner;

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

    public 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;
        }
    }

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

    public 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

ここでの重要ポイントは、
「失敗したときに null や UNKNOWN を返す」ことです。

readInt が失敗したら null
readCommand が分からない入力なら UNKNOWN

Main はそれを見て、
「どう振る舞うか」を決めます。

CommandType を“安全なスイッチ”として使う

CommandType は、
アプリが理解できるコマンドの種類です。

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

文字列のまま if するのではなく、
一度 CommandType に変換してから switch することで、

スペルミスの心配がない
メニュー番号を変えても CommandType は変わらない

という状態になります。


6日目版 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();
    }

    private 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.getId() + " / " + task.getTitle());
        } catch (IllegalArgumentException e) {
            System.out.println("タスクの追加に失敗しました: " + e.getMessage());
        }
    }

    private 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);
        }
    }

    private 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.getId() + " / " + removed.getTitle());
        }
    }
}
Java

この Main を、流れだけ追ってみてください。

コマンドを読む
終了なら終わる
追加ならタイトルを聞いて追加する
一覧なら行を作って表示する
削除なら一覧を見せてから ID を聞き、削除する
変な入力ならエラーメッセージ

「正常な流れ」と「失敗したときの流れ」が、両方ともはっきり見える」
というのが、6日目の完成形です。


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

今日いちばん大事なのは、
「同じ機能でも、“テストできるか・失敗に耐えられるか”という目で設計すると、コードの質が一段上がる」
という感覚です。

Task は「外から状態を確認できる値オブジェクト」
TaskManager は「入出力に依存しないロジックの塊」
List 管理は「追加・検索・削除・変換」のパターンとして整理
InputHelper は「失敗も含めて入力を扱う窓口」
CommandType は「安全に分岐するためのコマンドの型」

ここまで来ると、
明日「完了機能」を足すときも、やることは見えています。

ID から Task を探す(findById)
markDone を呼ぶ
表示のときに [x] がつく

あなたはもう、
「とりあえず動く ToDo」ではなく、
「育てていける ToDo アプリの土台」 を持っています。

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