entities
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDDD Entities & Aggregates
DDD实体与聚合根
1. Core Principles (RFC 2119)
1. 核心原则(RFC 2119)
Entities represent domain concepts with a distinct identity (usually a ). Their primary role is to encapsulate state and business logic, ensuring that the object is always valid.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
UUID- Identity: Every entity MUST have an explicit identifier (e.g., ,
folderId) typed as a Value Object (e.g.,userId).UUID - Encapsulation: State MUST be for mutable properties, and
privatefor identifiers and immutable creation dates. Public fields are STRICTLY FORBIDDEN.private readonly - No Setters: You MUST NOT use accessors or generic
setmethods. You MUST create domain-specific behavior methods indicating intent (e.g.,update(data)instead ofrename(newName: FolderName)).setName(newName: string) - Constructors: Constructors MUST always take a single object pattern (
params). They MUST ONLY accept valid domain types (Value Objects or native Dates), never primitives.constructor(params: { ... }) - Factories / Hydration: The constructor is primarily used by the layer to reconstruct (hydrate) entities from the database, or by use cases when creating completely new entities.
infrastructure/ - Getters: Expose internal state only when necessary using explicit getters (e.g., ,
getId()). These getters MUST return Value Objects or Dates, never primitives.getName() - Snapshots: Every entity MUST define a type and expose a
<EntityName>Snapshotmethod. This method MUST return a plain object containing only Branded Primitives (viatoSnapshot()) and native Dates. The snapshot MUST NEVER include truly private or internal data (such as password hashes or internal security states)..toBranded()
实体代表具有唯一标识(通常为)的领域概念。它们的核心作用是封装状态与业务逻辑,确保对象始终处于有效状态。
本文档中的关键词"MUST"、"MUST NOT"、"REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、"RECOMMENDED"、"MAY"和"OPTIONAL"的含义需按照RFC 2119中的定义进行解读。
UUID- 标识:每个实体必须具备一个显式标识符(例如、
folderId),且该标识符的类型为值对象(例如userId)。UUID - 封装:可变属性的状态必须设为,标识符和不可变的创建时间需设为
private。严格禁止使用公共字段。private readonly - 禁止Setter:不得使用访问器或通用的
set方法。必须创建体现业务意图的领域特定行为方法(例如使用update(data)而非rename(newName: FolderName))。setName(newName: string) - 构造函数:构造函数必须始终采用单个对象模式(
params)。只能接受有效的领域类型(值对象或原生Date类型),不得接受原始类型。constructor(params: { ... }) - 工厂/数据还原:构造函数主要由层用于从数据库中重构(还原)实体,或用例用于创建全新实体。
infrastructure/ - 访问器(Getters):仅在必要时通过显式访问器暴露内部状态(例如、
getId())。这些访问器必须返回值对象或Date类型,不得返回原始类型。getName() - 快照(Snapshots):每个实体必须定义一个类型,并暴露
<EntityName>Snapshot方法。该方法必须返回一个仅包含标记原始类型(通过toSnapshot())和原生Date类型的普通对象。快照绝对不能包含真正的私有或内部数据(如密码哈希或内部安全状态)。.toBranded()
2. Structure of an Entity
2. 实体的结构
Here is the template for a domain entity.
typescript
import { UUID, type BrandedUUID } from "@/shared/domain/value-objects/uuid";
import { FolderName, type BrandedFolderName } from "../value-objects/folderName";
// 1. Snapshot definition (No private fields like password hashes)
export type FolderSnapshot = {
folderId: BrandedUUID;
folderName: BrandedFolderName;
createdAt: Date;
updatedAt: Date;
};
export class Folder {
// 2. Private properties, strictly typed with VOs
private readonly folderId: UUID;
private folderName: FolderName;
private readonly createdAt: Date;
private updatedAt: Date;
// 3. Object parameter constructor
constructor(params: {
folderId: UUID;
folderName: FolderName;
createdAt: Date;
updatedAt: Date;
}) {
this.folderId = params.folderId;
this.folderName = params.folderName;
this.createdAt = params.createdAt;
this.updatedAt = params.updatedAt;
}
// 4. Domain behavior (mutates state with intent)
rename(newName: FolderName): void {
// Optionally apply business rules here before mutation
this.folderName = newName;
this.updatedAt = new Date();
}
// 5. Explicit Getters
getId(): UUID {
return this.folderId;
}
getName(): FolderName {
return this.folderName;
}
getCreatedAt(): Date {
return this.createdAt;
}
getUpdatedAt(): Date {
return this.updatedAt;
}
// 6. Snapshot builder
toSnapshot(): FolderSnapshot {
return {
folderId: this.folderId.toBranded(),
folderName: this.folderName.toBranded(),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
}
}以下是领域实体的模板。
typescript
import { UUID, type BrandedUUID } from "@/shared/domain/value-objects/uuid";
import { FolderName, type BrandedFolderName } from "../value-objects/folderName";
// 1. Snapshot definition (No private fields like password hashes)
export type FolderSnapshot = {
folderId: BrandedUUID;
folderName: BrandedFolderName;
createdAt: Date;
updatedAt: Date;
};
export class Folder {
// 2. Private properties, strictly typed with VOs
private readonly folderId: UUID;
private folderName: FolderName;
private readonly createdAt: Date;
private updatedAt: Date;
// 3. Object parameter constructor
constructor(params: {
folderId: UUID;
folderName: FolderName;
createdAt: Date;
updatedAt: Date;
}) {
this.folderId = params.folderId;
this.folderName = params.folderName;
this.createdAt = params.createdAt;
this.updatedAt = params.updatedAt;
}
// 4. Domain behavior (mutates state with intent)
rename(newName: FolderName): void {
// Optionally apply business rules here before mutation
this.folderName = newName;
this.updatedAt = new Date();
}
// 5. Explicit Getters
getId(): UUID {
return this.folderId;
}
getName(): FolderName {
return this.folderName;
}
getCreatedAt(): Date {
return this.createdAt;
}
getUpdatedAt(): Date {
return this.updatedAt;
}
// 6. Snapshot builder
toSnapshot(): FolderSnapshot {
return {
folderId: this.folderId.toBranded(),
folderName: this.folderName.toBranded(),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
}
}3. Creating vs Reconstituting
3. 创建与重构
Entities don't return from constructors because the primitives passed to them are already validated Value Objects (which handled the validation).
Resultneverthrow- Infrastructure Layer: Reads primitives from the DB, calls ,
UUID.from(), etc. If valid, it MUST pass them into theFolderName.from()constructor.new <Entity>({ ... }) - Use Cases: Creates entirely new identifiers , takes validated input from the Server Action, and instantiates the Entity to persist it.
UUID.random()
实体的构造函数不会返回,因为传入的原始类型已经是经过验证的值对象(由处理验证逻辑)。
Resultneverthrow- 基础设施层:从数据库读取原始类型,调用、
UUID.from()等方法。如果验证通过,必须将它们传入FolderName.from()构造函数。new <Entity>({ ... }) - 用例层:创建全新的标识符,从Server Action获取经过验证的输入,然后实例化实体以进行持久化。
UUID.random()
4. Data Boundaries (Snapshots & Serialization)
4. 数据边界(快照与序列化)
Entities MUST NOT be returned directly from Server Actions to the Client, as class instances lose their methods across the RSC (React Server Component) / Client boundary.
When an Entity needs to be serialized for the client, Server Actions MUST extract data using the entity's method. The type is a structured plain object explicitly mapping Value Objects down to Branded Primitives.
.toSnapshot()SnapshotCrucially, a Snapshot MUST NEVER include restricted private fields (like cryptographic hashes or internal server configurations). The Snapshot serves simultaneously as the serialization format and the public boundary type.
不得直接从Server Action向客户端返回实体,因为类实例在React Server Component(RSC)与客户端的边界传输时会丢失其方法。
当需要将实体序列化给客户端时,Server Action必须使用实体的方法提取数据。类型是一个结构化的普通对象,显式地将值对象映射为标记原始类型。
.toSnapshot()Snapshot关键的是,快照绝对不能包含受限的私有字段(如加密哈希或内部服务器配置)。快照同时作为序列化格式和公共边界类型。
5. Aggregate Roots
5. 聚合根
If an entity controls child entities (e.g., a controlling ), the workspace is the Aggregate Root.
WorkspaceTeamMembers- All modifications to child entities MUST route through the Aggregate Root's methods.
- Repositories MUST ONLY be built for Aggregate Roots, not for child entities directly.
如果一个实体控制子实体(例如控制),那么该工作区就是聚合根。
WorkspaceTeamMembers- 对子实体的所有修改必须通过聚合根的方法进行。
- 仓库(Repositories)只能为聚合根构建,不能直接为子实体构建。