はじめに
同期との輪読会で「値オブジェクト」について議論しましたが、改めて整理した内容をまとめます。
輪読会で読んでいる本はドメイン駆動設計入門になります。
(一部のサンプルコードについては完全ではないものもあるので、ご注意ください!)
値オブジェクトとは
値オブジェクト1とはシステム内に登場する金銭や単位などの値をオブジェクトとして定義することです。
例えば、お金は「金額 + 単位」で表されます。
金額:1000
通貨:JPY(日本円)
このようなお金はIDを持つ必要がありません。
なぜなら、お金というものは「いくらか」「どの通貨か」という値そのものが意味のすべてだからです。
1000円(JPY)
は誰が所持しようとその価値は変わらないが、2000円(JPY)
や$1000(USD)
はその価値が異なります。
このように値オブジェクトは、識別子(ID)を持たず、内部の属性(値)によってのみ区別されるオブジェクトのことです。
値オブジェクトの特徴
値オブジェクトの特徴として
- 不変性
- 交換が可能である
- 等価性は値で判断される
があります。
これからの説明では次の住所の値オブジェクトを使って説明します。
public class Address{
private final String prefecture;
private final String city;
private final String street;
public Address changeStreet(String newStreet) {
return new Address(this.prefecture, this.city, newStreet);
}
}
不変性
一度生成された値オブジェクトは値や状態を変更することができません。
状態を変える場合、新しいインスタンスを生成する必要があります。
Address a1 = new Address("東京都", "新宿区", "西新宿2-8-1");
log.info(a1) //東京都新宿区西新宿2-8-1
Address a2 = a1.changeStreet("西新宿2-8-2"); // 新しい住所インスタンスを生成する
log.info(a2) //東京都新宿区西新宿2-8-2
a1
の値は不変で、変更するときはa2
のように別のインスタンスを返します。
このようにすることで、副作用のない安全な設計ができます。
下記のように直接属性を変更することは値オブジェクトとして、不変性の観点からNGとなります。
public class NgAddress{
private final String prefecture;
private final String city;
private final String street;
public NgAddress changeStreet(String newStreet) {
this.street = newStreet;
return this;
}
}
NgAddress a1 = new NgAddress("東京都", "新宿区", "西新宿2-8-1");
log.info(a1) //東京都新宿区西新宿2-8-1
NgAddress a2 = a1.changeStreet("西新宿2-8-2") //これは属性を変更しているためNG
log.info(a1) //東京都新宿区西新宿2-8-2
log.info(a2) //東京都新宿区西新宿2-8-2
交換が可能である
値オブジェクトが不変であるため、属性(値)の更新・交換はインスタンスを交換することによって行います。
Address myAddress = new Address("東京都", "新宿区", "西新宿2-8-1");
log.info(myAddress) //東京都新宿区西新宿2-8-1
//引っ越ししたので、住所を更新する
myAddress = new Address("北海道", "札幌市中央区", "北3条西6丁目");
log.info(myAddress) //北海道札幌市中央区北3条西6丁目
等価性は値で判断される
2つの値オブジェクトが「同じかどうか」は、中身の値が一致しているかどうかで判断します。
Address a1 = new Address("東京都", "新宿区", "西新宿2-8-1");
Address a2 = new Address("東京都", "新宿区", "西新宿2-8-1");
System.out.println(a1.equals(a2)); // true(同じ住所)
a1
とa2
は異なるインスタンスでも、属性(値)が完全に一致していれば、等価であるとみなされます。
完全な値オブジェクトの実装
先ほどの住所オブジェクトを完全なコードで実装すると下記のようになります。
import java.util.Objects;
public class Address {
private final String prefecture;
private final String city;
private final String street;
public Address(String prefecture, String city, String street) {
if (prefecture == null || city == null || street == null) {
throw new IllegalArgumentException("住所の各項目は null にできません");
}
this.prefecture = prefecture;
this.city = city;
this.street = street;
}
public String getPrefecture() {
return prefecture;
}
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
public Address changeStreet(String newStreet) {
return new Address(this.prefecture, this.city, newStreet);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Address)) return false;
Address other = (Address) o;
return prefecture.equals(other.prefecture) &&
city.equals(other.city) &&
street.equals(other.street);
}
@Override
public int hashCode() {
return Objects.hash(prefecture, city, street);
}
@Override
public String toString() {
return prefecture + city + street;
}
}
ただし実際の現場の実装では
実際の現場では、ライブラリやフレームワークを使用して実装しています
import lombok.Value;
@Value
public class Address {
String prefecture;
String city;
String street;
public Address changeStreet(String newStreet) {
return new Address(this.prefecture, this.city, newStreet);
}
}
上記は、Lombok2というコード生成補助のライブラリです。
アノテーションで@Value
をつけることにより、
- 不変なクラス(immutable class)
- final フィールド
- コンストラクタ
- equals(), hashCode(), toString()
を自動生成してくれます。
なぜ値オブジェクトを使うのか?
それは一言で言えば、
モデルの意味を明確にし、コードの安全性・再利用性・保守性を高めるためです。
具体的な理由として、
- 意味のある型を作れる
- 不変性によりバクを防げる
- 等価性が判断しやすい
- ドメインの知識を表現できる
- コードの再利用性と保守性が高まるから
より詳細な説明は随時更新していきます。><
私自身は値オブジェクトを使用できる場面では、積極的に使用するべきだと考えています。
(もちろん現場の意思決定によります)
終わりに
値オブジェクトの概念をざっくり整理しました。
一部コードについては完全ではないものもあるので、ご注意ください!
ご指摘・ご助言があればコメントお願いします!
今後も輪読の内容などをまとめていきます!