LoginSignup
5
2

More than 1 year has passed since last update.

TypeScriptでValue Objectを試す

Posted at

概要

表題の通り、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あれば随時アップデートしていきます。

参考資料

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2