はじめに
immutableでないオブジェクトを扱うクラスをvalueディレクトリに置いてしまい、レビューで指摘をいただきました。この経験から学んだことをまとめたいと思います。
- DDD (Domain-Driven Design): ドメイン駆動設計。ビジネスロジック(ドメイン)を中心にソフトウェア設計を行う手法
- immutable: 不変。一度作成したら値を変更できないこと
ValueObjectとは
ValueObjectとは、値そのものを表現するオブジェクトのこと。DDDにおけるvalueディレクトリには、このValueObjectを配置します。
ValueObjectは以下の3つの特徴を持ちます
- 不変である (Immutable)
- 交換可能である (Replaceable)
- 等価性によって比較される (Compared by Value)
それぞれの特徴を詳しく見ていきましょう。
1. 不変である (Immutable)
ValueObjectは、一度作成したら内部の値を変更することができません。
例えば、金額を表すMoneyクラスを考えてみましょう
public record Money(int amount, String currency) {
}
Java 16から導入されたレコードクラスを使うことで、ValueObjectを非常にシンプルに表現できます。
レコードクラスは以下の特徴があります
- 全てのフィールドが自動的に
private finalになる - コンストラクタが自動生成される
- getterが自動生成される(
getAmount()ではなくamount()という名前) -
equals()、hashCode()、toString()が自動実装される - setterは存在しない(不変性が保証される)
値を変更したい場合は、新しいインスタンスを作成して返します。
2. 交換可能である (Replaceable)
price1, price2自体は変更できないものの、同じ箱を使って異なる値を入れることは可能です!
Money price1 = new Money(1000, "JPY");
Money price2 = new Money(2000, "JPY");
3. 等価性によって比較される (Compared by Value)
ValueObjectは、オブジェクトの同一性(同じインスタンスか)ではなく、値の等価性(同じ値を持つか)で比較します。
Money money1 = new Money(1000, "JPY");
Money money2 = new Money(1000, "JPY");
// 異なるインスタンスだが、値が同じなので等しい
System.out.println(money1.equals(money2)); // true
immutableでないオブジェクトをvalueディレクトリに置くべきでない理由
ValueObjectの最も重要な特徴は「不変性」です。もし可変なオブジェクトをvalueディレクトリに配置してしまうと以下の問題が発生します。
- コードを読む人が混乱する(「ValueObjectのはずなのに値が変わる?」)
- 予期しない副作用が発生する可能性がある
- DDDの設計思想に反する
そのため、可変なオブジェクトは別の場所に配置する必要があります。
Entityとは
immutableでないオブジェクトはEntityとして扱います。
Entityは以下の3つの特徴を持ちます
- 可変である (Mutable)
- 同じ属性であっても区別される
- 同一性により区別される (Compared by Identity)
1. 可変である (Mutable)
Entityは、ライフサイクルの中で状態が変化します。
public class User {
private final Long id; // IDは不変
private String name; // 名前は変更可能
private String email; // メールアドレスも変更可能
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// setterを提供し、状態の変更を許可
public void changeName(String newName) {
this.name = newName;
}
public void changeEmail(String newEmail) {
this.email = newEmail;
}
}
2. 同じ属性であっても区別される
Entityは、たとえ全ての属性値が同じでも、異なるオブジェクトとして扱われます。
User user1 = new User(1L, "田中太郎", "tanaka@example.com");
User user2 = new User(2L, "田中太郎", "tanaka@example.com");
// 名前とメールアドレスが同じでも、異なるユーザー
System.out.println(user1.equals(user2)); // false (IDが異なる)
現実世界で例えるなら、同姓同名の人がいても、それぞれ別の人として扱われますよね。それと同じイメージです。
3. 同一性により区別される (Compared by Identity)
Entityは、一意の識別子(ID)によって区別します。
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
User user = (User) obj;
// IDのみで比較する
return id.equals(user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
名前やメールアドレスが変わっても、IDが同じであれば同じユーザーとして扱われます。
ValueObjectとEntityの使い分け
| 項目 | ValueObject | Entity |
|---|---|---|
| 可変性 | 不変 (Immutable) | 可変 (Mutable) |
| 識別方法 | 値で識別 | IDで識別 |
| 交換可能性 | 交換可能 | 交換不可 |
| 例 | 金額、日付、住所、色 | ユーザー、注文、商品 |
判断基準としては
- 「それ自体に個性があるか?」 → Yes ならEntity
- 「単なる値の組み合わせか?」 → Yes ならValueObject
まとめ
-
ValueObjectは不変で、値そのものを表現するオブジェクト
- 金額、日付、住所などが該当
- 一度作成したら変更できない
- 値が同じなら交換可能
-
Entityは可変で、同一性を持つオブジェクト
- ユーザー、注文、商品などが該当
- ライフサイクルの中で状態が変化する
- IDで識別される
参考資料
- 『ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本』(成瀬允宣 著)