導入
TaskTitleとPriorityが揃ったので、TaskItemのtitleをStringからTaskTitleに差し替え、Priority・complete()(完了にする)・rename()(タイトルを変える)を備えた最終形にします。あわせて、Entityがequalsをあえて上書きしない理由も確認します。
図解
classDiagram
class TaskItem {
+String getId()
+TaskTitle getTitle()
+Priority getPriority()
+boolean isCompleted()
+complete() void
+rename(TaskTitle) void
}
class TaskTitle {
+String getValue()
}
class Priority {
<<enumeration>>
LOW
MEDIUM
HIGH
}
TaskItem --> TaskTitle
TaskItem --> Priority
サンプル
public class Order {
private final String id;
private final String[] items;
public Order(String id, String[] items) {
this.id = id;
this.items = items;
}
public String getId() { return id; }
public String[] getItems() { return items; }
// equalsをあえて上書きしない=Objectの既定の参照比較のまま
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class OrderTest {
@Test
void nakamiGaOnajiDemo_IdGaChigaeba_BetsuNoChumon() {
Order order1 = new Order("1", new String[]{"本", "ペン"});
Order order2 = new Order("2", new String[]{"本", "ペン"});
assertNotEquals(order1, order2, "中身(商品構成)が同じでもIDが違えば別の注文");
}
}
Orderはequalsを上書きしていないので、Java既定の参照比較(Object#equals)になる- Entityにとってはこれで十分どころか好都合。「中身が同じだから同じ注文」と誤判定されずに済む
- 値オブジェクト(
TaskTitle)では値で等しいと判定したい一方、Entity(TaskItem)では区別したい――この非対称がポイント
演習
TaskItemを契約どおりの最終形にします。titleはTaskTitle型、priorityを持ち、complete()・rename(TaskTitle)で状態を変えられるようにしてください。前レッスンで作ったTaskTitleはTaskTitle.javaとしてそのまま使い、PriorityとTaskItem本体はTaskItem.javaに実装、テストはTaskItemTest.javaに分けます。
public final 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 boolean equals(Object o) {
if (!(o instanceof TaskTitle)) return false;
return value.equals(((TaskTitle) o).value);
}
@Override
public int hashCode() { return value.hashCode(); }
@Override
public String toString() { return value; }
}
public enum Priority {
LOW("低"), MEDIUM("中"), HIGH("高");
private final String label;
Priority(String label) { this.label = label; }
public String getLabel() { return label; }
}
// TODO: 次を満たす TaskItem クラスを実装してください
// ・id(String)は読み取り専用、コンストラクタで受け取る
// ・title(TaskTitle)・priority(Priority)は外からは getTitle()/getPriority() でのみ読める
// ・completed(boolean)は既定false、isCompleted()で読める
// ・complete()でcompletedをtrueにする
// ・rename(TaskTitle newTitle)でtitleを差し替える
// ・equalsはあえて上書きしない(Objectの既定の参照比較のままでよい)
public class TaskItem {
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TaskItemTest {
@Test
void titleToPriority_WoHojishiteiru() {
TaskTitle title = new TaskTitle("牛乳を買う");
TaskItem task = new TaskItem("1", title, Priority.HIGH);
assertEquals("牛乳を買う", task.getTitle().getValue(), "titleを保持している");
assertEquals(Priority.HIGH, task.getPriority(), "priorityを保持している");
assertFalse(task.isCompleted(), "作成直後は未完了");
}
@Test
void complete_DeKanryoNiNaru() {
TaskTitle title = new TaskTitle("牛乳を買う");
TaskItem task = new TaskItem("1", title, Priority.HIGH);
task.complete();
assertTrue(task.isCompleted(), "complete()で完了になる");
}
@Test
void rename_DeTitleWoHenkoDekiru() {
TaskTitle title = new TaskTitle("牛乳を買う");
TaskItem task = new TaskItem("1", title, Priority.HIGH);
task.rename(new TaskTitle("卵も買う"));
assertEquals("卵も買う", task.getTitle().getValue(), "rename()でタイトルを変更できる");
}
@Test
void nakamiGaOnajiDemo_IdGaChigaeba_BetsuNoTask() {
TaskTitle title = new TaskTitle("牛乳を買う");
TaskItem task = new TaskItem("1", title, Priority.HIGH);
TaskItem other = new TaskItem("2", title, Priority.HIGH);
assertNotEquals(task, other, "内容が同じでもIDが違えば別のタスク(Entityは参照比較のままでよい)");
}
}
- 期待される結果: 4件のテストがすべて成功
ヒント1を見る
TaskItem.javaに実装します。public class TaskItem { private final String id; private TaskTitle title; private 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; } }
ヒント2を見る
titleとpriorityのフィールドをただのprivate(finalを付けない)にすると、「コンストラクタ経由で受け取り・外からはゲッターのみ・クラス内のメソッドからは変更可能」という形になります
まとめ
TaskItemはid・title(TaskTitle)・priority・completedを持ち、complete()/rename()でふるまいを表現する- Entityは
equalsをあえて上書きせず、既定の参照比較のままにしてよい(値オブジェクトとの対比) - 「重複タイトルの禁止」「完了済みタスクの扱い」のようなアプリ全体に関わるルールは、この後のRepository・Serviceの章で扱う
次章: new TaskItem(...)の呼び出しを1か所にまとめるFactoryパターンです。