本文へスキップ
BecomeCoder

Java MVVMコース · 第2章 Entity ― 業務データの核 · レッスン8

Entityの同一性とふるまい ― TaskItemを完成させる

ブラウザで完結

導入

TaskTitlePriorityが揃ったので、TaskItemtitleStringからTaskTitleに差し替え、Prioritycomplete()(完了にする)・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が違えば別の注文");
    }
}
  • Orderequalsを上書きしていないので、Java既定の参照比較Object#equals)になる
  • Entityにとってはこれで十分どころか好都合。「中身が同じだから同じ注文」と誤判定されずに済む
  • オブジェクトTaskTitle)では値で等しいと判定したい一方、Entity(TaskItem)では区別したい――この非対称がポイント

演習

TaskItemを契約どおりの最終形にします。titleTaskTitle型、priorityを持ち、complete()rename(TaskTitle)で状態を変えられるようにしてください。前レッスンで作ったTaskTitleTaskTitle.javaとしてそのまま使い、PriorityTaskItem本体は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を見る

titlepriorityのフィールドをただのprivatefinalを付けない)にすると、「コンストラクタ経由で受け取り・外からはゲッターのみ・クラス内のメソッドからは変更可能」という形になります

まとめ

  • TaskItemidtitle(TaskTitle)・prioritycompletedを持ち、complete()/rename()でふるまいを表現する
  • Entityはequalsをあえて上書きせず、既定の参照比較のままにしてよい(値オブジェクトとの対比)
  • 「重複タイトルの禁止」「完了済みタスクの扱い」のようなアプリ全体に関わるルールは、この後のRepository・Serviceの章で扱う

次章: new TaskItem(...)の呼び出しを1か所にまとめるFactoryパターンです。

実際に動かしてみよう

このレッスンのサンプルは、実務と同じように役割ごとの .java ファイルへ分けてあります。下のエディタは最初からその複数ファイルが入った状態で、上のタブでファイルを切り替えられます。そのまま「▶ 実行」を押せば全ファイルをまとめて解釈して動かせます。@Test の付いたテストがあるレッスンでは、テストメソッドごとに ✅/❌ の一覧(Red/Green)が出るので、まずテストを赤くしてから実装で緑にする、というTDDの回し方をその場で体験できます(本物のJVMではなく、JUnit・Mockitoの主要な書き方まで再現した学習用シミュレータです)。

Java — ブラウザ内で実行(学習用シミュレータ)

Javaの教材サブセットを動かす学習用シミュレータを読み込みます(本物のJVMではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。