第41章:Entity入門:同一性(ID)が主役🪪
同じ注文かどうかは「内容」じゃなくて 注文IDで決まる🪪
この章は「VO(値で同じ)」と「Entity(IDで同じ)」を、頭で理解するだけじゃなく コードとテストで体感できるようにするよ〜😊🧡
1) 今日のゴール🎯💡
読み終わったらこうなってる状態がゴールだよ👇✨
- ✅ VOとEntityの違いを、説明できる(しかも迷わない)🗣️
- ✅ Entityは“IDで同一性”が決まると腹落ちしてる🪪
- ✅ TypeScriptで Entityの最小実装が書ける🧩
- ✅ テストで「IDが同じなら同じEntity」を確認できる🧪
2) まず結論!VOとEntityの違いはコレ🌸
Value Object(VO)💎
- ✅ “値が同じなら同じ”
- ✅ **不変(immutable)**で、変更したら「別の値」になる🧊
- 例:Money、Email、Quantity…💴✉️📏
Entity 🪪
- ✅ “IDが同じなら同じ”
- ✅ 値(状態)は変わってもいい(むしろ変わる)🔁
- 例:Order(注文)、Customer(顧客)など☕🧾
3) 例で一発理解☕🧾(カフェ注文)
たとえば「注文」ってさ…👇
- 注文に商品を追加した📌
- 数量を変えた📌
- 支払い済みに進んだ📌
…って変化があるのに、**「同じ注文」って言うよね?**😊 これが Entity的な発想だよ✨ 同じ注文かどうかは「内容」じゃなくて 注文IDで決まる🪪
4) 迷わない!Entity判定の3チェック✅🧠
「これEntity?VO?」って迷ったら、まずこの3つを見てね👇✨
-
時間をまたいでも“同じもの”って言いたい? ⏳
- YES → Entityっぽい🪪
-
更新される前提?(状態が変わる) 🔁
- YES → Entityっぽい🪪
-
同一性の根拠が“ID”で語れる? 🆔
- YES → Entity確定に近い🪪✨
逆に「値が同じなら同じ」「不変で持ちたい」はVO寄り💎
5) TypeScriptで最小Entityを書こう🧩🪪
ここからは “小さく作って、テストで確認” の流れでいくよ😊🧡 (この章は 最小の形に絞るね。ID生成や比較ルールの深掘りは次章でもっとやるよ✨)
5-1) IDはVOとして作る(OrderId)🪪💎
ポイントはこれ👇
- IDはただの
stringにしない(混ざると事故る)😵💫 - equalsを持たせて「比較のルール」を固定🧷
// src/domain/order/OrderId.ts
export class OrderId {
private constructor(private readonly value: string) {}
static from(value: string): OrderId {
const v = value.trim();
if (!v) throw new Error("OrderId must not be empty");
return new OrderId(v);
}
// テストやサンプル用(本格的な生成は次章で扱うよ)
static generate(): OrderId {
return new OrderId(crypto.randomUUID());
}
equals(other: OrderId): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}
ちなみに最新のTypeScriptは 5.9 系(5.9.3 が “Latest” 表示)だよ📌 最新追従のときは、公式リリース情報を基準にするのが安全✨ (GitHub)
5-2) Entityの共通クラス(同一性=ID)🪪✨
Entityの核はこれだけ👇
- equalsはIDで判定する
- 値が全部同じでも、IDが違えば別物🙅♀️
// src/domain/shared/Entity.ts
export abstract class Entity<TId extends { equals(other: TId): boolean }> {
protected constructor(protected readonly _id: TId) {}
get id(): TId {
return this._id;
}
equals(other: Entity<TId>): boolean {
if (other === this) return true;
return this._id.equals(other._id);
}
}
5-3) Order Entity(状態は変わってもOK)☕🧾🪪
「注文」は変化するけど、同じ注文かどうかはIDで決まるよね😊
// src/domain/order/Order.ts
import { Entity } from "../shared/Entity";
import { OrderId } from "./OrderId";
export type OrderStatus = "Draft" | "Confirmed";
export class Order extends Entity<OrderId> {
private _status: OrderStatus;
private constructor(id: OrderId, status: OrderStatus) {
super(id);
this._status = status;
}
// 新規作成
static createNew(): Order {
return new Order(OrderId.generate(), "Draft");
}
// 永続化から復元(DBやAPIから読み込んだ体で)
static rehydrate(id: OrderId, status: OrderStatus): Order {
return new Order(id, status);
}
get status(): OrderStatus {
return this._status;
}
confirm(): void {
if (this._status !== "Draft") throw new Error("Only Draft can be confirmed");
this._status = "Confirmed";
}
}
6) テストで「IDが同じなら同じ」を証明🧪✨
ここは超だいじ! テストで体感すると、Entityが一気に腹落ちするよ😊🧡
Vitestでの例(今どきの主流寄り)🧪
Vitestは “Node >= 20” など要件が明記されてるので、導入時は公式ガイドを見てね📌 (Vitest)
// src/domain/order/Order.identity.test.ts
import { describe, it, expect } from "vitest";
import { Order } from "./Order";
import { OrderId } from "./OrderId";
describe("Order Entity identity", () => {
it("IDが同じなら、状態が違っても「同じ注文」になる🪪", () => {
const id = OrderId.from("order-001");
const a = Order.rehydrate(id, "Draft");
const b = Order.rehydrate(OrderId.from("order-001"), "Confirmed");
expect(a.equals(b)).toBe(true);
});
it("値(状態)が同じでも、IDが違えば別物🙅♀️", () => {
const a = Order.rehydrate(OrderId.from("order-001"), "Draft");
const b = Order.rehydrate(OrderId.from("order-002"), "Draft");
expect(a.equals(b)).toBe(false);
});
it("同じインスタンスは当然同じ👌", () => {
const a = Order.createNew();
expect(a.equals(a)).toBe(true);
});
});
7) AIの使いどころ(この章で効くやつ)🤖💞
7-1) Entity/VO分類クイズを作らせる🎮✨
AIにこう投げると便利👇
- 「カフェ注文ドメインで、Entity/VO候補を20個出して。理由も1行ずつ」
✅ 出てきた理由を見て、**“同一性がIDか?”**で仕分けしよう🪪
7-2) equals設計レビューをさせる🧠🔍
- 「Entityのequalsが値比較になってない?落とし穴を指摘して」
✅ “IDで比較する” を崩すとDDDがすぐ崩れるので、ここはAIレビューが刺さる💘
7-3) テスト観点を増やす🧪📌
- 「Entityの同一性テストで、追加すべきケースを列挙して」
✅ 抜けがちな観点(別クラス比較・null扱い等)を拾えるよ✨
8) よくある事故パターン集😂⚠️(超重要)
-
IDをただのstringで扱って混線😵💫
orderIdとcustomerIdを間違えてもコンパイルが通る地獄…
-
equalsを値の一致で作っちゃう🤯
- 「注文内容が同じなら同じ注文」になって破綻する
-
復元時にIDを作り直す🧨
- DBから読み込むたびに別注文扱いになる(同一性崩壊)
9) ミニ演習🎓🌸(10〜20分)
お題:Customer Entity を作ろう🧍🪪
-
CustomerId(VO)を作る -
Customer(Entity)を作る -
changeName()で名前は変わってOK -
テスト:
- ✅ ID同じなら同一
- ✅ 名前が違っても同一
- ✅ ID違えば別物
できたら最高〜!🎉✨
10) まとめ🎁✨
-
Entityは **「IDで同一性が決まる」**🪪
-
VOは **「値で同一性が決まる」**💎
-
TypeScriptでは
- IDをVOにして事故を防ぐ🧱
- EntityのequalsはID比較に固定🧷
- テストで腹落ち🧪
理解チェック✅🧠(答えは自分で言えると強い!)
- 「注文」がEntityな理由は?🪪
- 「Money」がVOな理由は?💴
- Entityのequalsが“値比較”だと何が起きる?😱
rehydrateみたいな復元が必要なのはなぜ?📦
次の第42章は、ここで軽く触れた「ID生成・比較ルール」を、もっと実務っぽくガッチリ固めていくよ〜🆔✨