1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Node.js】リファクタリングを通して学ぶドメイン駆動設計 Part2: エンティティの作成

Last updated at Posted at 2025-12-02

はじめに

この記事は、以下の記事の続きです。

今回はエンティティの作成について説明します。

開発環境

開発環境は以下の通りです。

  • Windows11
  • Docker Engine 27.0.3
  • Docker Compose 2
  • PostgreSQL 18.1
  • Node.js 24.11.0
  • npm 11.6.2
  • TypeScript 5.9.3
  • Express 5.1.0
  • Prisma 6.18.0
  • Zod 4.1.12

エンティティ(Entity) とは

エンティティ(Entity)は、一意の識別子(ID)を持つオブジェクトです。IDが同じであれば、他の属性が変わっても同じエンティティとみなします。

特徴 説明
一意の識別子を持つ データベースのIDなど、他と区別できる識別子を持ちます。 ユーザーID、注文番号など
可変性(Mutability) 属性の値を変更できます。ただし、IDは変更できません メールアドレスを変更しても、同じユーザーとして扱われる
同一性による比較 IDが同じであれば、他の属性が異なっていても同じエンティティとみなされます。 user1.id === user2.id なら同じユーザー
ライフサイクルを持つ 作成、更新、削除といったライフサイクルを持ちます。 ユーザー登録 → ユーザー情報更新 → ユーザー削除
ビジネスロジックを持つ そのエンティティに関するビジネスルールをメソッドとして実装します。 メールアドレス変更時のルール、パスワード変更時のルールなど

例えば、ユーザーがメールアドレスを変更しても、IDが同じであれば同じユーザーです。

const user1 = { id: 1, email: "old@example.com", name: "John" };
const user2 = { id: 1, email: "new@example.com", name: "John" };
// IDが同じなので、同じユーザー(メールアドレスが変わっただけ)

ユーザーエンティティの作成

それでは、ユーザーエンティティを作成します。

domain/entities/User.ts
import { Email } from "../value-objects/Email";
import { UserId } from "../value-objects/UserId";
import { UserName } from "../value-objects/UserName";

export class User {
  private constructor(
    private readonly id: UserId | null,
    private email: Email,
    private name: UserName
  ) {}

  /** ファクトリーメソッド: 新規ユーザー作成(IDなし) */
  static create(email: Email, name: UserName): User {
    return new User(null, email, name);
  }

  /** ファクトリーメソッド: 既存ユーザーの再構築(IDあり) */
  static reconstruct(id: UserId, email: Email, name: UserName): User {
    return new User(id, email, name);
  }

  getId(): UserId | null {
    return this.id;
  }

  getEmail(): Email {
    return this.email;
  }

  getName(): UserName {
    return this.name;
  }

  hasId(): boolean {
    return this.id !== null;
  }

  /** ビジネスロジック: メールアドレス変更 */
  changeEmail(newEmail: Email): void {
    if (this.email.equals(newEmail)) {
      throw new Error("New email is the same as current email");
    }
    this.email = newEmail;
  }

  /** ビジネスロジック: 名前変更 */
  changeName(newName: UserName): void {
    if (this.name.equals(newName)) {
      throw new Error("New name is the same as current name");
    }
    this.name = newName;
  }

  /** プリミティブ型への変換 */
  toObject() {
    if (!this.id) {
      throw new Error("Cannot convert user without ID to object");
    }
    return {
      id: this.id.getValue(),
      email: this.email.getValue(),
      name: this.name.getValue(),
    };
  }
}

ファクトリーメソッドとは

ファクトリーメソッド(Factory Method)は、オブジェクトを生成するための専用メソッドです。コンストラクタを直接呼び出すのではなく、静的メソッドを経由してオブジェクトを生成します。

通常、JavaScriptでは new キーワードでオブジェクトを生成します(コンストラクタを直接呼び出します)。

const user = new User(id, email, name); // 通常の方法

しかし、今回は以下の理由でファクトリーメソッドを使います。

理由1. 生成方法が複数ある

ユーザーの生成には2つのパターンがあります。

  • 新規作成: IDがまだない状態(データベースに保存する前)
  • 既存データの再構築: IDがある状態(データベースから取得した後)
// 新規作成用(IDはまだない)
const newUser = User.create(email, name);

// データベースから取得したデータの再構築用(IDあり)
const existingUser = User.reconstruct(userId, email, name);

理由2. コンストラクタを隠蔽できる

コンストラクタを private にすることで、外部から直接 new User() できないようにします。これにより、必ず適切なファクトリーメソッドを経由してオブジェクトを生成することを強制できます。

// これはできない(コンストラクタがprivate)
const user = new User(null, email, name); // エラー

// これが正しい使い方
const user = User.create(email, name); // OK

理由3. 意図が明確になる

メソッド名から、何をしようとしているのかが明確になります。

User.create(email, name);              // 新しいユーザーを作る
User.reconstruct(id, email, name);     // 既存のユーザーデータを復元する

単に new User() と書くだけでは、新規作成なのか既存データの復元なのか、コードを読んだだけではわかりません。

エンティティのポイント

ポイント1. ビジネスロジックをエンティティに集約

エンティティを使うと、ビジネスルールを一箇所にまとめられます。

// エンティティにルールを集約
user.changeEmail(newEmail); // この中で「同じメールアドレスか」をチェック

// User.ts 内部
changeEmail(newEmail: Email): void {
  if (this.email.equals(newEmail)) {
    throw new Error('New email is the same as current email');
  }
  this.email = newEmail;
}

これにより、メール変更のルールを変更する場合、Userエンティティだけを修正すればよくなります。

メリット:

  • ビジネスルールが一箇所にまとまる
  • ルール変更時の修正箇所が明確
  • 同じルールを複数の場所で書く必要がない

ポイント2. 不正な状態を作れないようにする

エンティティを使うと、不正な操作を防げます。

// 決められたメソッド経由でしか変更できない
const user = User.reconstruct(userId, email, name);
user.changeEmail(newEmail); // このメソッド内でバリデーションが行われる

// 直接プロパティにアクセスできない
user.email = newEmail; // エラー: emailはprivate

メリット:

  • 常に正しい状態のオブジェクトであることが保証される
  • バリデーション漏れによるバグを防げる
  • コード全体の信頼性が向上する

ポイント3. 変更履歴やイベントを記録しやすい

エンティティのメソッドを経由することで、変更の記録が容易になります。

changeEmail(newEmail: Email): void {
  if (this.email.equals(newEmail)) {
    throw new Error('New email is the same as current email');
  }
  
  // ここでイベントを記録できる
  console.log(`Email changed from ${this.email.getValue()} to ${newEmail.getValue()}`);
  
  // または、ドメインイベントを発行することもできる
  // this.addDomainEvent(new UserEmailChangedEvent(this.id, newEmail));
  
  this.email = newEmail;
}

メリット:

  • 監査ログが取りやすい
  • イベント駆動アーキテクチャへの拡張が容易
  • デバッグがしやすい

エンティティの使用例

これらのエンティティを実際に使うと、以下のようになります。

// 新規ユーザーの作成(IDなし)
const email = new Email("test@example.com");
const name = new UserName("John Doe");
const newUser = User.create(email, name);

console.log(newUser.hasId()); // false(まだIDが割り当てられていない)

// データベースから取得したユーザーの再構築(IDあり)
const userId = new UserId(1);
const existingUser = User.reconstruct(userId, email, name);

console.log(existingUser.hasId()); // true(IDが割り当てられている)
console.log(existingUser.getId()?.getValue()); // 1

// メールアドレスの変更
const newEmail = new Email("newemail@example.com");
existingUser.changeEmail(newEmail);

console.log(existingUser.getEmail().getValue()); // "newemail@example.com"

// 同じメールアドレスへの変更は失敗する
try {
  existingUser.changeEmail(newEmail);
} catch (error) {
  console.log(error.message); // "New email is the same as current email"
}

元のコードとの比較

Before(元のコード)

// コントローラーに全てが詰まっている
app.put("/users/:id", async (req, res) => {
  try {
    const { id } = req.params;
    const { email, name } = req.body;

    const user = await prisma.user.update({
      where: { id: Number(id) },
      data: {
        ...(email && { email }),
        ...(name && { name }),
      },
    });

    res.json(user);
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: "Failed to update user" });
  }
});

問題点:

  • ビジネスルールがない(同じメールアドレスへの変更もチェックされない)
  • ドメインの概念が不明確
  • テストしにくい

After(エンティティ使用)

// ユースケース層(次回解説)
const userId = new UserId(input.id);
const user = await this.userRepository.findById(userId);

if (input.email) {
  const newEmail = new Email(input.email);
  user.changeEmail(newEmail); // ビジネスルールがエンティティに集約
}

if (input.name) {
  const newName = new UserName(input.name);
  user.changeName(newName);
}

await this.userRepository.update(user);

改善点:

  • ビジネスルールがエンティティに明確に書かれている
  • ドメインの概念が明確(Userとは何か、どんな操作ができるか)
  • テストしやすい(エンティティ単体でテスト可能)

まとめ

今回は、DDDにおけるエンティティ(Entity) について説明しました。

  • ポイント
    • エンティティは一意の識別子(ID)を持つドメインオブジェクト
    • ファクトリーメソッドで生成方法を明確にする
    • ビジネスロジックをエンティティに集約する
    • 不正な状態を作れないようにする
  • メリット
    • ビジネスルールが一箇所にまとまる
    • 常に正しい状態のオブジェクトであることが保証される
    • 変更履歴やイベントの記録が容易
    • テストがしやすい

次回は、リポジトリ(Repository) インターフェースの定義について説明します。リポジトリは、エンティティの永続化を抽象化し、ドメイン層をインフラ層から独立させる重要なパターンです。

参考

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?