導入
「タスク一覧を優先度の高い順に並べたい」――リリース後によくある仕様追加です。既存のadd・complete・list・remainingCountには一切手を触れず、新しい仕様を新しいメソッドとして付け加えられるかを確認しましょう。既存のテストを1つも壊さずに機能を追加できることが、ここまで積み上げてきたTDDの実践力の見せ所です。
図解
flowchart LR
R["repository.findAll()<br/>(TaskItemの一覧)"] --> S["並び替え<br/>(Priorityのordinalで比較)"]
S --> M["TaskMapperで変換"]
M --> OUT["優先度High→Medium→Lowの順に<br/>並んだTaskDtoの一覧"]
style S fill:#e1f5fe
説明
00-index.mdで決めた約束のとおり、並び替えは既存のlist()を変えず、listSortedByPriority()という用途ごとの新しいメソッドとして追加します。
PriorityはLOW・MEDIUM・HIGHの順で宣言されたenumなので、.ordinal()が0・1・2を返します。これを比較に使えば、優先度の大小をそのまま数値の大小として扱えますList#sort(Comparator)にラムダを渡すと、そのラムダが2つの要素を受け取ってintを返す比較関数として使われます。マイナス・ゼロ・プラスの意味は、あなたが書いた比較用のメソッド(例えばcompareTo)と同じですrepository.findAll()が返す一覧をそのまま並び替えると保存されているデータの順序まで変わってしまうため、必ずnew ArrayList<>(...)でコピーしてから並び替えます
Priority p = Priority.HIGH;
System.out.println(p.ordinal()); // 2 (LOW=0, MEDIUM=1, HIGH=2)
やってみよう
まず何も書かずに「▶ 実行」を押し、listSortedByPriorityが無くてテストが失敗する(Red)ことを確認してから演習に進みましょう。
演習
TaskService.javaの担当は新しい1メソッドだけです。他のファイルは前の章までに作ったものをそのままコピーしてあります。
public interface IdGenerator {
String next();
}
// --- 前の章までに作ったクラスのコピー ---
final class TaskTitle {
private final String value;
TaskTitle(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("タイトルは空にできません");
}
if (value.length() > 50) {
throw new IllegalArgumentException("タイトルは50文字以内です");
}
this.value = value;
}
String getValue() { return value; }
public String toString() { return value; }
}
enum Priority {
LOW("低"), MEDIUM("中"), HIGH("高");
private final String label;
Priority(String label) { this.label = label; }
String getLabel() { return label; }
}
class TaskItem {
private final String id;
private TaskTitle title;
private final Priority priority;
private boolean completed;
TaskItem(String id, TaskTitle title, Priority priority) {
this.id = id;
this.title = title;
this.priority = priority;
}
String getId() { return id; }
TaskTitle getTitle() { return title; }
Priority getPriority() { return priority; }
boolean isCompleted() { return completed; }
void complete() { completed = true; }
}
class TaskFactory {
private final IdGenerator idGenerator;
TaskFactory(IdGenerator idGenerator) { this.idGenerator = idGenerator; }
TaskItem create(String title, Priority priority) {
return new TaskItem(idGenerator.next(), new TaskTitle(title), priority);
}
TaskItem create(String title) { return create(title, Priority.MEDIUM); }
}
interface TaskRepository {
void add(TaskItem task);
List<TaskItem> findAll();
Optional<TaskItem> findById(String id);
}
class InMemoryTaskRepository implements TaskRepository {
private final List<TaskItem> tasks = new ArrayList<>();
public void add(TaskItem task) { tasks.add(task); }
public List<TaskItem> findAll() { return tasks; }
public Optional<TaskItem> findById(String id) {
for (TaskItem t : tasks) {
if (t.getId().equals(id)) return Optional.of(t);
}
return Optional.empty();
}
}
public record TaskDto(String id, String title, String priorityLabel, boolean completed) { }
public class TaskMapper {
public static TaskDto toDto(TaskItem task) {
return new TaskDto(task.getId(), task.getTitle().getValue(), task.getPriority().getLabel(), task.isCompleted());
}
}
public class TaskService {
private final TaskRepository repository;
private final TaskFactory factory;
public TaskService(TaskRepository repository, TaskFactory factory) {
this.repository = repository;
this.factory = factory;
}
public void add(String title, Priority priority) {
repository.add(factory.create(title, priority));
}
public void complete(String id) {
Optional<TaskItem> found = repository.findById(id);
if (found.isPresent()) {
found.get().complete();
}
}
public List<TaskDto> list() {
List<TaskDto> result = new ArrayList<>();
for (TaskItem task : repository.findAll()) {
result.add(TaskMapper.toDto(task));
}
return result;
}
public int remainingCount() {
int count = 0;
for (TaskItem task : repository.findAll()) {
if (!task.isCompleted()) count++;
}
return count;
}
// TODO: 優先度が高い順(High → Medium → Low)に並んだ一覧を返す
// listSortedByPriority を実装してください。add/complete/list/remainingCount は変更禁止です
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TaskServiceTest {
private TaskService newService() {
TaskRepository repository = new InMemoryTaskRepository();
int[] counter = {0};
IdGenerator idGenerator = () -> "id-" + (counter[0]++);
TaskFactory factory = new TaskFactory(idGenerator);
return new TaskService(repository, factory);
}
@Test
void yuusendoNoTakaiJunNiNarabu() {
TaskService service = newService();
service.add("牛乳を買う", Priority.LOW);
service.add("レポート提出", Priority.HIGH);
service.add("掃除する", Priority.MEDIUM);
List<TaskDto> sorted = service.listSortedByPriority();
assertEquals("レポート提出", sorted.get(0).title(), "Highが先頭");
assertEquals("掃除する", sorted.get(1).title(), "次にMedium");
assertEquals("牛乳を買う", sorted.get(2).title(), "Lowが最後");
}
}
- 期待される結果: 1件のテストが成功
ヒント1を見る
List<TaskItem> items = new ArrayList<>(repository.findAll()); items.sort((a, b) -> b.getPriority().ordinal() - a.getPriority().ordinal());
ヒント2を見る
並び替えたあとはforで1件ずつTaskMapper.toDto(...)して新しいList<TaskDto>に詰め替えます(list()と同じ形)
まとめ
- 新しい仕様がどの層に属するかを見極め、既存メソッドは変えずに新しいメソッドとして追加する
enumの.ordinal()を使えば、順序のある選択肢を数値として比較できる- 既存の
add・complete・list・remainingCountのテストは1つも壊れていない――これが「安全な仕様追加」の証拠
次回: 動くけど読みにくいコードを、テストに守られながらリファクタリングします。