導入
TaskItemのgetTitle()はTaskTitle、getPriority()はenumを返します。画面(View)にそのまま渡すと、表示のたびに.getValue()や.getLabel()を書くことになり、Viewが業務クラスの内部構造を知りすぎてしまいます。画面用のデータの形を別に用意しましょう。
図解
flowchart LR
TI["TaskItem<br/>(Entity・業務データ)"] -->|TaskMapper.toDto| DTO["TaskDto<br/>(画面用・プレーンな値)"]
DTO --> VM["ViewModel / View"]
style DTO fill:#e1f5fe
説明
TaskDto.java
public record TaskDto(String id, String title, String priorityLabel, boolean completed) {
}
TaskMapper.java
public class TaskMapper {
public static TaskDto toDto(TaskItem task) {
return new TaskDto(task.getId(), task.getTitle().getValue(), task.getPriority().getLabel(), task.isCompleted());
}
}
TaskDtoはrecord(値の入れ物)。TaskTitleではなくString、PriorityではなくStringというプレーンな型だけを持つrecordは成分名と同じ名前のアクセサ(dto.title()のように、getが付かない)が自動で作られるTaskMapper.toDtoが変換を1箇所に集約する。ViewやViewModelが変換ロジックを個別に持たなくてよい
TaskDtoができたので、TaskServiceに一覧取得のlist()を足せます。
public List<TaskDto> list() {
List<TaskDto> result = new ArrayList<>();
for (TaskItem t : repository.findAll()) {
result.add(TaskMapper.toDto(t));
}
return result;
}
やってみよう
TaskDtoからTaskItemを作る変換(逆方向)はこのコースでは扱いません。なぜ普通は「表示専用」の一方向(Entity→Dto)で十分なのか考えてみましょう(更新はTaskServiceのメソッド経由で行うため)。
演習
Domain.java(土台)・TaskDto.java(画面用データの形)・TaskMapper.java(今日のTODO)・TaskService.java(list()を含む完成形)・TaskMapperTest.java(テスト)の5ファイルです。
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
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.trim();
}
public String getValue() { return value; }
@Override
public String toString() { return value; }
@Override
public boolean equals(Object o) {
if (!(o instanceof TaskTitle)) return false;
return value.equals(((TaskTitle) o).value);
}
@Override
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; }
@Override
public boolean equals(Object o) {
if (!(o instanceof TaskItem)) return false;
return id.equals(((TaskItem) o).id);
}
@Override
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);
}
}
public interface TaskRepository {
void add(TaskItem task);
List<TaskItem> findAll();
Optional<TaskItem> findById(String id);
}
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 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 {
// TODO: TaskItem を TaskDto に変換する toDto を実装してください
public static TaskDto toDto(TaskItem task) {
return null;
}
}
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
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) {
for (TaskItem t : repository.findAll()) {
if (t.getTitle().getValue().equals(title)) {
throw new IllegalStateException("同じタイトルのタスクが既にあります");
}
}
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 t : repository.findAll()) {
result.add(TaskMapper.toDto(t));
}
return result;
}
public int remainingCount() {
int count = 0;
for (TaskItem t : repository.findAll()) {
if (!t.isCompleted()) count++;
}
return count;
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TaskMapperTest {
@Test
void taskItemWoTaskDtoNiHenkanDekiru() {
TaskFactory factory = new TaskFactory(() -> "t1");
TaskItem task = factory.create("牛乳を買う", Priority.HIGH);
task.complete();
TaskDto dto = TaskMapper.toDto(task);
assertEquals("t1", dto.id(), "idが一致");
assertEquals("牛乳を買う", dto.title(), "titleが文字列になっている");
assertEquals("高", dto.priorityLabel(), "priorityLabelが日本語ラベルになっている");
assertTrue(dto.completed(), "completedが引き継がれる");
}
@Test
void taskServiceNoListDeDtoNoIchiranGaTorereru() {
TaskRepository repo = new InMemoryTaskRepository();
TaskService service = new TaskService(repo, new TaskFactory(() -> "t2"));
service.add("掃除する", Priority.LOW);
assertEquals(1, service.list().size());
assertEquals("掃除する", service.list().get(0).title());
}
}
- 期待される結果: 2件のテストがすべて成功
ヒント1を見る
return new TaskDto(task.getId(), task.getTitle().getValue(), task.getPriority().getLabel(), task.isCompleted());
ヒント2を見る
TaskTitleは.getValue()で文字列に、Priority(enum)は.getLabel()で日本語ラベルに変換します
まとめ
TaskDtoは画面用のプレーンなデータの形、変換はTaskMapperに集約するrecordは成分名と同じ名前のアクセサを持つ(getTitle()ではなくtitle())- Entity(
TaskItem)→Repository→Serviceという流れに、画面向けのTaskDtoが加わり、第6章のViewModelの土台が揃った
次章: このTaskDtoを状態として持つViewModelを作ります。