5日目のゴール
5日目のテーマは
「同じ“追加・一覧・削除”を、より“迷いなく拡張できる設計”に仕上げること」 です。
ここまでであなたはすでに、
Task という1件分のクラスを作り
TaskManager で ArrayList<Task> を管理し
InputHelper と Main で入力とメニューを扱い
CommandType で「コマンドの種類」を表現する
というところまで来ています。
5日目では、機能は増やさずに、
クラスの責務をもう一段クリアにする
List 管理を「検索・変換・削除」のパターンとして整理する
入力処理を「人間との会話」として意識して整える
という方向で、コードの“芯”を太くしていきます。
クラス設計を「役割の言葉」で見直す
Task を“値オブジェクト”として意識する
Task は「1件分のタスク」を表すクラスです。
5日目では、Task を「値オブジェクト」として意識してみます。
値オブジェクト、というのはざっくり言うと
「IDと中身がセットで1つの“値”として扱われるもの」です。
コードはこうです。
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 にして、
外からは getter 経由でしか触れないようにしたことです。
id と title はコンストラクタで決まったら変わらない
done だけが「状態として変わる」
という形にすることで、
「どこからでも書き換えられる不安定なオブジェクト」から
「振る舞いを通してだけ変わる、扱いやすいオブジェクト」に近づきます。
TaskManager を“タスクのコレクション”として意識する
TaskManager は「タスクの集まりを管理するクラス」です。
5日目では、次の3つの役割に名前をつけます。
タスクを追加する
タスクの一覧を“表示用の行”に変換する
タスクを検索・削除する
コードはこうです。
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;
}
}
Javaここでのポイントは、
「IDから Task を探す findById」を用意したことです。
削除だけでなく、
「完了にする」「タイトルを変更する」など、
今後の拡張でも必ず「IDから Task を探す」処理が必要になります。
その“よく出る処理”を、
最初からメソッドとして切り出しておくことで、
List 管理のパターンがよりはっきりします。
List 管理を“検索・変換・削除”で整理する
検索のパターン
List に対する検索は、基本的にこうです。
全件を for で回す
条件に合うものを見つけたら返す
見つからなければ null を返す
findById は、その典型です。
public Task findById(int id) {
for (Task t : tasks) {
if (t.getId() == id) {
return t;
}
}
return null;
}
Javaここで大事なのは、
「List は“順番に見るもの”」という感覚です。
Map のように一発では引けない代わりに、
順番を持てる、という特徴があります。
変換のパターン
List を「別の List に変換する」パターンも、
よく使う形です。
buildTaskLines は、ArrayList<Task> から ArrayList<String> に変換しています。
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 するだけ」で済みます。
削除のパターン
削除は「検索+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(i) する
このパターンは、
List 管理の「削除の基本形」として覚えておくと、
どんなアプリでも使い回せます。
入力処理を“会話の流れ”として整える
InputHelper を「UIの窓口」として意識する
InputHelper は、
「ユーザーとのやりとりをまとめるクラス」です。
5日目では、
「メニュー入力」「文字列入力」「整数入力」を
はっきり分けておきます。
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ここでの重要ポイントは、
「入力の細かいエラー処理」を InputHelper に閉じ込めていることです。
Main は「何を聞きたいか」だけを書き、
「どう読み取るか」「失敗したらどうするか」は
InputHelper に任せられます。
CommandType を“アプリが理解できる言葉”として使う
CommandType は、
アプリが理解できる「コマンドの種類」を表す enum です。
public enum CommandType {
ADD,
LIST,
DELETE,
EXIT,
UNKNOWN
}
Java文字列 “1” や “2” のまま扱うのではなく、
一度 CommandType に変換してから扱うことで、
Main のコードが「意味のある言葉」で書ける
メニュー番号を変えても、CommandType は変わらない
という状態になります。
5日目版 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 を、あえて“日本語で言い直す”とこうなります。
コマンドを読む
終了なら終わる
追加なら追加の処理をする
一覧なら一覧の処理をする
削除なら削除の処理をする
分からないコマンドならエラーを出す
中身を見なくても、
「アプリが何をしているか」が自然に頭に入ってくるはずです。
5日目で絶対に押さえてほしい本質
今日いちばん大事なのは、
「機能は変えずに、設計とパターンを“言葉で説明できるレベル”に整理した」
ということです。
Task は「ID・タイトル・完了状態を持つ値オブジェクト」
TaskManager は「タスクの追加・検索・削除・表示用変換を担うコレクション管理者」
InputHelper は「ユーザー入力とメニューの窓口」
CommandType は「アプリが理解できるコマンドの種類」
Main は「コマンドに応じて処理を振り分ける司会進行」
List 管理は「検索・変換・削除」のパターンとして整理され、
入力処理は「文字列 → 意味のある型(CommandType / Integer)」という流れで扱えるようになりました。
ここまで来ると、
機能を増やすときにやることは、かなりシンプルです。
どのクラスの責務かを決める
List に対してどのパターン(検索・変換・削除)を使うか考える
入力をどんな“意味のある型”に変換するか決める
この3つを押さえておけば、
ToDo アプリに「完了」「フィルタ」「保存」「読み込み」を足していくのも、
もう“怖いチャレンジ”ではなく、“設計の練習”になっていきます。

