最近話題になっている「良いコード/悪いコードで学ぶ設計入門」という書籍を読みました。
非常に勉強・参考になる点が多い本でした。
その中でも今回は特に勉強になった、ValueObjectについてアウトプットしてみます。
この記事の内容はあくまで値オブジェクトについて学び始めたばかりのuhablogが書いていますので、間違いが含まれる可能性があります。
「しっかりとValueObjectについて学びたい!」という方はこちらの記事が参考にしてください。
https://qiita.com/MinoDriven/items/5e69d9bd028aa350e2c4
値オブジェクト(ValueObject)とは
値オブジェクトというのは、開発していくシステムの中で出てくる様々なデータをクラスとして扱って、オブジェクトとして表現したものです。
値オブジェクトではインスタンス変数を持ちます。
しかし基本的にこれらのインスタンス変数を変更することができません。
そしてこのインスタンス変数を変更し、正常に操作することを保証したメソッドを持ちます。
また、値オブジェクト同士を比較したときに属性(インスタンス変数)の値が同じであれば、同一のものとみなされます。
まとめると、値オブジェクトとは
- システムの中で出てくるデータをクラスで表現したもの
- インスタンス変数を持ちインスタンス変数の変更はできない
- インスタンス変数を正常に操作するメソッドを持つ
- インスタンス変数の値が同じであれば、同一のものとみなす
具体的にはどんなものでしょうか?
実際のコードで確認してみます。
実際のコードで値オブジェクト(ValueObject)
今回は例として「お金」を値オブジェクトとして実装していきます。
何も考えることなく、実装を始めると「お金」は数字なので、int型のデータとして扱うことが多いと思います。
int money = 100;
しかしこれでは問題点があります。
例えば、int型の変数ではマイナスの値も扱うことができてしまうので、−100という数値を入れることが可能です。
int money = -100;
このようにクラスを作ることなく、データを扱うことで謎の値(不正値)が紛れ込んでしまう可能性があります。
謎の値(不正値)の混入はそのままバグに直結しかねません。
そこで登場するのが値オブジェクトになります。
お金をクラスにして、謎の値(不正値)の混入を防ぐ
では「お金」をクラスで表現していきます。
package money;
public class Money {
/**
* finalをつけて、変更不可にする
*/
private final int amount;
/**
* コンストラクタでインスタンス変数に値を代入する
* @param amount
*/
public Money(int amount) {
// 0より小さい時には例外を発生させる
if (amount < 0) {
throw new IllegalArgumentException();
}
this.amount = amount;
}
/**
* 金額を返却する
* @return
*/
public int getAmount() {
return amount;
}
}
ポイント1:privateでfinalなインスタンス変数
ポイントの一つ目は「privateでfinalなインスタンス変数」を持つことです。
これにより外部から謎の値を入れられることを防ぎ、インスタンス変数を安全に保つことが可能になります。
finalにしておくことで、変更不可になりますが、新しい値を扱いたくなったときは別のインスタンスを作ります。
/**
* finalをつけて、変更不可にする
*/
private final int amount;
ポイント2:コンストラクタで値のチェック
二つ目のポイントはコンストラクタで値のチェックをして、謎の値が入ってしまうことを防いでいます。
このコンストラクタのおかげで、インスタンス変数amountにマイナスの値が入ることを防ぐことができます。
/**
* コンストラクタでインスタンス変数に値を代入する
* @param amount
*/
public Money(int amount) {
// 0より小さい時には例外を発生させる
if (amount < 0) {
throw new IllegalArgumentException();
}
this.amount = amount;
}
ポイント3:setterは作らない
このクラスにはgetterはありますが、setterはありません。
なぜならsetterがあることで、謎の値を入れることが可能になってしまいます。
せっかくコンストラクタで謎の値が入らないようにしていても、setterで入れられてしまっては元も子もありません。
まあ、finalがついているので、値を入れることは不可能なのですが。
「それじゃあ値の変更ができないじゃないか」と思うかもしれませんが、違う値を入れたいならもう一度インスタンス化して別のオブジェクトとして扱うべきです。
ちなみに今回はgetterを追加していますが、getterも不要であれば追加する必要はありません。
またgetterを利用するのは単純にその値を画面に表示したかったりする場合に限って使うようにします。
getterで取得した値を使って、ロジックを書くことは好ましくありません。
そのロジックはクラス内に書くべきロジックになります。
値オブジェクト(ValueObject)にメソッドを作る
次にMoneyクラスに金額の加算をするメソッドを追加します。
このメソッドのポイントは戻り値としてMoneyクラスを返却していることです。
受け取ったint型の引数と元々持っているフィールドを加算した値で新しいMoneyオブジェクトを作成し、そのオブジェクトを返却します。
/**
* 金額を加算する
* @param addedAmount
* @return 加算されたMoneyオブジェクト
*/
public Money add(int addedAmount) {
return new Money(this.amount + addedAmount);
}
メソッドの問題点
しかし実はこのメソッドには問題点があります。
それは引数のint型にどんな値が入れられるかわからないという点です。
このメソッドを使う側でこんなことが行われるかもしれません。
// Moneyクラスをインスタンス化
Money money1 = new Money(100);
// 高さを保持するint型の変数heightに185を代入
int height = 185;
// Moneyクラスのaddメソッドに高さを渡す
Money money2 = money1.add(height);
このようにこのままでは謎の値が入ってしまう可能性があり、バグに繋がってしまいそうです。
メソッドの解決策
問題を解決するために、引数の型をint型からMoney型に変更します。
これでお金以外の値を表現した謎のint型をぶち込まれることを防ぐことができます。
/**
* 金額を加算する
* @param addedMoney
* @return 加算されたMoneyオブジェクト
*/
public Money add(Money addedMoney) {
return new Money(this.amount + addedMoney.getAmount());
}
比較するメソッドの準備
値オブジェクトの特徴として、比較可能であることという特徴もあります。
なので、equalsメソッドでオブジェクト同士を比較できるようにしましょう。
/**
* 金額を比較する
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
// Moneyクラス以外のインスタンスの場合はfalse
if (!(obj instanceof Money)) return false;
return this.amount == ((Money) obj).getAmount();
}
値オブジェクトまとめ
今回は値オブジェクトについてJavaで実装しながらアウトプットしてみました。
設計をしっかりと行い、変更が容易な堅牢なシステムを開発していくことで、エンジニアは幸せになれます。
値オブジェクトはそんな堅牢なシステムを作るための基礎となる考え方です。
設計について学んでみたいと思ったエンジニアの方は「良いコード/悪いコードで学ぶ設計入門」ぜひ手に取ってみてください。オススメの技術書です。