不変条件からドメインオブジェクトのガード手法を考える

## 概要

DDD界隈で、ドメインオブジェクト基本原則の一つに、ドメインオブジェクトは常に正しいインスタンスのみを存在しなければならないと言われている。

その目標の実現には、契約によるプログラミングをベースに、事前条件、事後条件と不変条件を指定して、プロパティに不適切な値が設定されないようにガードする手法が多数採用されている。

ガードを実現する手法として、不正な値に対して例外を発生させる手法と不正な値をシステムが勝手に正常値に変換する手法があった。
ここでは、ここ2つの手法を少し詳細的に考える。

## ガードを実現する手法

以下では、簡単な具体例を挙げる。

人を表現するPersonオブジェクトがあるとする。
Personオブジェクトの不変条件は以下である。
- 氏名は文字列またはnullである。

```ts
class Person {
  private name: string | null

  constructor(name: string | null) {
    if (typeof name !== "string" && name !== null) {
      throw new IllegalPreConditionException("事前条件: 引数nameが不正")
    }

    this.name = name

    if (typeof name !== "string" && name !== null) {
      throw new IllegalInvariantConditionException(
        "不変条件: プロパティnameが不正"
      )
    }
  }
}
```


Personクラスのコンストラクターに文字列とnull以外のものが来たら、例外が発生し、初期化処理が止まるので、不正な値をもつPersonインスタンスは生成されない。


Personオブジェクトの制約を少し増やす。

- 氏名は文字列である。
- 氏名はnullになってはいけない。
- 氏名が文字列の場合、1文字以上でなければいけない（new）

氏名が空文字の場合に例外を発生させる。

```ts
class Person {
  private name: string | null

  constructor(name: string | null) {
    if (typeof name !== "string" && name !== null) {
      throw new IllegalPreConditionException("事前条件: 引数nameの型が不正")
    }

    if (name === "") {
      throw new IllegalPreConditionException("事前条件: 引数nameの長さが不正")
    }

    this.name = name

    if (typeof name !== "string" && name !== null) {
      throw new IllegalInvariantConditionException(
        "不変条件: 引数nameの型が不正"
      )
    }

    if (name === "") {
      throw new IllegalInvariantConditionException(
        "不変条件: 引数nameの長さが不正"
      )
    }
  }
}
```
Personクラスのコンストラクターに空文字列が来たら、例外が発生し、初期化処理が止まるので、不正な値をもつPersonインスタンスは生成されない。


氏名が空文字の場合にnullに変えるパターン。

```ts
class Person {
  private name: string | null

  constructor(name: string | null) {
    if (typeof name !== "string" && name !== null) {
      name = null
    }

    if (name === "") {
      name = null
    }

    this.name = name

    if (typeof name !== "string" && name !== null) {
      throw new IllegalInvariantConditionException(
        "不変条件: 引数nameの型が不正"
      )
    }

    if (name === "") {
      throw new IllegalInvariantConditionException(
        "不変条件: 引数nameの長さが不正"
      )
    }
  }
}
```

Personクラスのコンストラクターに空文字列が来たら、nullとしてプロパティにセットするので、不正な値をもつPersonインスタンスは生成されない。


Personオブジェクトの制約がさらに増える。

- 氏名は文字列である。
- 氏名はnullになってはいけない。
- 氏名が文字列の場合、1文字以上でなければいけない
- 氏名は英数字と「_」のみの文字列を許容する

氏名に英数字と「_」以外の文字列が来たら、例外を発生させて、不正な値をもつPersonインスタンスは生成されない。

```ts
class Person {
  private name: string | null

  constructor(name: string | null) {
    if (typeof name !== "string" && name !== null) {
      throw new IllegalPreConditionException("事前条件: 引数nameの型が不正")
    }

    if (name === "") {
      throw new IllegalPreConditionException("事前条件: 引数nameの長さが不正")
    }

    if (!/^\w+$/.test(name)) {
      throw new IllegalPreConditionException("事前条件: 引数nameに英数字とハイフン以外の文字列がある")
    }

    this.name = name

    if (typeof this.name !== "string" && name !== null) {
      throw new IllegalInvariantConditionException(
        "不変条件: 引数nameの型が不正"
      )
    }

    if (this.name === "") {
      throw new IllegalInvariantConditionException(
        "不変条件: 引数nameの長さが不正"
      )
    }

    if (!/^\w+$/.test(this.name)) {
      throw new IllegalPreConditionException("不変条件: 引数nameに英数字とハイフン以外の文字列がある")
    }
  }
}
```

氏名に英数字と「_」以外の文字列が来たら、英数字と「_」以外の部分を削除して、不正な値をもつPersonインスタンスは生成されない。

```ts
class Person {
  private name: string | null

  constructor(name: string | null) {
    if (typeof name !== "string" && name !== null) {
      name = null
    }

    if (name === "") {
      name = null
    }

    name.replaceAll(/[^\w]+/g, "")

    this.name = name

    if (typeof this.name !== "string" && name !== null) {
      throw new IllegalInvariantConditionException(
        "不変条件: 引数nameの型が不正"
      )
    }

    if (this.name === "") {
      throw new IllegalInvariantConditionException(
        "不変条件: 引数nameの長さが不正"
      )
    }

    if (!/^\w+$/.test(this.name)) {
      throw new IllegalPreConditionException("不変条件: 引数nameに英数字とハイフン以外の文字列がある")
    }
  }
}
```

## 思考

上記の例からみると、
不正な値に対して例外を発生させる手法と不正な値をシステムが勝手に正常値に変換する手法どちらも不変条件を担保することができた。

ただし、以下の観点で両者の違いもある。

まず、仕様によって、そもそもシステムが勝手に入力値を変更することができないケースとシステムが自動で入力値を変更しなければいけないケースがある。システムが勝手に入力値を変更することができない場合、不正値が発生した場合に、エラーメッセージを画面に表示させて、ユーザーに入力値を修正してもらったの方がもっと自然だと思う。

次に、「不変条件を満たすための変換ロジックは果たしてドメインロジックなのか」という疑問がある。webアプリケーションの場合、HTTPリクエストにあったユーザー入力のバリデーションをプレゼンテーション層（コントローラー）で済ませる流儀があるし、DB保存のために氏名をnullに変更することも本当はインフラストラクチャー層でカバーする考え方もあるはず。


そして、オブジェクトの制約が増えるに連れて、制約に満たすための変換ロジックも複雑になり、ドメインオブジェクトの制約が暗黙になる可能性ある。

最後の例を振り返ると、

```ts
    if (typeof name !== "string" && name !== null) {
      name = null
    }

    if (name === "") {
      name = null
    }

    name.replaceAll(/[^\w]+/g, "")
```

この処理からPersonオブジェクトの制約を直感的に受け取ることがやや困難に感じた。宣言的プログラミング（あるべき状態を明示的に宣言するスタイル）を好む開発現場ならば、明示的に例外を発生させた方がコードベース全体の見通しを高めるだろう。