AgentSkillsCN

Scaffold Value Object

脚手架值对象

SKILL.md

Scaffold Value Object

Generate a DDD Value Object with immutability, structural equality, and validation.

Triggers

  • "scaffold value object"
  • "create value object"
  • "create vo"
  • "new value object"

Instructions

Step 1: Gather Information

Ask the student for:

  1. Value Object name (e.g., Money, EmailAddress, DateRange, Address)
  2. Properties with types (e.g., amount: number, currency: string)
  3. Validation rules (e.g., "amount must be positive", "currency must be 3 letters")

If the student provides a name only, suggest reasonable properties based on common DDD examples.

Step 2: Determine Output Location

Check the current working directory:

  • If in a before/ directory, create files there
  • If in a lesson directory, create in before/
  • Otherwise, create in the current directory

Step 3: Generate Value Object File

Create {value-object-name}.ts (kebab-case filename):

typescript
import { z } from 'zod';

// === Validation Schema ===

const {ValueObject}Schema = z.object({
  // Properties with Zod validation
});

type {ValueObject}Props = z.infer<typeof {ValueObject}Schema>;

// === Result Type ===

type Result<T, E = Error> =
  | { success: true; value: T }
  | { success: false; error: E };

// === Value Object ===

export class {ValueObject} {
  private constructor(private readonly props: Readonly<{ValueObject}Props>) {
    Object.freeze(this);
  }

  // Factory method
  static create(props: {ValueObject}Props): Result<{ValueObject}> {
    const validation = {ValueObject}Schema.safeParse(props);
    if (!validation.success) {
      return {
        success: false,
        error: new Error(validation.error.errors.map(e => e.message).join(', ')),
      };
    }

    return { success: true, value: new {ValueObject}(validation.data) };
  }

  // Property accessors (read-only)

  // Structural equality — two VOs are equal if all properties match
  equals(other: {ValueObject}): boolean {
    return (
      // Compare each property
      JSON.stringify(this.props) === JSON.stringify(other.props)
    );
  }

  // Return a new instance with modified properties (immutability)
  with(overrides: Partial<{ValueObject}Props>): Result<{ValueObject}> {
    return {ValueObject}.create({ ...this.props, ...overrides });
  }

  // String representation
  toString(): string {
    return `{ValueObject}(${JSON.stringify(this.props)})`;
  }
}

Step 4: Generate Test File

Create {value-object-name}.test.ts:

typescript
import { describe, it, expect } from 'vitest';
import { {ValueObject} } from './{value-object-name}';

describe('{ValueObject}', () => {
  describe('create', () => {
    it('should create with valid properties', () => {
      const result = {ValueObject}.create({
        // valid props
      });

      expect(result.success).toBe(true);
    });

    it('should fail with invalid properties', () => {
      const result = {ValueObject}.create({
        // invalid props
      });

      expect(result.success).toBe(false);
    });
  });

  describe('equality', () => {
    it('should be equal when all properties match', () => {
      const vo1 = {ValueObject}.create({ /* props */ });
      const vo2 = {ValueObject}.create({ /* same props */ });

      if (vo1.success && vo2.success) {
        expect(vo1.value.equals(vo2.value)).toBe(true);
      }
    });

    it('should not be equal when properties differ', () => {
      const vo1 = {ValueObject}.create({ /* props A */ });
      const vo2 = {ValueObject}.create({ /* props B */ });

      if (vo1.success && vo2.success) {
        expect(vo1.value.equals(vo2.value)).toBe(false);
      }
    });
  });

  describe('immutability', () => {
    it('should return a new instance from with()', () => {
      const original = {ValueObject}.create({ /* props */ });

      if (original.success) {
        const modified = original.value.with({ /* changed prop */ });

        expect(modified.success).toBe(true);
        if (modified.success) {
          expect(original.value.equals(modified.value)).toBe(false);
        }
      }
    });

    it('should be frozen', () => {
      const result = {ValueObject}.create({ /* props */ });

      if (result.success) {
        expect(Object.isFrozen(result.value)).toBe(true);
      }
    });
  });
});

Step 5: Explain the Pattern

After generating, briefly explain:

  1. No identity — Value Objects have no ID; they are defined entirely by their properties
  2. ImmutabilityObject.freeze() + readonly prevent mutation after creation
  3. Structural equality — two VOs with the same properties are considered equal
  4. with() method — creates a new instance instead of mutating (like Date vs moment)
  5. Factory + validation — ensures the VO is always in a valid state
  6. When to use — use for concepts like Money, Email, Address, DateRange — things with no lifecycle

Customization Notes

  • Replace placeholder properties with the student's actual properties
  • For simple single-value VOs (e.g., EmailAddress), simplify to a single property
  • Add domain-specific methods (e.g., Money.add(), DateRange.overlaps())
  • For complex equality, replace JSON.stringify with property-by-property comparison
  • Consider adding fromPrimitive() / toPrimitive() for serialization