Value Objectについて

# はじめに

- DDDについて学習していく中でDomain層の構成要素の一つであるValue Objectについてまとめました
- コードサンプルは普段よく使うTypeScriptで実装しております
- 弊社（株式会社 LIFULL）では、[HOME'Sという不動産ポータルサイト](https://www.homes.co.jp/)を運営しているため、コードの例は不動産関連です

# Value Objectについて

Value Objectは、値オブジェクトとも言われており、値に注目したオブジェクトです。
値に関するドメイン知識を集約し、生成したオブジェクトが常にドメインルールに則った値を持つように実装します。
例えば、[不動産物件から駅までの徒歩分数の表記ルールは徒歩1分＝80m](https://www.homes.co.jp/cont/rent/rent_00226/)と決められています。
このルールを踏まえ、駅までの徒歩分数をValue Objectで表してみます。

```typescript
export class WalkMinutes {
  /**
   * 80m=1分換算
   */
  static readonly DISTANCE_PER_MIN = 80;

  /**
   * @param value - 駅からの徒歩での時間の値
   */
  private constructor(private readonly value: number) {}

  /**
   * @param distance - 駅からの徒歩での距離[m]
   */
  static create(distance: number | undefined): WalkMinutes | undefined {
    if (!distance || distance <= 0) {
      return undefined;
    }
    const minutes = Math.ceil(distance / WalkMinutes.DISTANCE_PER_MIN);

    return new WalkMinutes(minutes);
  }

  /**
   * Numberに変換します
   */
  toNumber(): number {
    return this.value;
  }

  /**
   * Stringに変換します
   */
  toString(): string {
    return `${this.value}分`;
  }
}
```

上の例では、値が持つドメイン知識をcreateメソッド内で表現してドメイン知識を集約しています。

```typescript
  static create(distance: number | undefined): WalkMinutes | undefined {
    if (!distance || distance <= 0) {  // 徒歩距離の値が存在しない or または0以下のケースはundefined
      return undefined;
    }
    const minutes = Math.ceil(distance / WalkMinutes.DISTANCE_PER_MIN);  // 徒歩距離を80で割った値を徒歩分数とする

    return new WalkMinutes(minutes);  // staticメソッドでオブジェクトを生成する
  }
```

また、privateなコンストラクタを定義し、オブジェクトの生成をstaticメソッドでのみ行うことで、オブジェクト生成に関するルールを強制することができます。
こうすることで、いつどこでオブジェクトが生成されてもドメインルールが守られている保証があるため、安全に利用できるようになります。


オブジェクトの値を全てprivateな変数にすることも重要です。
privateな変数にしておくことで、以下のような不正な操作を防ぐことができます。

```typescript
const walkMinutes = WalkMinutes.create(distance);
walkMinutes.value += 10;
```

また、値を取得する際にgetterを経由することで、値の単位などを統一することも可能です。
上記ではtoStringメソッドで値を取得する際に、単位として分を付与しています。

```typescript
  /**
   * Stringに変換します
   */
  toString(): string {
    return `${this.value}分`;
  }
```

# Value Objectの重要な性質

Value Objectは以下のような性質を持っていることが望まれます

- オブジェクト生成時に保持する値が不変（immutable）
- 生成されるオブジェクトに一意性がない

#### オブジェクト生成時に保持する値が不変（immutable）

例えば、以下のようなValueObjectは生成するタイミングで値が変わるため、不変性が保証されません。

```typescript
/**
 * Age - 年齢
 */
export class Age {
  /**
   * @param value - 年齢
   */
  private constructor(private readonly value: number) {}

  /**
   * インスタンス化
   * @param value - 生年月日 8桁 YYYYMMDD
   */
  static create(birthday: number | undefined): Age | undefined {
    if (!value) {
      return undefined;
    }
    const strValue = `${value}`;
    if (!/^\d{8}$/.test(strValue)) {
      throw new Error('Age値オブジェクト');
    }
    const period = dayjs(strValue, 'YYYYMMDD', 'ja');  // dayjsは外部ライブラリですがサンプルのため採用
    const now = dayjs();
    const diffYear = now.diff(buildPeriod, 'year');

    return new Age(diffYear);
  }

  /**
   * Numberに変換します
   */
  toNumber(): number {
    return this.value;
  }

  /**
   * Stringに変換します
   */
  toString(): string {
    return `${this.value}歳`;
  }
}
```

オブジェクトを生成する際の日時を用いて年齢を計算しているため、最終的に生成されるオブジェクトの値が可変になっております。
そのため、処理の実行中に同じ値を使って複数箇所でオブジェクトを生成する場合などに、安全性が保証されない結果となってしまします。
また、同じ理由からsetterを実装することも避けるべきです。

#### 生成されるオブジェクトに一意性がない

オブジェクトが一意性を持つと、Value Objectの利点である交換可能性が損なわれます。
例えば、以下のようなお金と製造IDを持つオブジェクトを実装してしまうと、製造IDを常に気にせねばならず、非常に扱いづらいオブジェクトになってしまいます。

```typescript
/**
 * Money - お金
 */
export class Money {
  /**
   * @param value - 金額
   * @param id - 製造番号
   */
  private constructor(private readonly value: number, private readonly id: number) {}

  /**
   * インスタンス化
   * @param value - 金額
   */
  static create(value: number | undefined, id: number): Money | undefined {
    if (!value || value <= 0) {
      return undefined;
    }

    return new Money(value, id);
  }
}
```


このような一意性を持つIDなどを含んだオブジェクトは、Domain Entityと呼ばれるものになるため、Value Objectとは別の概念で扱うことになります。


# まとめ

現在、APIをClean Architectureで実装しておりますが、Value Objectを採用することで、ドメイン知識が集約されて値の取り回しがかなり楽になる印象があります。
ただし、Value Objectだけでは吸収しきれないビジネスロジックや複数のValue Objectが絡むものなどは、どう実装していくかが見えてないため、引き続きDDDを勉強していきEntity層に関する知識を深めていきたいと思います。