導入
いよいよ本題です。「登録する(add)」「全件取る(findAll)」「IDで探す(findById)」の3つだけを持つTaskRepositoryを定義し、List<TaskItem>で保持するInMemoryTaskRepositoryをTDDで実装します。
図解
classDiagram
class TaskRepository {
<<interface>>
+add(TaskItem) void
+findAll() List~TaskItem~
+findById(String) Optional~TaskItem~
}
class InMemoryTaskRepository {
-List~TaskItem~ tasks
}
TaskRepository <|.. InMemoryTaskRepository
説明
このコースを通じて使う契約は次の形に統一します(以降の章もこの3メソッドだけを使います)。
public interface TaskRepository {
void add(TaskItem task);
List<TaskItem> findAll();
Optional<TaskItem> findById(String id);
}
addはタスクを1件追加する(戻り値なし)findAllは今持っている全タスクをリストとして返すfindByIdは見つからなければ**空のOptional**を返す。Javaにはnullを返す代わりに「値があるかもしれない箱」であるOptionalを使う習慣があります
やってみよう
InMemoryTaskRepositoryを実装したら、TaskFactoryで3件タスクを作ってaddし、findAll().size()で件数を確認してみましょう。
演習
実務のファイル構成そのまま、4つの.javaファイルに分けて実装します。「ドメインの型」「インターフェース」「実装(ここが未実装)」「テスト」がそれぞれ独立したタブになります。
// --- 第2〜3章の型(コピー。シグネチャは変更しない) ---
public interface IdGenerator {
String next();
}
public class TaskTitle {
private final String value;
public TaskTitle(String value) {
if (value == null || value.isBlank()) throw new IllegalArgumentException("タイトルは空にできません");
if (value.length() > 50) throw new IllegalArgumentException("タイトルは50文字以内です");
this.value = value;
}
public String getValue() { return value; }
public String toString() { return value; }
public boolean equals(Object o) {
if (!(o instanceof TaskTitle)) return false;
return value.equals(((TaskTitle) o).value);
}
public int hashCode() { return value.hashCode(); }
}
public enum Priority {
LOW("低"), MEDIUM("中"), HIGH("高");
private final String label;
Priority(String label) { this.label = label; }
public String getLabel() { return label; }
}
public class TaskItem {
private final String id;
private TaskTitle title;
private final Priority priority;
private boolean completed;
public TaskItem(String id, TaskTitle title, Priority priority) {
this.id = id;
this.title = title;
this.priority = priority;
}
public String getId() { return id; }
public TaskTitle getTitle() { return title; }
public Priority getPriority() { return priority; }
public boolean isCompleted() { return completed; }
public void complete() { completed = true; }
public void rename(TaskTitle newTitle) { title = newTitle; }
public boolean equals(Object o) {
if (!(o instanceof TaskItem)) return false;
return id.equals(((TaskItem) o).id);
}
public int hashCode() { return id.hashCode(); }
}
public class TaskFactory {
private final IdGenerator idGenerator;
public TaskFactory(IdGenerator idGenerator) { this.idGenerator = idGenerator; }
public TaskItem create(String title, Priority priority) {
return new TaskItem(idGenerator.next(), new TaskTitle(title), priority);
}
public TaskItem create(String title) {
return create(title, Priority.MEDIUM);
}
}
// テストで使う簡単な IdGenerator(呼ばれるたびに t-1, t-2, ... を返す)
public class SequentialIdGenerator implements IdGenerator {
private int seq = 0;
public String next() {
seq++;
return "t-" + seq;
}
}
public interface TaskRepository {
void add(TaskItem task);
List<TaskItem> findAll();
Optional<TaskItem> findById(String id);
}
// TODO: List<TaskItem> で保持する InMemoryTaskRepository を実装してください
// findById は見つからなければ Optional.empty() を返します
public class InMemoryTaskRepository implements TaskRepository {
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class InMemoryTaskRepositoryTest {
@Test
void tsuikashitaTaskHaFindAllDeZenkenShutokuDekiru() {
TaskRepository repo = new InMemoryTaskRepository();
TaskFactory factory = new TaskFactory(new SequentialIdGenerator());
TaskItem t1 = factory.create("牛乳を買う");
TaskItem t2 = factory.create("レポートを書く", Priority.HIGH);
repo.add(t1);
repo.add(t2);
assertEquals(2, repo.findAll().size(), "2件登録されている");
}
@Test
void findByIdDeIdWoShiteiShiteShutokuDekiru() {
TaskRepository repo = new InMemoryTaskRepository();
TaskFactory factory = new TaskFactory(new SequentialIdGenerator());
TaskItem t1 = factory.create("牛乳を買う");
repo.add(t1);
assertEquals("牛乳を買う", repo.findById(t1.getId()).get().getTitle().getValue(), "IDで取得できる");
}
@Test
void sonzaishinaiIdHaKaraNoOptionalWoKaesu() {
TaskRepository repo = new InMemoryTaskRepository();
assertTrue(repo.findById("nai-id").isEmpty(), "存在しないIDは空のOptional");
}
}
- 期待される結果: 3件のテストがすべて成功
ヒント1を見る
public class InMemoryTaskRepository implements TaskRepository { private final List<TaskItem> tasks = new ArrayList<>(); public void add(TaskItem task) { tasks.add(task); } public List<TaskItem> findAll() { return new ArrayList<>(tasks); } public Optional<TaskItem> findById(String id) { for (TaskItem t : tasks) { if (t.getId().equals(id)) return Optional.of(t); } return Optional.empty(); } }
ヒント2を見る
findByIdは拡張forでtasksを1件ずつ見て、getId().equals(id)が一致したらOptional.of(t)を返します。最後まで見つからなければOptional.empty()です
まとめ
TaskRepositoryはadd/findAll/findByIdの3メソッドだけの小さな契約InMemoryTaskRepositoryはList<TaskItem>で保持する素直な実装findByIdはnullではなくOptionalで「見つからない」を表す
次回: 本物ではなく偽物のFakeTaskRepositoryでテストを書く理由です。