Value Objectで空欄をどう表す?
値が存在しないなら生成しない
画面でまだ入力されていない空欄と、業務上有効な値は分けて扱います。入力途中の空欄は画面側で持ち、Value Objectには変換しません。
Value Objectは、業務上意味のある正しい値だけを表します。そのため、中身が空文字やnullのValue Objectは基本的に作りません。
値が単に設定されていない場合は、Entityの該当プロパティをnullにできます。「制限なし」のように値がないこと自体が業務上の状態なら、その状態をValue Objectで明示します。
入力途中の空欄
例えば、募集を作る画面の「開催場所」は、入力前には空欄です。この状態は画面へ入力した値として持ちます。
// 画面へ入力した値を持つ型
type RecruitmentFormInput = {
location: string; // 入力前は空文字
};入力途中では、まだ有効な開催場所とは限らないため、Value Objectを作りません。
必須項目が空欄の場合
開催場所が必須なら、登録するときに空欄をエラーにします。有効な値を確認できた場合だけValue Objectへ変換します。
const location = input.location.trim();
// 必須項目が空欄なら、Value Objectを作らない
if (location === "") {
throw new Error("開催場所を入力してください");
}
// 有効な値だけをValue Objectへ変換する
const recruitmentLocation =
new RecruitmentLocationValueObject(location);任意項目に値がない場合
例えば、フットサルNOWの募集には、任意で「年齢制限」を設定できるとします。
設定されていないことを表す
年齢制限を単なる任意項目として扱うなら、年齢制限がある場合だけValue Objectを作ります。設定されていない場合は、Entityが持つ年齢制限をnullにします。
type RecruitmentProps = {
// 年齢制限がない場合はnull
ageRestriction: AgeRangeValueObject | null;
};
// 18歳から39歳まで参加できる募集
const restrictedRecruitment: RecruitmentProps = {
ageRestriction: new AgeRangeValueObject({
minimumAge: 18,
maximumAge: 39,
}),
};
// 年齢制限がない募集
const unrestrictedRecruitment: RecruitmentProps = {
ageRestriction: null,
};この例では、Value Objectがあれば「年齢制限あり」、nullなら「年齢制限なし」です。
「年齢制限なし」を状態として表す
「年齢制限なし」も業務上の状態として明確に扱いたい場合は、「制限なし」と「範囲指定」を表せるValue Objectにします。
// 「制限なし」と「範囲指定」を表す値
type AgeRestrictionValue =
| { type: "none" }
| {
type: "range";
minimumAge: number;
maximumAge: number;
};
class AgeRestrictionValueObject {
constructor(readonly value: AgeRestrictionValue) {}
}
const noRestriction = new AgeRestrictionValueObject({
type: "none",
});
const restricted = new AgeRestrictionValueObject({
type: "range",
minimumAge: 18,
maximumAge: 39,
});この場合、Value Objectは常に存在し、その中に「制限なし」または「範囲指定」という有効な状態があります。最小年齢と最大年齢へ直接nullを入れる必要はありません。
DBで数字を管理する場合
DBでは、最小年齢と最大年齢を数値のカラムとして持ち、年齢制限がない場合だけNULLで保存できます。
age_restriction_type | minimum_age | maximum_age
none | NULL | NULL
range | 18 | 39Repositoryは、DBのNULLを、ドメインモデルの「年齢制限なし」へ変換します。DBでNULLを使っていても、Value Objectの年齢へnullを入れる必要はありません。
type AgeRestrictionRow =
| {
type: "none";
minimumAge: null;
maximumAge: null;
}
| {
type: "range";
minimumAge: number;
maximumAge: number;
};
// RepositoryでDBのデータをValue Objectへ変換する
function toAgeRestriction(row: AgeRestrictionRow) {
if (row.type === "none") {
return new AgeRestrictionValueObject({ type: "none" });
}
return new AgeRestrictionValueObject({
type: "range",
minimumAge: row.minimumAge,
maximumAge: row.maximumAge,
});
}0や999などの特別な数字で「制限なし」を表すと、本当の年齢と区別できなくなるため避けます。