オブジェクト指向においては、値オブジェクトを使用することによって、優れた設計を行うことができます。
値オブジェクトとは何か
値オブジェクトとはその名の通り、業務で出現する値とそれにまつわるルールを保持するオブジェクトのことです。
例えば以下のようなオブジェクトです。
public class Quantity {
private int value;
// 上限100個
private static final int LIMIT = 100;
public Quantity(int value) {
// 業務ルール:0 ~ 100の整数であること
if(value < 0 || value > LIMIT) {
// Exception
}
this.value = value;
}
public int getValue() {
return this.value;
}
}
このオブジェクトは数量の値の他に、その値が一定の範囲内のものであるというルールを保持しています。
値オブジェクトの必要性
なぜ値オブジェクトが必要なのでしょうか。
わざわざこのようなオブジェクトを作らなくても、intで表現することは可能です。
しかし、その場合Quantityは途方もない数や負数を設定できてしまいます。
これは明らかに業務で想定されたものではありません。
バグが発生する原因にもなります。
これを値オブジェクトを使用することによって防ぐことができます。
値オブジェクトの必要性は、型の厳格化ということになります。
例えば、オブジェクトのあるプロパティがマスタテーブルのコード値を持つとします。
その場合、おそらくそのプロパティの型をコードのEnum型にするかと思います。
これは、そのプロパティに意図しない不正な値が入るのを防ぎたいからです。
値オブジェクトも本質的にはそれと同じことです。
不正な値が設定された場合のチェックロジックを設けるという手段もあります。
しかし、それをやってしまうと処理の本筋ではないif文が増えてしまいます。
また、似たようなロジックが色々な個所に散在してしまいます。
これは優れた設計とは言えません。
型の厳格化により静的解析段階でバグを検知できる
型を厳格化することで、静的解析段階でバグを検出することができます。
例えば以下のようなメソッドがあるとします。
public Amount calcAmount(int price, int quantity) { ... }
このメソッドはint型である価格と量の引数を二つ取ります。
このメソッドの問題点は、それが入れ違いになっていても発覚しないということです。
実行して結果を確認するまでバグが発覚しないのです。
最悪の場合、顧客が請求書を見た時に気が付くかもしれません。
しかし、値オブジェクトを使用するように書き換えることでそれを防げます。
public Amount calcAmount(Price price, Quantity quantity) { ... }
価格と量でそれぞれ専用の値オブジェクトを用意します。
そうすることで引数の渡し間違いが無くなります。
このように型を厳格化することにより、実行前のコンパイルの段階でバグを検知することができます。
オブジェクトにする必要性
ルールを厳格にするならチェックユーティリティを作ることでも可能です。
しかし、これだとチェックユーティリティを使用するか否かが実装者にゆだねられてしまいます。
実装者がチェックをかけるのを忘れてしまえばそれまでです。
値を使用するたびにチェックロジックを実施しないといけないため、同じような記述が散在してしまいます。
それならば、オブジェクトの中にルールをまとめてしまった方が記述を簡潔にできます。
そのユーティリティはその値が存在する時にしか使用されないのであれば、値とルールをまとめてしまった方がプログラムの見通しが良くなります。
また、チェックをかけるということは、ロジックの中でその値がチェックされるべき値であることを知っていないといけません。
呼び出し側が呼び出される側の事情を知りすぎてしまうことは密結合につながり、処理の独立性を損ないます。
そうした理由から値とルールを一体化させた値オブジェクトを使用するのが良いのです。
値オブジェクトでは状態を変更しない
値オブジェクトでは状態を変更しません。
状態とはインスタンス変数のことと考えてもらえればよいです。
状態を変更してしまうと、値を追いにくくなりバグの温床となります。
そのため値オブジェクトでは以下を原則とします。
・Setterやインスタンス変数を変更するメソッドを作らない
・インスタンス変数はコンストラクタで設定して以降は不変(完全コンストラクタ)
・新しい値が必要になったら、その都度新しいインスタンスを生成する
これらの原則を守って作った値オブジェクトは以下のようになります。
public class Price {
BigDecimal value;
public Price(BigDecimal value) {
this.value = value;
}
public BigDecimal getValue() {
return this.value;
}
// NGな値引き処理
public void minus(BigDecimal discount) {
// NG:インスタンス変数を変更してしまっている
this.value = this.value.subtract(discount);
}
// OKな値引き処理
public Price minus(BigDecimal discount) {
// OK:新しいオブジェクトを生成している
return new Price(this.value.subtract(discount));
}
}
オブジェクトの内部値が変更されるとバグが起きやすくなります。
なので、一つのオブジェクトを値を変更して使い回さず、使用される範囲を狭めましょう。
この考え方は説明変数と同じです。