想定読者
- クラス設計に悩みを持つ業務アプリケーションエンジニア
- 値オブジェクト(Value Object)の概要を理解したい方
よくあるクラスとその問題点
よくあるクラス
JavaやPythonなどのオブジェクト指向言語を使って、あるクラスを作ることにしましょう。以下の例では、タスクをクラスとして表現しており、タスク名、ポイント、及び期限をフィールドとして保持しています。String型やint型を使ったよくあるクラスです。
String taskName;
int point;
Date dueDate;
...
保持する各フィールドに対してgetter/setterメソッドを保持するもよく見かける実装パターンです。
public Point getPoint() {
return point;
}
public void setPoint(int point) {
this.point = point;
}
...
"よくある"クラスの特徴を簡単にまとめます。
- int型やDate型など、言語で用意された型を使用してフィールドが宣言されている
- getter/setterメソッドが実装されている
このような一般的なクラスの一体何が問題なのでしょうか。
よくあるクラスの問題点
さきほど二つの特徴をあげましたが、よくあるクラスにはこれらに関連した大きな問題点があります。それは、業務アプリケーションを作り上げるために存在するクラスであるにも関わらず、**「業務ルールに反した値や操作を許す構造になっている」**ことです。実際のソースコードを見てみましょう。
コードで見てみる問題点
例として、先のクラスのポイント(point)というフィールドを考えることにします。仮に、「ポイントは0から1000までとすること」という業務ルールがあるとしましょう。しかし、Taskクラスにおけるポイントはint型で宣言されていますから、こんなこともできてしまいます。
task.setPoint(-2000);
-2000という業務ルールに反したポイントが設定されています。
このようなことを可能にしてしまうと、ビジネスロジック上でバグが生まれやすくなります。さらに言えば、業務ルールから見た論理的なバグであり、プログラムとしてエラーは発生しないため、発見が難しくなってしまうという大きな問題を抱えることになります。
当然、int型やDate型が個別のアプリケーションの業務ルールなんて知るわけないので、業務ルールに反する行為を防ぐことはできません。さらに、setterメソッドを許すことで外部から何の値でも自由に設定できてしまいます。
問題解決の方法 -値オブジェクトを使ってみる-
値オブジェクト(Value Object)を私なりの解釈で説明すると、**「業務で使う単位や値のルールをクラスとして表現したもの」**です。
コードで見てみる値オブジェクト
実際のコードを見てみるとなんとなくわかると思います。
ポイントは以下2点です。
- 業務ルールを定義する(MIN, MAXなどの制約を明確に宣言する)
- 業務ルールに反する操作を許容しない(反した場合は例外を投げる)
元々はint型に対してsetterで「何でも設定OK」な状態でしたが、業務ルールを無視したsetterは使いません。というか作ってはいけません。値オブジェクトでは、業務ルールに基づいて値が設定されるようなメソッドを作成する必要があります。
例えば、addメソッドを呼び出すと、次にcanAddが呼び出されて業務ルールに反した値(0から10000以外の値)が渡されていないかがチェックされます。問題ないならそのまま新たなPointのインスタンスが生成され、問題ありなら業務ルールに反する値が設定されないように例外が投げられます。
public class Point {
static final int MIN = 0;
static final int MAX = 10000;
int value;
public Point(int value) {
if (value < MIN) throw new
IllegalArgumentException("不正:" + MIN + "未満");
if (value > MAX) throw new
IllegalArgumentException("不正:" + MAX + "オーバー");
this.value = value;
}
Point add(Point other) {
if (!canAdd(other)) throw new
IllegalArgumentException("不正:合計が" + MAX + "以上");
int added = addValue(other);
return new Point(added);
}
boolean canAdd(Point other) {
int added = addValue(other);
return added <= MAX;
}
private int addValue(Point other) {
return this.value + other.value;
}
}
作成した値オブジェクトPointは、元々のTaskクラスで宣言して使用します。
//ポイントのみを値オブジェクトに変えています
String taskName;
Point point; //元々は[int point;]
Date dueDate;
また、先に述べた"よくある"クラスとは違い、値オブジェクトを見るだけで業務ルールの概要が容易に理解できます。
このように値オブジェクトは、業務ルールをクラスとして表現することで、バグの混入を防ぐだけでなく仕様理解の助けにもなるという非常に便利な設計方式なのです。
ぜひ今度使ってみてください。
注意事項
当記事ではドメイン駆動設計で登場する「値オブジェクト」に関して説明していますが、値オブジェクトの厳密な定義や特質すべてには触れず、概要のみを述べる記事になっています。ご了承ください。