はじめに
値オブジェクトを知らずにコードを書いている見習いエンジニアが多すぎる!!
私のことです、ごめんなさい。
n番煎じな上にお粗末な記事ですが、見習い・駆け出しエンジニアの0→1になればと思います。
値オブジェクトとは
値オブジェクト(Value Object)とは、アプリケーション内で使用する値をそのルールや制限に基づいて扱うための専用クラスです。
簡単にいうと、こういうことです。本当は他にも色々な定義がありますが、それらを考えるのは今は良いです。
決して、説明できないということではないです。決して。
とりあえず具体例
javaでクレジットカードを扱うアプリケーションを開発するとしましょう。
あなたは、カード番号をLong型で定義しますか?String型で定義しますか?
値オブジェクトを用いる場合、CreditCardNumber型で定義します。
実装例は以下です。
(カード番号の定義は12桁の数字、ただし先頭の数字は1~9とします。)
class final CreditCardNumber {
String value;
CreditCardNumber(String value) {
this.value = value;
}
private void checkValue(String value) {
if (!isCorrectValue(value)) {
throw new IllegalArgumentException();
}
}
private boolean isCorrectValue(String value) {
if (value == null) {
return false;
}
if (value.matches("[1-9][0-9]{11}")) {
return true;
}
return false;
}
}
値オブジェクトを使用するメリットって何なの?
値オブジェクトを利用することで、以下のようなメリットが得られます。
- 定義上不適切な値が混入することを避けられる
- コードが分かりやすく、安全になる
- 定義の変更もそのクラスに閉じ込めやすくなる
- 業務ロジックを閉じ込めることができる
またまた具体例
文章だけだと分かりにくいので具体例を示します。
先ほどの例を引き続き使い、CreditCardクラスを定義します。
まず、javaの基本型だけで定義します。
class final CreditCard {
String firstName;
String lastName;
String creditCardNumber;
LocalDate cardExpiration;
CreditCard(String firstName, String lastName, String creditCardNumber, LocalDate cardExpiration) {
this.firstName = firstName;
this.lastName = lastName;
this.creditCardNumber = creditCardNumber;
this.cardExpiration = cardExpiration;
}
}
上記の実装には、以下のようなデメリットがあります。
- 定義上不適切な値が混入する可能性がある。例えば、「カード番号は12桁の数字」という定義があってもそれ以外の値が混入する可能性があります。
- このクラスのコンストラクタを使用する際、分かりにくく、バグ混入の可能性がある。例えば、第1〜3引数は全てString型であり、それぞれの順番を間違えて代入する可能性があります。
- カード番号などの定義適用がどこでされるのかが不明瞭であり、変更の工数(影響範囲調査も含む)、バグの混入可能性が増えます。
先程列挙した値オブジェクトのメリットも見るとこれらのデメリットが解消できそうです。
それでは、値オブジェクトを使用した実装を見てみましょう。
class final CreditCard {
UserName userName;
CreditCardNumber creditCardNumber;
CardExpiration cardExpiration;
CreditCard(UserName userName, CreditCardNumber creditCardNumber, CardExpiration cardExpiration) {
checkNull(userName, creditCardNumber, cardExpiration);
this.userName = userName;
this.creditCardNumber = creditCardNumber;
this.cardExpiration = cardExpiration;
}
private void checkNull(Object... args) {
for(Object obj : args) {
if (obj == null) {
throw new NullPointerException();
}
}
}
}
上記のように実装することで、基本型のみの実装におけるデメリットを解消できていることがわかります。
- カード番号は、nullでなければCreditCardNumberクラスで値の検証がされていることが保証されています。
- コンストラクタの引数はそれぞれの型で明確であり、誤った値の代入は起こりにくいです。
- 値の定義検証がどこでされているかは明確であり、変更箇所の特定も容易です。(場合によっては影響調査は同様に大変な可能性もあるが、バグの混入可能性は減ると思います。)
さいごに
今回は未熟者なりに「値オブジェクト」について記事を書いてみました。
値オブジェクトを意識して実装することは大切ですが、何でもかんでも値オブジェクトで実装すればよいというわけでもないので、試行錯誤しながらより良い実装を目指しましょう。(私も人のことは言えないですが)
また、もし間違っている箇所や、追記したほうが良い箇所がありましたらコメントしていただけると嬉しいです!