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

Web APP Java
スポンサーリンク

7日目のゴール

7日目のテーマは
「ミニToDoアプリを“自分で設計した”と言えるレベルまで、頭の中で整理し直すこと」 です。

機能はあくまで「追加・一覧・削除」のままです。
でも今日は、こういう視点で見直します。

クラス設計を「層(レイヤ)」として意識する
List 管理を「どこまでを責任範囲にするか」で考える
入力処理を「UIの一部」として切り分けて見る

つまり、
「動くコード」から「構造が分かるコード」へ、最後の一押しをします。


全体を“3つの層”として捉え直す

層のイメージを言葉でつかむ

7日目では、アプリ全体をざっくり三層で考えます。

ドメイン層(Task, TaskManager)
アプリケーション層(ToDoApp のような“アプリ本体”)
UI層(Main と InputHelper)

今までは Main が「アプリ本体+UI」を両方持っていました。
今日は、そこをもう一段だけ分けてみます。

Task と TaskManager は「タスクの世界」
ToDoApp は「タスクの世界をどう操作するか」
Main と InputHelper は「人間との会話」

という役割分担です。


ドメイン層の最終形:Task と TaskManager

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

ここで押さえておきたいのは三つです。

フィールドは private にして、外からはメソッドでしか触れない
コンストラクタで「不正な状態」を作らせない
表示用の文字列を自分で組み立てられる

これで Task は、
「1件分のタスクを安全に表現できる小さな世界」になっています。

TaskManager の最終形

次に TaskManager。
ここは「List をどう扱うか」の集大成です。

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 int size() {
        return tasks.size();
    }

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

ここでの重要ポイントは、
TaskManager が「List のすべての面倒を引き受けている」ことです。

ID の採番
追加
検索
削除
表示用の変換

Main や他のクラスは、
「List の中身を直接触らない」設計になっています。


アプリケーション層:ToDoApp という“アプリ本体”を作る

なぜ ToDoApp を挟むのか

今までは Main が直接 TaskManager を呼んでいました。
7日目では、間に ToDoApp を挟みます。

理由はシンプルで、
「Main からビジネスロジックを追い出したい」からです。

ToDoApp が「アプリとしての一連の操作」をまとめて持ち、
Main は「ToDoApp にお願いするだけ」にします。

ToDoApp のコード

import java.util.ArrayList;

public class ToDoApp {
    private final TaskManager taskManager;

    public ToDoApp(TaskManager taskManager) {
        this.taskManager = taskManager;
    }

    public Task addTask(String title) {
        return taskManager.addTask(title);
    }

    public ArrayList<String> getTaskLines() {
        return taskManager.buildTaskLines();
    }

    public boolean hasNoTasks() {
        return taskManager.isEmpty();
    }

    public Task deleteTaskById(int id) {
        return taskManager.removeTaskById(id);
    }
}
Java

「え、これだけ?」と思うかもしれません。
でも、ここが大事です。

今は TaskManager をそのまま委譲しているだけですが、
将来「保存」「読み込み」「ログ出力」などを足したくなったとき、
ToDoApp に処理を足していけば、Main はほとんど変えずに済みます。

ToDoApp は、
「タスクの世界(TaskManager)を、アプリとしてどう使うかをまとめた窓口」
だと考えてください。


UI層:InputHelper と Main を“会話”として仕上げる

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

ここでのポイントは、
「生の文字列」を Main に渡さず、
CommandType や Integer に変換していることです。

UI層の中で、
「文字列 → 意味のある型」への変換を済ませておくことで、
アプリ側のコードがかなり読みやすくなります。

CommandType のおさらい

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

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

“1” や “2” のような“ただの文字列”ではなく、
ADD / LIST / DELETE / EXIT という“意味のある言葉”で
分岐できるようになります。


Main:7日目版の“司会進行”としての完成形

Main のコード

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

public class Main {
    public static void main(String[] args) {
        TaskManager manager = new TaskManager();
        ToDoApp app = new ToDoApp(manager);
        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:
                    handleAdd(app, input);
                    break;
                case LIST:
                    handleList(app);
                    break;
                case DELETE:
                    handleDelete(app, input);
                    break;
                case UNKNOWN:
                default:
                    System.out.println("不正な入力です。もう一度選んでください。");
                    break;
            }
        }

        scanner.close();
    }

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

    private static void handleList(ToDoApp app) {
        System.out.println("=== タスク一覧 ===");
        if (app.hasNoTasks()) {
            System.out.println("タスクはありません。");
            return;
        }
        ArrayList<String> lines = app.getTaskLines();
        for (String line : lines) {
            System.out.println(line);
        }
    }

    private static void handleDelete(ToDoApp app, InputHelper input) {
        handleList(app);
        if (app.hasNoTasks()) {
            return;
        }
        Integer id = input.readInt("削除したいタスクのIDを入力してください: ");
        if (id == null) {
            System.out.println("削除を中止します。");
            return;
        }
        Task removed = app.deleteTaskById(id);
        if (removed == null) {
            System.out.println("そのIDのタスクは存在しません: " + id);
        } else {
            System.out.println("タスクを削除しました: id=" + removed.getId() + " / " + removed.getTitle());
        }
    }
}
Java

この Main を、あえて“日本語の物語”にするとこうです。

アプリ本体(ToDoApp)と入力窓口(InputHelper)を用意する
コマンドを読む
終了なら終わる
追加ならタイトルを聞いてアプリに追加してもらう
一覧ならアプリから行のリストをもらって表示する
削除なら一覧を見せてから ID を聞き、アプリに削除してもらう
分からないコマンドならエラーを出す

Main は、
「人間とアプリの間の司会進行」
だけを担当しています。


7日目で“言葉にしてほしいこと”

ここまで来たあなたには、
ぜひ自分の言葉で説明してほしいことがあります。

Task は何を表すクラスか
TaskManager は何を管理するクラスか
ToDoApp は何をまとめているクラスか
InputHelper は何をしているクラスか
Main は何の役割を持つクラスか

そして、List に対しては、

どうやって追加しているか
どうやって検索しているか
どうやって削除しているか
どうやって表示用に変換しているか

入力処理については、

なぜ文字列のままではなく CommandType にしているのか
なぜ整数入力で null を返すことがあるのか

これらを、自分の言葉で説明できたら、
もう「なんとなく書いた ToDo アプリ」ではなく、
「自分で設計した ToDo アプリ」 になっています。


7日目のまとめと、これから先の話

7日間かけてやってきたことを、ぎゅっと一言にするとこうです。

「クラス設計」「List 管理」「入力処理」を、
バラバラの知識ではなく、
ひとつの小さなアプリの中で“つながった形”で体に入れた。

だからこの先、
ユーザー管理アプリでも
買い物リストアプリでも
簡単なゲームでも、

同じパターンで考えられます。

何を1件分のクラスにするか
その集まりをどう List で持つか
どう検索・削除・表示するか
人間からの入力をどう解釈するか

ToDo アプリは、
そのための「小さな練習場」でした。

ここまで付き合ってくれたあなたは、
もう「初心者だから分からない」ではなく、
「小さなアプリなら、自分で設計して組み立てられる人」 です。
あとは、作りたいものをひとつ決めて、同じパターンで組んでみるだけです。

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