導入
もう1つ、よくある要望「タスクを仕事/プライベートのようなカテゴリで分けたい」を追加してみましょう。これも進め方は同じです。「あるカテゴリのタスクだけを取り出す」という判断は第5章で学んだ通りServiceの仕事、ViewModelとViewは、その結果を表示するだけにとどめます。
図解
flowchart LR
C["TaskItem.category"] --> SVC["TaskService.listByCategory(category)<br/>(Serviceが絞り込む)"]
SVC --> VM["ViewModel<br/>(結果件数を表示するだけ)"]
説明
00-index.mdの拡張仕様どおり、TaskItemにString categoryを足し、絞り込みはTaskServiceの用途ごとの新しいメソッドlistByCategory(String category)として追加します(レッスン35のlistSortedByPriorityと同じ考え方です)。
categoryも期限切れのときと同じく、既存の3引数コンストラクタは変えず、categoryを受け取る新しいコンストラクタを追加しますlistByCategory(String category)は、repository.findAll()を1件ずつ見て、categoryが一致するものだけを新しいList<TaskItem>に集める、というremainingCount()と同じ形の「1件ずつ見て条件に合うものだけ拾う」ループで書けます- 「あるカテゴリだけを取り出す」という判断が
TaskServiceの役割になっているのは、第4章で学んだ「Repositoryは出し入れだけ、Serviceが判断する」という役割分担にそのまま従っています
やってみよう
まず何も書かずに「▶ 実行」を押し、categoryとlistByCategoryが無くてコンパイルエラーになる(Red)ことを確認してから演習に進みましょう。
演習
Support.javaは完成済みとしてそのまま使い、今回の担当はDomain.javaのTaskItemとTaskService.javaです。
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; }
}
// TODO: category を受け取るコンストラクタと getCategory を実装してください。
// 既存の3引数コンストラクタは変更禁止(categoryは新しいコンストラクタに委譲してください)
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 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 int remainingCount() {
int count = 0;
for (TaskItem task : repository.findAll()) {
if (!task.isCompleted()) count++;
}
return count;
}
// TODO: 指定したカテゴリのタスクだけを集めた一覧を返す listByCategory を実装してください。
// add / complete / remainingCount は変更禁止です
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TaskServiceCategoryTest {
@Test
void shiteiShitaCategoryDakeGaKaesareru() {
TaskRepository repository = new InMemoryTaskRepository();
repository.add(new TaskItem("1", new TaskTitle("企画書を書く"), Priority.HIGH, "仕事"));
repository.add(new TaskItem("2", new TaskTitle("牛乳を買う"), Priority.LOW, "プライベート"));
repository.add(new TaskItem("3", new TaskTitle("見積もりを送る"), Priority.MEDIUM, "仕事"));
int[] counter = {0};
TaskService service = new TaskService(repository, new TaskFactory(() -> "id-" + (counter[0]++)));
List<TaskItem> workTasks = service.listByCategory("仕事");
assertEquals(2, workTasks.size(), "指定カテゴリだけが2件返る");
for (TaskItem task : workTasks) {
assertEquals("仕事", task.getCategory(), "返るのは指定カテゴリだけ");
}
}
}
- 期待される結果: 1件のテストが成功
ヒント1を見る
TaskItem(String id, TaskTitle title, Priority priority) { this(id, title, priority, "未分類"); }のように、既存のコンストラクタからcategory付きの新しいコンストラクタへ委譲します
ヒント2を見る
public List<TaskItem> listByCategory(String category) { List<TaskItem> result = new ArrayList<>(); for (TaskItem task : repository.findAll()) { if (task.getCategory().equals(category)) result.add(task); } return result; }
まとめ
- 「特定の条件で絞り込む」機能追加も、絞り込みロジックはService/Entity側、ViewModel/Viewは結果の表示だけという役割分担で進める
- 既存のコンストラクタを変えずに項目を追加する手順(
this(...)への委譲)は、期限切れのときと全く同じ形で再利用できた - 期限切れ・カテゴリ分けのどちらも、コースを通して学んだ「依存の向き」「TDD」「役割分担」を崩さずに追加できた
次回: コース全体のまとめと、次に学ぶとよいコースを紹介します。