導入
値オブジェクト(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で表現します。