値オブジェクトとは何か
値オブジェクトは「値そのものを表す小さな不変の型」です。金額、メールアドレス、期間、座標、IDなど、意味とルールを持つ“ただの値”をクラス化して、検証・整合性・振る舞い(比較や計算)を中に閉じ込めます。重要なのは「同じ値なら同じもの」「生成後に変わらない」という性質です。
なぜ値オブジェクトにするのか
意味とルールを型に埋め込む
String や int のままだと「有効な値か」「範囲は正しいか」を毎回チェックする必要があります。値オブジェクトにすれば、生成時に一度検証して以後は安全に使えます。コードに“意味”が現れ、読み手にとって明快になります。
不変性でバグを減らし、比較が正確になる
作ってから変わらないので、途中状態や再代入のバグが消えます。等価性は“同じ値か”で判断でき、コレクションのキーにしても安全です(hashCode が変化しない)。
値オブジェクトの基本設計
不変にする(private final とセッター禁止)
フィールドは private final にし、コンストラクタで検証・確定します。書き換え用のメソッドは作らず、必要なら新インスタンスを返す「変換」だけを提供します。
public final class Email {
private final String value;
public Email(String raw) {
var v = normalize(raw);
if (!isValid(v)) throw new IllegalArgumentException("invalid email");
this.value = v;
}
public String value() { return value; }
private static String normalize(String s) {
return s == null ? "" : s.trim().toLowerCase(java.util.Locale.ROOT);
}
private static boolean isValid(String s) {
int at = s.indexOf('@');
return at > 0 && at == s.lastIndexOf('@') && at < s.length() - 1;
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Email e)) return false;
return value.equals(e.value);
}
@Override public int hashCode() { return value.hashCode(); }
@Override public String toString() { return value; }
}
Java等価性は“値が同じか”で判断する
同一性(同じインスタンスか)ではなく、等価性(値が同じか)を基準にします。equals/hashCode を必ず値に基づいて実装します。
代表例と小さな振る舞い
金額(通貨付き)と計算
通貨の異なる足し算を拒否するなど、ルールを型に埋め込みます。計算は新インスタンスを返す“副作用なし”にします。
public final class Money {
private final int amount;
private final String currency;
public Money(int amount, String currency) {
if (amount < 0) throw new IllegalArgumentException("amount>=0");
if (currency == null || currency.isBlank()) throw new IllegalArgumentException("currency");
this.amount = amount;
this.currency = currency.trim();
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) throw new IllegalArgumentException("currency mismatch");
return new Money(this.amount + other.amount, this.currency);
}
public int amount() { return amount; }
public String currency() { return currency; }
}
Java期間(start/end)と整合性
開始と終了の関係(end >= start)をコンストラクタで保証します。延長などの操作は新インスタンスで返します。
public final class Period {
private final java.time.LocalDate start;
private final java.time.LocalDate end;
public Period(java.time.LocalDate start, java.time.LocalDate end) {
if (start == null || end == null || end.isBefore(start)) {
throw new IllegalArgumentException("end >= start");
}
this.start = start;
this.end = end;
}
public Period extendDays(long days) {
if (days < 0) throw new IllegalArgumentException("days>=0");
return new Period(start, end.plusDays(days));
}
public java.time.LocalDate start() { return start; }
public java.time.LocalDate end() { return end; }
}
Javaコレクション・配列を含む場合の注意点
防御的コピーで不変を守る
可変の配列やリストをそのまま持つと、外から書き換えられて不変が崩れます。受け取り時・返却時にコピーまたは不変ビューにします。
public final class Tags {
private final java.util.List<String> list;
public Tags(java.util.List<String> input) {
var normalized = (input == null ? java.util.List.<String>of() : input).stream()
.map(s -> s == null ? "" : s.trim())
.filter(s -> !s.isEmpty())
.toList();
this.list = java.util.List.copyOf(normalized); // 不変ビュー
}
public java.util.List<String> asList() { return list; } // 変更不可のビュー
}
Javarecord の活用(値オブジェクトを簡潔に)
record は不変のデータキャリア
record は自動で private final フィールド、equals/hashCode/toString を生成します。検証が必要ならコンパクトコンストラクタを使います。
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) throw new IllegalArgumentException("non-negative");
}
}
Java配列など可変を持つなら、コンストラクタとアクセサでコピーして不変を保ちます。
境界での使い分けと実務上の指針
どこに導入するか
ID、メール、URL、金額、期間、電話番号、座標など「意味とルールがある単一値」は真っ先に値オブジェクト化します。ドメインロジックの条件分岐や検証が激減し、バグの侵入経路を塞げます。
DTO や永続化との関係
外部入出力(DTO)は可変でも構いませんが、受け取ったらすぐにドメインの値オブジェクトへ変換し、内部は不変で扱います。JPA を使う場合は、埋め込み値(Embeddable)や変換器を使って値オブジェクトのまま保存・復元できる形に寄せると整合性が保てます。
よくある落とし穴と回避
値オブジェクトにセッターを付けると不変が崩れ、型の価値が失われます。必ず不変にし、変更は新インスタンス返しで表現してください。equals/hashCode を未定義のままにすると、同じ値なのにコレクションで正しく扱えません。値に基づく等価を必ず実装します。配列やリストをそのまま持つと外から変更されるので、防御的コピーを徹底します。
仕上げのアドバイス(重要部分のまとめ)
値オブジェクトは「意味とルールを型に閉じ込めた不変の値」。生成時に検証して以後は安全、比較は値で、操作は新インスタンス返し。防御的コピーで不変を守り、record を活用して簡潔に書く。まず“意味のある単一値”から導入すると、ドメインの整合性とコードの明快さが劇的に向上します。
