本文へスキップ
BecomeCoder

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

値オブジェクト ― TaskTitle

ブラウザで完結

導入

オブジェクト(Value Object)は「値そのものが同一性」を持つ型です。1000円は1000円というだけで等しく、どの1000円かを区別する必要はありません。タスクのタイトルも「空文字は不可・50文字まで」というルールを持つ以上、ただのStringのまま各所でチェックするより、専用の型TaskTitleにまとめた方が安全です。

図解

flowchart TB
    A["Stringのまま各所で<br/>if (title.length() > 50) throw ..."] -->|検証がバラバラで漏れる| X["バグの温床"]
    B["TaskTitleに一本化"] -->|生成時に必ず検証| Y["不正な値が存在しえない"]

サンプル

public final class Money {
    private final int amount;

    public Money(int amount) {
        if (amount < 0) throw new IllegalArgumentException("金額は0以上です");
        this.amount = amount;
    }

    public int getAmount() { return amount; }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Money)) return false;
        Money other = (Money) o;
        return amount == other.amount;
    }

    @Override
    public int hashCode() { return Integer.hashCode(amount); }
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class MoneyTest {
    @Test
    void onajiKingaku_HaOnajiAtaiToShiteAtsukau() {
        Money m1 = new Money(1000);
        Money m2 = new Money(1000);
        assertEquals(m1, m2, "同じ金額は同じ値として扱う");
    }

    @Test
    void minasuNoKingaku_HaTsukurenai() {
        assertThrows(IllegalArgumentException.class, () -> new Money(-1), "負の金額は作れない");
    }
}
  • final classにして、コンストラクタで不正な値をthrowして弾く(生成できた時点で常に正しい状態が保証される)
  • 値で等しいと判定したいので、equals(と対になるhashCode)を自分で上書きする
  • assertThrows(型.class, () -> ...)は「期待どおり例外が発生したか」を確認するJUnit標準のアサーション。実行する処理はラムダ式で渡す。これ以降、検証ロジックのテストで多用する

演習

契約どおりのTaskTitleをTDDで実装します。検証ロジックの実装はTaskTitle.java、テストはTaskTitleTest.javaという別ファイルに分けて書きましょう。

public final class TaskTitle {
    // TODO: 次のルールを満たす TaskTitle を実装してください
    //   ・value(String)を getValue() で公開する
    //   ・null/空文字/空白だけの値は IllegalArgumentException で拒否する
    //   ・50文字を超えたら IllegalArgumentException で拒否する
    //   ・前後の空白はトリムして保持する(String#trim() / String#strip())
    //   ・equals(Object) / hashCode() を実装し、valueが同じなら等しいとみなす
    //   ・toString() は value をそのまま返す
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class TaskTitleTest {
    @Test
    void zengoNoKuhaku_HaTrimSareru() {
        TaskTitle t1 = new TaskTitle("  牛乳を買う  ");
        assertEquals("牛乳を買う", t1.getValue(), "前後の空白はトリムされる");
    }

    @Test
    void onajiMojiretsu_NaraAtaiToShiteHitoshii() {
        TaskTitle t1 = new TaskTitle("牛乳を買う");
        TaskTitle t2 = new TaskTitle("牛乳を買う");
        assertEquals(t1, t2, "同じ文字列なら値として等しい");
    }

    @Test
    void karaBunjiHaTsukurenai() {
        assertThrows(IllegalArgumentException.class, () -> new TaskTitle(""), "空文字は作れない");
    }

    @Test
    void kuhakuDakeMoTsukurenai() {
        assertThrows(IllegalArgumentException.class, () -> new TaskTitle("   "), "空白だけも作れない");
    }

    @Test
    void gojuIchiMojiHaTsukurenai() {
        assertThrows(IllegalArgumentException.class, () -> new TaskTitle("a".repeat(51)), "51文字は作れない");
    }
}
  • 期待される結果: 5件のテストがすべて成功
ヒント1を見る

TaskTitle.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; } public boolean equals(Object o) { if (!(o instanceof TaskTitle)) return false; return value.equals(((TaskTitle) o).value); } public int hashCode() { return value.hashCode(); } public String toString() { return value; } }

ヒント2を見る

空白判定はvalue.isBlank()nullチェックとは別に必要)、整形はvalue.trim()を使います。文字数のチェックはトリム前でも後でも、51文字の例では結果は変わりません

まとめ

  • 値オブジェクトは値そのものが同一性。不変(フィールドはfinal)にして、コンストラクタで不正値を拒否する
  • TaskTitleは「空文字禁止・50文字以内・トリムする」という検証ロジックを1か所に閉じ込めた
  • assertThrows(型.class, () -> ...)のように、例外が起きることを確認するテストの書き方も手に入れた

次回: 決まった選択肢から1つを選ぶ値、Priority(優先度)をenumで表現します。

実際に動かしてみよう

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

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

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