AgentSkillsCN

implement-value-object

运用冻结数据类模式,结合校验机制,打造不可变的领域值对象。适用于领域值对象的实现、不可变数据结构的构建,或为各类数值添加校验规则的场景。涵盖@dataclass(frozen=True)、object.__setattr__()在__post_init__中的应用、工厂方法(from_string、from_dict、from_content),以及在冻结上下文中进行校验的完整流程。当您“为X创建值对象”、“实现Y的不可变值”、“为Z的值添加校验规则”或“构建值对象”时,本指南将助您事半功倍。

SKILL.md
--- frontmatter
name: implement-value-object
description: |
  Creates immutable domain value objects using frozen dataclass pattern with validation.
  Use when implementing domain value objects, creating immutable data structures, or
  adding validation to values. Covers @dataclass(frozen=True), object.__setattr__()
  pattern in __post_init__, factory methods (from_string, from_dict, from_content),
  and validation in frozen context. Triggers on "create value object for X", "implement
  immutable Y value", "add validation to Z value", or "build value object".
allowed-tools:
  - Read
  - Grep
  - Glob
  - Write
  - Edit

Works with Python dataclasses in domain/value_objects/ and domain/values/ directories.

implement-value-object

Purpose

Create immutable domain value objects using the frozen dataclass pattern with proper validation, factory methods, and immutability guarantees enforced at the type system level.

When to Use

Use this skill when:

  • Implementing domain value objects - Creating validated, immutable domain concepts
  • Creating immutable data structures - Building type-safe value containers
  • Adding validation to values - Ensuring domain constraints are enforced

Trigger phrases:

  • "Create a value object for X"
  • "Implement immutable Y value"
  • "Add validation to Z value"
  • "Build value object with validation"

Quick Start

Create immutable domain value objects using the frozen dataclass pattern with proper validation, factory methods, and immutability guarantees enforced at the type system level.

Most common use case:

For a simple validated value object:

python
from dataclasses import dataclass

@dataclass(frozen=True)
class EmailAddress:
    """Immutable email address value object."""

    value: str

    def __post_init__(self) -> None:
        """Validate email format."""
        if not self.value:
            raise ValueError("Email address cannot be empty")
        if "@" not in self.value:
            raise ValueError(f"Invalid email format: {self.value}")

Table of Contents

Core Sections

Advanced Topics

Utility Scripts

Purpose

Create immutable domain value objects using the frozen dataclass pattern with proper validation, factory methods, and immutability guarantees enforced at the type system level.

Instructions

Step 1: Define Frozen Dataclass

Create the basic immutable structure:

python
from dataclasses import dataclass

@dataclass(frozen=True)
class YourValueObject:
    """Immutable value object representing [domain concept].

    Encapsulates [what logic/behavior].
    """

    value: str  # Primary value
    # Optional additional fields for metadata

Key points:

  • Always use @dataclass(frozen=True) for immutability
  • Add comprehensive docstring explaining domain meaning
  • Use value as primary field name for simple value objects
  • All fields are immutable after construction

Step 2: Add Validation in post_init

Validate constraints while respecting immutability:

python
def __post_init__(self) -> None:
    """Validate value object constraints."""
    # Validation checks (raise ValueError on failure)
    if not self.value:
        raise ValueError("Value cannot be empty")

    # For type coercion or normalization in frozen context:
    if not isinstance(self.value, ExpectedType):
        object.__setattr__(self, "value", ExpectedType(self.value))

    # For normalization (e.g., path resolution):
    object.__setattr__(self, "value", self.normalize(self.value))

CRITICAL: Modifying Frozen Dataclass Fields

Since frozen=True prevents normal attribute assignment, use object.__setattr__() to modify fields during __post_init__:

python
# ❌ WRONG - Will raise FrozenInstanceError
def __post_init__(self) -> None:
    self.value = self.value.lower()  # FAILS with frozen=True

# ✅ CORRECT - Use object.__setattr__()
def __post_init__(self) -> None:
    object.__setattr__(self, "value", self.value.lower())

Validation patterns:

  • Empty checks: if not self.value: raise ValueError(...)
  • Format validation: if not re.match(pattern, self.value): raise ValueError(...)
  • Type coercion: object.__setattr__(self, "field", Type(field))
  • Normalization: object.__setattr__(self, "value", normalized_value)

Step 3: Add Factory Methods

Provide convenient constructors for different input types:

python
@classmethod
def from_string(cls, value_str: str) -> "YourValueObject":
    """Create from string representation.

    Args:
        value_str: String to parse

    Returns:
        YourValueObject instance
    """
    return cls(value=value_str)

@classmethod
def from_dict(cls, data: dict) -> "YourValueObject":
    """Create from dictionary representation.

    Args:
        data: Dictionary with value object data

    Returns:
        YourValueObject instance
    """
    return cls(value=data["value"])

@classmethod
def from_content(cls, content: str) -> "YourValueObject":
    """Create from raw content (e.g., compute hash).

    Args:
        content: Raw content to process

    Returns:
        YourValueObject instance
    """
    # Process content (e.g., hash it)
    processed = process(content)
    return cls(value=processed)

Common factory method patterns:

  • from_string() - Parse string representation
  • from_dict() - Deserialize from dictionary
  • from_content() - Compute value from content (hashes)
  • from_components() - Build from multiple parts
  • from_bytes() - Create from binary data

Step 4: Add String Representations

Provide useful string representations:

python
def __str__(self) -> str:
    """User-friendly string representation.

    Returns:
        The value as a string
    """
    return str(self.value)

def __repr__(self) -> str:
    """Developer-friendly representation.

    Returns:
        String showing class and value
    """
    return f"YourValueObject('{self.value}')"

For long values (like hashes), truncate in repr:

python
def __repr__(self) -> str:
    """Developer-friendly representation."""
    if len(self.value) > 8:
        return f"FileHash('{self.value[:8]}...')"
    return f"FileHash('{self.value}')"

Step 5: Add Domain Behavior

Add methods that express domain operations:

python
@property
def short_version(self) -> str:
    """Get shortened version for display."""
    return self.value[:8]

def matches_pattern(self, pattern: str) -> bool:
    """Check if value matches a pattern."""
    import fnmatch
    return fnmatch.fnmatch(self.value, pattern)

def to_dict(self) -> dict:
    """Convert to dictionary representation."""
    return {
        "value": self.value,
        # Include computed properties
        "short": self.short_version,
    }

Common domain behaviors:

  • Comparison operations (equality is automatic with dataclass)
  • Pattern matching
  • Format conversion
  • Computed properties
  • Dictionary serialization

Step 6: Add Type Hints and Documentation

Ensure full type safety:

python
from typing import Optional

@dataclass(frozen=True)
class ComplexValueObject:
    """Detailed docstring."""

    value: str
    metadata: Optional[str] = None  # Optional fields need defaults

    def operation(self, param: str) -> "ComplexValueObject":
        """Return type hints use string for forward reference."""
        # Operations that return new instances (immutability)
        return ComplexValueObject(value=f"{self.value}:{param}")

Examples

See examples/detailed-examples.md for comprehensive examples including:

  • Simple string value objects with validation
  • Path value objects with normalization and type coercion
  • Hash value objects with factory methods
  • Multi-field value objects with complex validation

Requirements

  • Python 3.10+ (for dataclasses and type union syntax X | None)
  • No external dependencies for basic value objects
  • Optional: xxhash for fast hashing (uv pip install xxhash)

Common Patterns

Pattern: Computed Properties

Use @property for derived values:

python
@property
def short_id(self) -> str:
    """Get shortened version of identity."""
    parts = self.value.split(":")
    return parts[-1] if parts else self.value

Pattern: Type Coercion in Frozen Context

When you need to ensure type consistency:

python
def __post_init__(self) -> None:
    """Ensure value is correct type."""
    if not isinstance(self.value, Path):
        object.__setattr__(self, "value", Path(self.value))

Pattern: Validation with Custom Errors

Provide clear error messages:

python
def __post_init__(self) -> None:
    """Validate with descriptive errors."""
    if not self.value:
        raise ValueError(f"{self.__class__.__name__} value cannot be empty")
    if len(self.value) < 3:
        raise ValueError(f"{self.__class__.__name__} must be at least 3 characters")

Pattern: Serialization

Support dictionary conversion:

python
def to_dict(self) -> dict:
    """Convert to dictionary for serialization."""
    return {
        "value": self.value,
        # Include any metadata or computed properties
    }

@classmethod
def from_dict(cls, data: dict) -> "YourValueObject":
    """Deserialize from dictionary."""
    return cls(value=data["value"])

Testing Value Objects

Value objects should be tested for:

  1. Validation: Ensure invalid inputs raise ValueError
  2. Immutability: Verify frozen behavior
  3. Equality: Test value-based equality (automatic with dataclass)
  4. Factory methods: Test all construction paths
  5. Serialization: Test to_dict/from_dict round-trip
python
def test_value_object_validation():
    """Test validation raises on invalid input."""
    with pytest.raises(ValueError, match="cannot be empty"):
        YourValueObject(value="")

def test_value_object_immutability():
    """Test frozen behavior."""
    obj = YourValueObject(value="test")
    with pytest.raises(FrozenInstanceError):
        obj.value = "changed"

def test_factory_method():
    """Test factory method construction."""
    obj = YourValueObject.from_string("test")
    assert obj.value == "test"

def test_serialization_round_trip():
    """Test to_dict/from_dict round-trip."""
    obj = YourValueObject(value="test")
    data = obj.to_dict()
    restored = YourValueObject.from_dict(data)
    assert restored == obj

File Locations

Value objects belong in the domain layer:

  • Simple value objects: src/project_watch_mcp/domain/value_objects/
  • Complex value objects: src/project_watch_mcp/domain/values/

Naming convention: {concept_name}.py (e.g., file_path.py, identity.py)

See Also