Entity(實體)
是什麼?
Entity 是 DDD 中具有**唯一身份識別(Identity)**的領域物件。兩個 Entity 即使所有屬性都相同,只要 ID 不同,就是不同的物件;反之,即使屬性全都改了,只要 ID 相同,就是同一個物件。
ℹ️DDD 戰術模式
Entity 是 DDD 戰術設計(Tactical Design)的核心建構塊之一,與 Value Object、Aggregate 並列為最基礎的三個概念。
核心觀念
- Identity 是靈魂:Entity 的相等性由 ID 決定,不是由屬性值決定
- 有生命週期:Entity 會被建立、修改、最終可能被刪除或封存,狀態會隨時間改變
- 封裝業務規則:Entity 不只是資料容器,它內部包含業務邏輯和不變量(Invariants)
- Entity 基底類別:實務上常定義一個抽象基底類別,統一處理 ID 和相等性比較
常見誤區
⚠️常見誤區
誤區一:把 Entity 當成資料庫的 Row。Entity 是領域概念,不是 ORM 的產物。先思考業務需求,再考慮怎麼存。
誤區二:Entity 只有 getter/setter 沒有行為。這叫 Anemic Domain Model(貧血模型),失去了 DDD 的核心價值。Entity 必須封裝業務邏輯。
誤區三:用所有屬性來比較兩個 Entity 是否相同。Entity 的相等性只看 ID。
設計流程
識別領域概念
找出業務中需要追蹤身份的物件(如訂單、使用者、帳戶)
定義 Identity
決定用什麼作為唯一識別(GUID、流水號、業務編號)
封裝業務規則
把相關的業務邏輯放進 Entity,而非散落在 Service 裡
實作相等性
覆寫 Equals 和 GetHashCode,只比較 ID
管理生命週期
定義 Entity 的狀態轉換規則(如訂單:建立→付款→出貨→完成)
程式碼範例
C# 版本
// Entity 基底類別
public abstract class Entity<TId> where TId : notnull
{
public TId Id { get; protected set; }
protected Entity(TId id)
{
Id = id;
}
public override bool Equals(object? obj)
{
if (obj is not Entity<TId> other) return false;
if (ReferenceEquals(this, other)) return true;
return Id.Equals(other.Id);
}
public override int GetHashCode() => Id.GetHashCode();
public static bool operator ==(Entity<TId>? left, Entity<TId>? right)
=> Equals(left, right);
public static bool operator !=(Entity<TId>? left, Entity<TId>? right)
=> !Equals(left, right);
}
// 具體 Entity:訂單
public class Order : Entity<Guid>
{
public Customer Customer { get; }
public OrderStatus Status { get; private set; }
private readonly List<OrderLineItem> _lineItems = new();
public IReadOnlyList<OrderLineItem> LineItems => _lineItems.AsReadOnly();
public Order(Guid id, Customer customer) : base(id)
{
Customer = customer;
Status = OrderStatus.Created;
}
public void AddLineItem(Product product, int quantity)
{
if (Status != OrderStatus.Created)
throw new InvalidOperationException("Cannot modify a placed order.");
_lineItems.Add(new OrderLineItem(product, quantity));
}
public void Place()
{
if (!_lineItems.Any())
throw new InvalidOperationException("Order must have at least one line item.");
Status = OrderStatus.Placed;
}
}
// 使用
var order1 = new Order(Guid.NewGuid(), customer);
var order2 = new Order(order1.Id, customer);
Console.WriteLine(order1 == order2); // true — 同一個 ID 就是同一張訂單TypeScript 版本
// Entity 基底類別
abstract class Entity<TId> {
constructor(public readonly id: TId) {}
equals(other: Entity<TId>): boolean {
if (this === other) return true;
return this.id === other.id;
}
}
// 具體 Entity:訂單
class Order extends Entity<string> {
private lineItems: OrderLineItem[] = [];
private status: OrderStatus = OrderStatus.Created;
constructor(id: string, public readonly customer: Customer) {
super(id);
}
addLineItem(product: Product, quantity: number): void {
if (this.status !== OrderStatus.Created) {
throw new Error("Cannot modify a placed order.");
}
this.lineItems.push(new OrderLineItem(product, quantity));
}
place(): void {
if (this.lineItems.length === 0) {
throw new Error("Order must have at least one line item.");
}
this.status = OrderStatus.Placed;
}
}
// 使用
const order1 = new Order("ORD-001", customer);
const order2 = new Order("ORD-001", customer);
console.log(order1.equals(order2)); // truePython 版本
from abc import ABC
from dataclasses import dataclass, field
from typing import Generic, TypeVar, List
from uuid import UUID, uuid4
from enum import Enum
TId = TypeVar("TId")
class Entity(ABC, Generic[TId]):
def __init__(self, id: TId):
self._id = id
@property
def id(self) -> TId:
return self._id
def __eq__(self, other: object) -> bool:
if not isinstance(other, Entity):
return False
return self._id == other._id
def __hash__(self) -> int:
return hash(self._id)
class OrderStatus(Enum):
CREATED = "created"
PLACED = "placed"
SHIPPED = "shipped"
class Order(Entity[UUID]):
def __init__(self, id: UUID, customer: "Customer"):
super().__init__(id)
self.customer = customer
self._status = OrderStatus.CREATED
self._line_items: List["OrderLineItem"] = []
def add_line_item(self, product: "Product", quantity: int) -> None:
if self._status != OrderStatus.CREATED:
raise ValueError("Cannot modify a placed order.")
self._line_items.append(OrderLineItem(product, quantity))
def place(self) -> None:
if not self._line_items:
raise ValueError("Order must have at least one line item.")
self._status = OrderStatus.PLACED
# 使用
order1 = Order(uuid4(), customer)
order2 = Order(order1.id, customer)
print(order1 == order2) # True概念圖
此圖展示 Entity 的三大特徵(Identity、生命週期、業務行為)和它與其他 DDD 建構塊的關係。Entity 的屬性通常是 Value Object(如 Money、Email),而 Entity 本身又是組成 Aggregate 的核心元素。
Entity vs Value Object 決策樹
問自己以下問題來判斷一個概念是 Entity 還是 Value Object:
- 這個東西需要被「追蹤」嗎? 你需要說「就是那一個」而不是「任何一個都可以」嗎?
- 是 → 往 Entity 方向
- 否 → 往 Value Object 方向
- 兩個屬性完全相同的實例,是「同一個」嗎?
- 不是(如兩個同名同齡的人是不同的人) → Entity
- 是(如兩張一百元是等值的) → Value Object
- 它的屬性會隨時間改變嗎?
- 會(如訂單狀態從「建立」變「已付款」) → Entity
- 不會,要改就建新的 → Value Object
常見範例對照:
| Entity | Value Object | |--------|-------------| | User(使用者帳號) | Email(電子郵件地址) | | Order(訂單) | Money(金額) | | Product(商品) | Address(地址) | | BankAccount(銀行帳戶) | DateRange(日期區間) |
實戰補充
💡資深開發者筆記
在 C# 專案中,建議建立一個 Entity<TId> 基底類別放在 Domain 層的 SeedWork 或 Common 資料夾。所有 Entity 繼承它,省去重複實作 Equals 和 GetHashCode 的麻煩。
ID 的型別選擇:GUID 適合分散式系統(無需中央分配);自增整數適合單體應用(查詢效率高);業務編號(如 ORD-20260407-001)適合需要人類可讀的場景。三者可以混搭使用。
最重要的一點:Entity 不是 DTO。Entity 要有行為,DTO 只有資料。如果你的 Entity 只有 getter/setter,回頭檢查業務邏輯是不是被放到 Service 裡了。
理解測驗
🤔 兩個 Entity 什麼情況下被視為「相同」?
🤔 以下哪個是 Entity 而不是 Value Object?
🤔 什麼是 Anemic Domain Model(貧血模型)?
重點整理
💡一句話記住
口訣:「Entity 有身分證、有生命、會做事」—— 三個特徵缺一就不是 Entity。
| 概念 | 說明 | |------|------| | Identity | Entity 的唯一識別,決定相等性 | | 生命週期 | Entity 會被建立、修改、刪除 | | 業務行為 | Entity 封裝業務邏輯,不只是資料容器 | | 基底類別 | 統一處理 ID 和 Equals/GetHashCode | | vs Value Object | Entity 看 ID,Value Object 看值 |