value-objects

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

DDD Value Objects

DDD 值对象

1. Core Principles (RFC 2119)

1. 核心原则(RFC 2119)

Value Objects (VOs) in this project are immutable containers for domain primitives. They guarantee that an instantiated primitive is always valid according to business rules. 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.
  • Immutability: Class properties MUST be
    private readonly
    . No setters are allowed. Operations that "change" a VO MUST return a new instance.
  • Encapsulation: Constructors MUST be
    private
    .
  • Creation via Static Methods: You MUST use
    static from(value: T)
    to instantiate. You MAY use
    static random()
    for testing or generation (like generating a new UUID).
  • Railway-Oriented Validation: The
    from()
    method MUST NOT throw. Instead, it MUST return a
    Result<MyVO, InvalidMyVOError>
    using
    neverthrow
    .
  • Validation Isolation: You MUST provide a
    static checkValidity(value: T): boolean
    method. This allows external validation checks without instantiation.
  • Helper Functions: Value Objects MAY contain pure helper functions that evaluate or mutate algebraically (e.g.,
    isGreaterThan(other: ValueObject)
    ,
    add(other: ValueObject)
    ,
    equals(other: ValueObject)
    ). These logic-heavy domain methods are encouraged inside VOs.
  • Value Extraction & Branded Types: VOs MUST export a Branded Primitive Type and MUST only use a
    .toBranded()
    method to extract the raw value when crossing boundaries (e.g., Server Action boundaries returning to the client).
本项目中的值对象(VO)是领域原语的不可变容器。它们保证实例化后的原语始终符合业务规则的有效性要求。 本文档中的关键词 "MUST"、"MUST NOT"、"REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、"RECOMMENDED"、"MAY" 和 "OPTIONAL" 应按照 RFC 2119 中的描述进行解读。
  • 不可变性:类属性必须为
    private readonly
    。不允许使用setter。任何“修改”VO的操作必须返回新实例。
  • 封装性:构造函数必须为
    private
  • 通过静态方法创建:必须使用
    static from(value: T)
    进行实例化。可使用
    static random()
    用于测试或生成(例如生成新UUID)。
  • 铁路导向验证
    from()
    方法不得抛出异常。相反,必须使用
    neverthrow
    返回
    Result<MyVO, InvalidMyVOError>
  • 验证隔离:必须提供
    static checkValidity(value: T): boolean
    方法。这允许在不实例化的情况下进行外部验证检查。
  • 辅助函数:值对象可包含纯辅助函数,用于代数运算或判断(例如
    isGreaterThan(other: ValueObject)
    add(other: ValueObject)
    equals(other: ValueObject)
    )。鼓励将这些逻辑密集的领域方法放在VO内部。
  • 值提取与品牌类型:VO必须导出品牌化原语类型,并且只有在跨边界(例如Server Action返回给客户端)时,才能使用
    .toBranded()
    方法提取原始值。

2. Permitted "Infrastructure" Dependencies in the Domain

2. 领域层允许使用的“基础设施”依赖

While the domain layer is theoretically pure, implementing complex specification parsers (like UUID, Email, or URL) from scratch is unviable and error-prone. Therefore, the following "infrastructure" libraries are strictly considered extensions of our domain and MUST be used when needed:
  • UUID (v7): If a Value Object requires UUID generation or validation, it MUST use UUID version 7 (via an external library like
    uuid
    ). You MUST NOT implement your own UUID generator.
  • Zod: You MAY use
    zod
    internally in
    checkValidity()
    for complex structural validation (e.g.,
    z.uuid()
    ,
    z.email()
    , or
    z.url()
    ).
  • Neverthrow: The
    Result
    type is central to our domain architecture.
虽然领域层理论上是纯业务逻辑层,但从头实现复杂的规范解析器(如UUID、Email或URL)既不可行又容易出错。因此,以下“基础设施”库被严格视为领域层的扩展,必要时必须使用:
  • UUID (v7):如果值对象需要UUID生成或验证,必须使用UUID版本7(通过
    uuid
    等外部库)。不得自行实现UUID生成器。
  • Zod:可在
    checkValidity()
    内部使用
    zod
    进行复杂的结构验证(例如
    z.uuid()
    z.email()
    z.url()
    )。
  • Neverthrow
    Result
    类型是我们领域架构的核心。

3. Branded Types & Error Mapping

3. 品牌类型与错误映射

Errors related to a specific VO (e.g., parsing/validation errors) MUST be defined in the same file as the VO. This keeps domain meaning perfectly coupled.
All domain errors MUST use TypeScript branding to allow for safe
instanceof
narrowing. Additionally, the primitive value itself MUST be branded so external clients/layers (like ORMs or UI) receive type-safe validated strings/numbers.
typescript
declare const uniqueBrand: unique symbol;
export type BrandedDomainConcept = string & { readonly [uniqueBrand]: never };

declare const uniqueError: unique symbol;
export class InvalidDomainConceptError extends Error {
  declare private readonly [uniqueError]: never;
  private readonly invalidValue: string;
  // ...
}
与特定VO相关的错误(例如解析/验证错误)必须定义在与VO相同的文件中。这确保领域含义与错误完全耦合。
所有领域错误必须使用TypeScript品牌化,以支持安全的
instanceof
类型收窄。 此外,原语值本身必须品牌化,以便外部客户端/层(如ORM或UI)接收类型安全的验证后字符串/数字。
typescript
declare const uniqueBrand: unique symbol;
export type BrandedDomainConcept = string & { readonly [uniqueBrand]: never };

declare const uniqueError: unique symbol;
export class InvalidDomainConceptError extends Error {
  declare private readonly [uniqueError]: never;
  private readonly invalidValue: string;
  // ...
}

4. Structure of a Value Object

4. 值对象的结构

Here is the perfect template for a Value Object in this architecture.
以下是本架构中值对象的标准模板。

Example Implementation

示例实现

typescript
import { Result, ok, err } from "neverthrow";
import { z } from "zod";

// 1. Primitive Branded Type
declare const brandedTopicNameSymbol: unique symbol;
export type BrandedTopicName = string & { readonly [brandedTopicNameSymbol]: never };

// 2. Co-located Error Class
declare const unique: unique symbol;

export class InvalidTopicNameError extends Error {
  declare private readonly [unique]: never;
  private readonly invalidValue: string;

  constructor(invalidValue: string) {
    super(`The topic name "${invalidValue}" is invalid.`);
    this.invalidValue = invalidValue;
  }

  getInvalidValue(): string {
    return this.invalidValue;
  }
}

// 3. The Value Object
export class TopicName {
  static readonly MIN_LENGTH = 3;
  static readonly MAX_LENGTH = 50;

  // Immutability
  private readonly value: string;

  // Private constructor
  private constructor(value: string) {
    this.value = value.trim();
  }

  // Pure validation check
  static checkValidity(value: string): boolean {
    const trimmed = value.trim();
    // Zod can be used for things like z.email().safeParse(value).success
    return trimmed.length >= TopicName.MIN_LENGTH && trimmed.length <= TopicName.MAX_LENGTH;
  }

  // Instantiation via Result pattern
  static from(value: string): Result<TopicName, InvalidTopicNameError> {
    const trimmed = value.trim();
    
    if (!TopicName.checkValidity(trimmed)) {
      return err(new InvalidTopicNameError(trimmed));
    }
    
    return ok(new TopicName(trimmed));
  }

  // Test / System generation
  static random(): TopicName {
    return new TopicName(`topic-${Math.floor(Math.random() * 10000)}`);
  }

  // Safe extraction for cross-boundary serialization and queries
  toBranded(): BrandedTopicName {
    return this.value as BrandedTopicName;
  }
}
typescript
import { Result, ok, err } from "neverthrow";
import { z } from "zod";

// 1. 原语品牌类型
declare const brandedTopicNameSymbol: unique symbol;
export type BrandedTopicName = string & { readonly [brandedTopicNameSymbol]: never };

// 2. 共存的错误类
declare const unique: unique symbol;

export class InvalidTopicNameError extends Error {
  declare private readonly [unique]: never;
  private readonly invalidValue: string;

  constructor(invalidValue: string) {
    super(`The topic name "${invalidValue}" is invalid.`);
    this.invalidValue = invalidValue;
  }

  getInvalidValue(): string {
    return this.invalidValue;
  }
}

// 3. 值对象
export class TopicName {
  static readonly MIN_LENGTH = 3;
  static readonly MAX_LENGTH = 50;

  // 不可变性
  private readonly value: string;

  // 私有构造函数
  private constructor(value: string) {
    this.value = value.trim();
  }

  // 纯验证检查
  static checkValidity(value: string): boolean {
    const trimmed = value.trim();
    // Zod可用于类似z.email().safeParse(value).success的场景
    return trimmed.length >= TopicName.MIN_LENGTH && trimmed.length <= TopicName.MAX_LENGTH;
  }

  // 通过Result模式实例化
  static from(value: string): Result<TopicName, InvalidTopicNameError> {
    const trimmed = value.trim();
    
    if (!TopicName.checkValidity(trimmed)) {
      return err(new InvalidTopicNameError(trimmed));
    }
    
    return ok(new TopicName(trimmed));
  }

  // 测试/系统生成
  static random(): TopicName {
    return new TopicName(`topic-${Math.floor(Math.random() * 10000)}`);
  }

  // 跨边界序列化和查询的安全提取
  toBranded(): BrandedTopicName {
    return this.value as BrandedTopicName;
  }
}

5. Helpful References

5. 实用参考

Rather than providing every variant of helper logic in this document, we rely on concrete reference files built from actual DDD project specifications provided in the
.agents/skills/value-objects/references/
folder. Ensure you read these files to copy the robust structure:
  • Positive Integer VO: Example of a numeric VO featuring extensive helper/domain operations (
    isGreaterThan
    ,
    add
    ,
    subtract
    ,
    min
    ,
    max
    ).
  • Folder Name VO: Example of string constraints based on bounds mapping (business rules).
  • UUID VO: Example of infrastructure dependency usage inside the domain (generating and validating UUIDv7).
  • Email VO: Example of using
    zod
    locally in the domain specifically strictly for standard formatting checks (
    z.email()
    ).
本文档不提供所有辅助逻辑的变体,而是依赖于从实际DDD项目规范构建的具体参考文件,这些文件位于
.agents/skills/value-objects/references/
文件夹中。请务必阅读这些文件以复制健壮的结构:
  • Positive Integer VO:数值型VO的示例,包含丰富的辅助/领域操作(
    isGreaterThan
    add
    subtract
    min
    max
    )。
  • Folder Name VO:基于边界映射(业务规则)的字符串约束示例。
  • UUID VO:领域层使用基础设施依赖的示例(生成和验证UUIDv7)。
  • Email VO:在领域层本地使用
    zod
    进行标准格式检查的示例(
    z.email()
    )。