【やさしいDDD入門 第1章】値オブジェクトとは

ドメイン駆動設計のイメージ画像 設計

値オブジェクトとは

ざっくり言うと、現実世界の物をクラスで表現したもの。

エンティティと似ているが、その違いについてはエンティティの章で解説する。

例:ユーザー名

例えば、ユーザー名について考えてみる。

特に何も考えなければ、ユーザー名は以下のように定義して扱えば正常に動作する。

const userName = "yamada taro";

しかし、DDDではこれを「UserName」クラスとして定義してみてはどうか?という考え方である。
なぜならば現実世界でユーザー名はただの文字列ではないからだ。

実際のシステムでは、ユーザー名に対して何らかのルールが設けられていることが多い。

  1. 姓と名に分けて取り出すことがある
  2. 最低でも2文字以上であること(姓と名で1文字ずつ必須)

これをそのまま実装した場合と、値エンティティとして実装した場合で比較してみる。

そのまま実装した場合

名だけを取り出す時

let userName = "yamada taro";

const firstName = userName.split(" ")[1];
console.log(firstName); // 「taro」が出力

ユーザー名を設定する時

let userName: string;

const inputFirstName = "a"; // 名の入力値
const inputLastName = "b"; // 姓の入力値

if (inputFirstName.length >= 1 && inputLastName.length >= 1) {
  userName = `${inputFirstName} ${inputLastName}`;
  console.log(`ユーザー名が設定されました: ${userName}`);
} else {
  console.error("2文字以上ではないためエラー");
}

値オブジェクトとして実装した場合

まずはUserNameクラスを定義する。

class UserName {
  firstName: string;
  lastName: string;

  constructor(firstName: string, lastName: string) {
    if (firstName.length >= 1 && lastName.length >= 1) {
      this.firstName = firstName;
      this.lastName = lastName;
    } else {
      throw new Error("2文字以上ではないためエラー");
    }
  }

  // 名を取り出すメソッド
  getFirstName() {
    return this.firstName;
  }
}

名だけを取り出す時

const userName = new UserName("yamada", "taro");
console.log(userName.getFirstName());

ユーザー名を設定する時

const userName = new UserName("", "taro");

値オブジェクトではこれらの実装を行い、加えて以下3つの性質を持たせる。

性質①:不変であること

値オブジェクトは不変であり、中身を変更してはいけない。
姓を変更したい場合を考える。

const userName = new UserName("yamada", "taro");

// NG(こんなメソッドは用意しない)
userName.changeFirstName("tanaka");

// ここでuserNameを使うと「tanaka taro」に変わってる

この性質によって、一度定義した「userName」の中身は知らないところで勝手に書き換えられることなく、
常に「yamada taro」であることが保証される。

性質②:交換可能であること

値の書き換えはしてはいけないが、値の交換はしても良い。
ユーザー名を書き換える場合は次のように実装する。

let userName = new UserName("yamada", "taro");

// OK
userName = new UserName("tanaka", "taro");

性質③:等価性によって比較できること

class UserName {
  // 略

  // 姓と名が両方一致していれば同じものと判定
  equals(userName: UserName) {
    return this.firstName == userName.firstName && this.lastName == userName.lastName;
  }
}

使う時

const userName1 = new UserName("yamada", "taro");
const userName2 = new UserName("tanaka", "taro");

if (userName1.equals(userName2)) {
  // ユーザー名が同じ場合の処理
}

何が良いのか?

ただこのコードを見ただけだと何が良いのか分からないと思う。
ここからは開発者が受けるメリットを説明する。

  • クラスそのものが仕様書になる
  • 不正な値が存在しなくなる
  • 誤った代入ができなくなる
  • ロジックが分散せず、改修に強いコードになる

メリット①:クラスそのものが仕様書になる

例えば以下のようなユーザーIDを受け取って何か処理する関数を作りたいとする。

function doSomething(userId: string) {
  // IDを使って何か処理をする
}

このとき、ユーザーIDに普通の文字列を使っていた場合

  • どんなフォーマットか?
  • 使ってはいけない文字があるのか?
  • 長さはどれくらいか?空文字の場合はあり得るのか?

ということを、プログラムの処理を追いかけて理解する必要がある。

これが値オブジェクトとして定義されていた場合、ユーザーIDクラスを参照すれば
「1文字以下の文字列はありえない」ということが容易に分かる。

class UserId {
  id: string;

  constructor(id: string) {
    if (id.length >= 2) {
      this.id = id;
    } else {
      throw new Error("エラー");
    }
  }
}

メリット②:不正な値が存在しなくなる

上記のようにUserIdクラスを定義しておくと、「1文字以下」のようなありえないケースが存在しなくなる。

// stringでユーザーIDを持つ場合
const userId = "a";  // この時点ではエラーにならない

// UserIdクラスでユーザーIDを持つ場合
const userId = new UserId("a");  // この時点でエラーになる

そのため、「不正な値が混入し、処理しようとしたときに落ちる」ということが発生しにくくなる。

メリット③:誤った代入ができなくなる

// ユーザー名とユーザーIDをstringで持つ場合
const userId = "aaa";
const userName = userId;  // 代入できる

// ユーザー名とユーザーIDをそれぞれ値オブジェクトで持つ場合
const userId = new UserId("aaa");
const userName = userId;  // 型が異なるためエラーになる

メリット④:ロジックが分散せず、改修に強いコードになる

今まではユーザー名が姓と名という前提のもと話を進めてきたが、
新たな仕様として「ミドルネーム」が加わったとする。

値オブジェクトとして定義していなかった場合、
ユーザー名を作成している箇所と使っている箇所を洗い出し、全て修正していく必要がある。

もし値オブジェクトとして定義していたならば、修正箇所はUserNameクラスのみとなる。

まとめ

  • 値オブジェクトとは、ざっくり言うと「現実世界の物をクラスで表現したもの」
  • 性質
    • 不変である
    • 交換可能である
    • 等価比較可能である
  • メリット
    • クラスそのものが仕様書になる
    • 不正な値が存在しなくなる
    • 誤った代入ができなくなる
    • ロジックが分散せず、改修に強いコードになる
タイトルとURLをコピーしました