概要
表題の通り、TypeScriptでValue Objectの実装を試したのでメモします。
Value Objectとは?
本記事でいうValue Objectは、「DDD」に登場するValue Object(値オブジェクト)のことです。
特にここではValue Objectの説明には触れません。
参考
https://little-hands.hatenablog.com/entry/2018/12/09/entity-value-object
実装
GOAL
下記の2種類のValue Objectを作成してみます。
- Email: Eメールアドレス (正しいメールアドレスかどうかバリデーション)
- Password: パスワード (8文字以上かどうかバリデーション)
const email = new Email("xxxx@example.com") // 成功
const email = Email.instantiate("xxxx@example.com") // 成功
const email = new Email("xxxxxxx") // エラー
const email = Email.instantiate("xxxxxxxxxx") // null
Value Objectの抽象クラス
abstract class ValueObject<T extends string, K> {
readonly valueObjectType: T | undefined
constructor(
readonly value: K
) {
if (!this.isValid()) {
throw new Error()
}
}
equals(v: ValueObject<T, K>): boolean {
return this.value == v.value
}
protected abstract isValid(): boolean
}
Value Objectのabstract classを用意しました。
TypeScriptはStructural Typingな言語なため、クラスの構造が同じの場合は、同じクラスとしてみなされてしまいます。
class Email {
constructor(readonly value: string) {}
}
class Password {
constructor(readonly value: string) {}
}
例えば上記の実装をすると、
const email: Email = new Password("maaaa") 👉 利用できてしまう
こんな実装をしても、エラーで警告されません。
そこでValue Objectの抽象クラスに、valueObjectTypeというプロパティを必ず1つ持たせ、区別します。
また、等価判定のためのequalsメソッドを生やし、abstract methodとしてisValidメソッドの実装を明示しています。
isValidメソッドをコンストラクタ内部で呼び、isValidに引っかかった場合はerrorをthrowしています。
これにより、Value Objectの概念の肝である、「生成された瞬間に完成していること」を満たせます。
Email, Passwordのクラスを定義
上記のabstract classを用いてEmailとPasswordのValue Objectを定義します。
class Email extends ValueObject<"Email",string> {
constructor(readonly value: string) {
super(value)
}
protected isValid(): boolean {
const regexp = /^[a-zA-Z0-9_+-]+(\.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;
return regexp.test(this.value)
}
}
class Password extends ValueObject<"Password",string> {
constructor(readonly value: string) {
super(value)
}
protected isValid(): boolean {
console.log(this.value)
return this.value.length >= 8
}
}
(パスワードのバリデーションはとりあえず文字数カウントだけチェックしています。)
こうすることにより、下記のような利用ができない状態で、各Value Objectをクラスとして扱えます。
const email: Email = new Password("maaaa") 👉 エラーになる
バリデーションエラーはnullにしたい
上記でValue Objectとしての要件は満たせました。
しかし、バリデーションエラーに引っ掛かるとエラーがスローされるので、扱いが少々面倒です。
try {
const email = new Email("hoge")
} catch(e) {
// TODO: handling
}
クラスの生成箇所でのエラーキャッチがOKであれば、このままでOKです。
ちょっと不便に感じる場合は、バリデーションに失敗したらnullが返ってくるような構造にします。
下記のようなメソッドを各クラスに仕込みます。
static instantiate(value: string): Email | null {
try {
const newInstance = new Email(value)
return newInstance
} catch(e) {
return null
}
}
バリデーションエラーのスローはコンストラクタ内で行われます。
インスタンス生成処理をstaticメソッドで担い、メソッド内部でエラーをキャッチします。
エラーだった場合はnullを返します。
利用方法は下記の通りです。
const email = Email.instantiate("example@com")
const email = Email.instantiate("example") // null
全体コードです。
class Email extends ValueObject<"Email",string> {
constructor(readonly value: string) {
super(value)
}
static instantiate(value: string): Email | null {
try {
const newInstance = new Email(value)
return newInstance
} catch(e) {
return null
}
}
protected isValid(): boolean {
const regexp = /^[a-zA-Z0-9_+-]+(\.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;
return regexp.test(this.value)
}
}
class Password extends ValueObject<"Password",string> {
constructor(readonly value: string) {
super(value)
}
static instantiate(value: string): Password | null {
try {
const newInstance = new Password(value)
return newInstance
} catch(e) {
return null
}
}
protected isValid(): boolean {
console.log(this.value)
return this.value.length >= 8
}
}
以上です。もう少し良いTipsあれば随時アップデートしていきます。