はじめに
この記事は、以下の記事の続きです。
今回はエンティティの作成について説明します。
開発環境
開発環境は以下の通りです。
- 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が同じなので、同じユーザー(メールアドレスが変わっただけ)
ユーザーエンティティの作成
それでは、ユーザーエンティティを作成します。
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) インターフェースの定義について説明します。リポジトリは、エンティティの永続化を抽象化し、ドメイン層をインフラ層から独立させる重要なパターンです。