💴

【ドメイン駆動設計(DDD)】値オブジェクトとエンティティ

に公開

はじめに

私はエンジニアとして、保守性の高いソースコード、きれいなアプリケーションアーキテクチャを目指しています。
その実現のために「ドメイン駆動設計をはじめよう」という本を読みドメイン駆動設計(以下、DDD)について学んでいます。せっかくなのでこれを読んで得た知識をアウトプットすることで、頭の中を整理するとともに、読者の皆さんの参考になりますと幸いです。
Amazonでのご購入はこちら

値オブジェクトとは

値オブジェクトはアプリケーション固有のデータ型です。例えば以下のように電話番号をstringで持つとします。

class User {
  name: string;
  phoneNumber: string;

  constructor(name: string, phoneNumber: string) {
    this.name = name;
    this.phoneNumber = phoneNumber;
  }
}

このときphoneNumberは有効な電話番号の桁数である必要や、080など実在する電話番号の数字で始まる必要があります。しかしstringだとその保証はなく、バリデーションが必要な場合にあちこちで検査ロジックが散らばります。また所見で中身がどんな形式なのか分かりません。これを値オブジェクトを使って修正してみます。

class PhoneNumber {
  readonly value: string;

  constructor(value: string) {
    if (
      // 電話番号の形式チェックロジック
    ) {
      throw new Error(`Invalid phone number: ${value}`);
    }
    this.value = value;
  }
}

class User {
  readonly name: string;
  readonly phoneNumber: PhoneNumber;

  constructor(name: string, phoneNumber: PhoneNumber) {
    this.name = name;
    this.phoneNumber = phoneNumber;
  }
}

phoneNumberの中身はPhoneNumberを見ればすぐわかりますし、必ず電話番号の要件を満たした値が入っていることが保証されるようになりました。また電話番号の妥当性検査はPhoneNumberに一元管理してあるので、同じようなコードが散らばることもありません。

値オブジェクトの性質

値オブジェクトはidではなくプロパティの組み合わせによって区別されます。

この例ではプロパティが1つですが、プロパティの値が同じであれば同じもの、異なる場合は別物です。
中身が変更できてしまうと値オブジェクトの性質から外れてしまうため、イミュータブル(不変)なオブジェクトとして実装します。

値オブジェクトの性質から外れてしまうとは具体的にどういうこと?

いくつかありますが大きく以下の2点が挙げられます。

中身が保証できない

先程の電話番号の例ですと、せっかく電話番号の形式であることが保証されているのに、中身を変更できてしまうと、不正な形式の値を入れることができるので値オブジェクトとしての役割が果たせていません。

class PhoneNumber {
  value: string;

  constructor(value: string) {
    if (
      // 電話番号の形式チェックロジック
      false
    ) {
      throw new Error(`Invalid phone number: ${value}`);
    }
    this.value = value;
  }
}
const phoneNumber = new PhoneNumber("09012345678");
phoneNumber.value = "aaaaa"; // ←不正な形式の文字列が混入

副作用が起こる

同じ値オブジェクトを複数箇所から参照している場合に、中身が同じという前提で参照しているため、どこかで値を変えてしまうと、それを参照している他の箇所でバグが起きる可能性があります。

class Price {
  value: number;

  constructor(value: number) {
    this.value = value;
  }
}
const price = new Price(2000);
const product_A = new Product(price);
const product_B = new Product(price);

product_A.price.value = 1000; // ←商品Aだけ価格変更のつもり
console.log(product_A.price.value); // 1000
console.log(product_B.price.value); // 1000 ← 副作用で商品Bの価格も変わった

このようにミュータブルにしてしまうと値オブジェクトとは呼べないですし、そもそも最初からプリミティブ型を使えばいいという話になってしまいます。

値を変更するには?

値が違うものは別物なので、別のインスタンスを返すようにします。
以下の例ではPrice(値オブジェクト)に別のインスタンスを返すminusメソッドを追加し、先程は省きましたがProductクラスに割引メソッドを追加しています。価格は割引メソッドを使用することで変更されます。ちなみにProductはプロパティの中身が変わってますが、こちらはエンティティと呼ばれるものです。エンティティについては後述します。

class Product {
  private price: Price;
  constructor(price: Price) {
    this.price = price;
  }

  getPrice(): number {
    return this.price.value;
  }

  // 割引メソッド
  discount(): void {
    this.price = this.price.minus(1000);
  }
}

class Price {
  readonly value: number;

  constructor(value: number) {
    this.value = value;
  }

  minus(amount: number): Price {
    // 別のインスタンスを返す
    return new Price(this.value - amount);
  }
}

const price = new Price(2000);
const product_A = new Product(price);
const product_B = new Product(price);

product_A.discount(); // ←商品Aだけ価格変更
console.log(product_A.getPrice()); // 1000
console.log(product_B.getPrice()); // 2000

エンティティとは

値オブジェクトの例で示したProductがエンティティです。値オブジェクトは各プロパティの値で識別しましたが、エンティティはidによって識別します。インスタンスごとに一意であり、名前などが同じでもidが違えば別物となります。またidは、その個体が存在する限り変更してはいけません。
コードで表現してみます。

class Product {
  readonly id: ProductId;
  private price: Price;
  constructor(price: Price) {
    this.id = // id生成ロジック
    this.price = price;
  }
}

id自体は値オブジェクトで表し、中身はエンティティやビジネスロジックにはよりますが、UUIDなどインスタンスを一意に識別できるものを入れます。

値オブジェクトとエンティティの違い


人の場合、2人の人がいて同姓同名だとしても違う人です。しかし金額という概念は商品が違ったとしても同じ数字であれば同じものです。前者がエンティティ、後者が値オブジェクトです。

Discussion