Files
freemo d9e5668cec fix(skills): comprehensive final audit pass for programming-patterns skill
Fixes and improvements from exhaustive audit:

Consistency fixes in SKILL.md:
- 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected)
- 'Singleton for factory instance' → clarified to 'register factory as
  singleton-scoped via DI container' (less misleading wording)
- Documentation Format section updated with note that SKILL.md itself is the
  authoritative source for related-pattern combinations

Coverage fix — Related Patterns sections:
- Added '## Related Patterns' to ALL 94 pattern files (was 0/94)
- Each section lists 3–6 related patterns with relationship descriptions
- Covers: why they're related, when to prefer one vs the other,
  and which are often confused

SOLID principles → Creational → Structural → Behavioral → Architectural →
Concurrency → Functional → Resilience → Data Access → Messaging →
Testing → Error Handling → Microservice — all 13 categories covered

Code verification:
- Python: 0 failures (all 85 testable blocks pass)
- Go: 0 failures (all 76 testable blocks pass)
- JavaScript: 0 failures (all 78 testable blocks pass)
- All 239 code blocks verified correct after edits

Final skill state:
- 108 files, 36,524 lines across 13 reference categories
- 94/94 pattern files have Related Patterns sections
- 2,815-line SKILL.md with 67 decision trees, 23 scenarios,
  0 broken references, 0 naming inconsistencies
2026-04-15 13:22:13 -04:00

9.0 KiB

Value Object Pattern

Problem

Primitive types (strings, numbers) are frequently used to represent domain concepts like money, email addresses, or coordinates. But primitives carry no validation, no behavior, and no semantic meaning — $10 USD is just a float, and mixing currencies or comparing incompatible values causes bugs that the type system can't catch.

Solution

Create small, immutable objects that represent domain concepts by value rather than identity. Two value objects are equal if all their attributes are equal (unlike entities, which are equal by ID). Value objects encapsulate validation and domain-specific behavior.

Key properties:

  • Immutable — Once created, never changed.
  • Equality by value — Two Money(10, "USD") instances are equal.
  • Self-validating — Invalid states are rejected at construction.
  • Side-effect free — Operations return new instances.

When to Use

  • Money/currency amounts
  • Email addresses, phone numbers, URLs
  • Geographic coordinates, measurements
  • Date ranges, time periods
  • Anywhere primitives carry implicit constraints

When to Avoid

  • When identity matters (use Entity instead)
  • When mutability is genuinely required
  • When the concept is truly a single primitive with no validation needs

Pseudocode

class Money:
    amount: decimal (immutable)
    currency: string (immutable)

    function equals(other):
        return amount == other.amount AND currency == other.currency

    function add(other):
        assert currency == other.currency
        return new Money(amount + other.amount, currency)

Python Implementation

from dataclasses import dataclass


@dataclass(frozen=True)
class Money:
    amount: float
    currency: str

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError(f"Amount cannot be negative: {self.amount}")
        if len(self.currency) != 3:
            raise ValueError(f"Currency must be 3-letter code: {self.currency}")

    def add(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError(f"Cannot add {self.currency} and {other.currency}")
        return Money(round(self.amount + other.amount, 2), self.currency)

    def subtract(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError(f"Cannot subtract {self.currency} and {other.currency}")
        result = round(self.amount - other.amount, 2)
        if result < 0:
            raise ValueError("Result would be negative")
        return Money(result, self.currency)

    def multiply(self, factor: float) -> "Money":
        return Money(round(self.amount * factor, 2), self.currency)

    def __str__(self):
        return f"{self.currency} {self.amount:.2f}"


@dataclass(frozen=True)
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address or "." not in self.address.split("@")[-1]:
            raise ValueError(f"Invalid email: {self.address}")
        # Normalize to lowercase
        object.__setattr__(self, "address", self.address.lower())

    def domain(self) -> str:
        return self.address.split("@")[1]

    def __str__(self):
        return self.address


# --- Demo ---
print("=== Money value object ===")
price = Money(29.99, "USD")
tax = Money(2.40, "USD")
total = price.add(tax)
print(f"  {price} + {tax} = {total}")

discounted = total.multiply(0.9)
print(f"  10% discount: {discounted}")

# Equality by value
a = Money(10.00, "USD")
b = Money(10.00, "USD")
c = Money(10.00, "EUR")
print(f"  Money(10, USD) == Money(10, USD)? {a == b}")
print(f"  Money(10, USD) == Money(10, EUR)? {a == c}")

# Can be used in sets/dicts (hashable because frozen)
prices = {Money(9.99, "USD"), Money(9.99, "USD"), Money(19.99, "USD")}
print(f"  Unique prices: {len(prices)}")

print()
print("=== Email value object ===")
email = Email("Alice@Example.COM")
print(f"  Normalized: {email}")
print(f"  Domain: {email.domain()}")
print(f"  Equal to lowercase? {email == Email('alice@example.com')}")

print()
print("=== Validation ===")
try:
    Money(-5, "USD")
except ValueError as e:
    print(f"  Negative amount: {e}")

try:
    Money(10, "USD").add(Money(5, "EUR"))
except ValueError as e:
    print(f"  Mixed currency: {e}")

try:
    Email("not-an-email")
except ValueError as e:
    print(f"  Bad email: {e}")

Output:

=== Money value object ===
  USD 29.99 + USD 2.40 = USD 32.39
  10% discount: USD 29.15
  Money(10, USD) == Money(10, USD)? True
  Money(10, USD) == Money(10, EUR)? False
  Unique prices: 2

=== Email value object ===
  Normalized: alice@example.com
  Domain: example.com
  Equal to lowercase? True

=== Validation ===
  Negative amount: Amount cannot be negative: -5
  Mixed currency: Cannot add USD and EUR
  Bad email: Invalid email: not-an-email

Go Implementation

package main

import (
	"fmt"
	"math"
	"strings"
)

type Money struct {
	Amount   float64
	Currency string
}

func NewMoney(amount float64, currency string) (Money, error) {
	if amount < 0 {
		return Money{}, fmt.Errorf("amount cannot be negative: %f", amount)
	}
	if len(currency) != 3 {
		return Money{}, fmt.Errorf("currency must be 3-letter code: %s", currency)
	}
	return Money{Amount: math.Round(amount*100) / 100, Currency: currency}, nil
}

func (m Money) Add(other Money) (Money, error) {
	if m.Currency != other.Currency {
		return Money{}, fmt.Errorf("cannot add %s and %s", m.Currency, other.Currency)
	}
	return NewMoney(m.Amount+other.Amount, m.Currency)
}

func (m Money) Multiply(factor float64) Money {
	result, _ := NewMoney(m.Amount*factor, m.Currency)
	return result
}

func (m Money) Equal(other Money) bool {
	return m.Amount == other.Amount && m.Currency == other.Currency
}

func (m Money) String() string {
	return fmt.Sprintf("%s %.2f", m.Currency, m.Amount)
}

type Email struct {
	Address string
}

func NewEmail(address string) (Email, error) {
	lower := strings.ToLower(address)
	parts := strings.SplitN(lower, "@", 2)
	if len(parts) != 2 || !strings.Contains(parts[1], ".") {
		return Email{}, fmt.Errorf("invalid email: %s", address)
	}
	return Email{Address: lower}, nil
}

func main() {
	fmt.Println("=== Money value object ===")
	price, _ := NewMoney(29.99, "USD")
	tax, _ := NewMoney(2.40, "USD")
	total, _ := price.Add(tax)
	fmt.Printf("  %s + %s = %s\n", price, tax, total)

	discounted := total.Multiply(0.9)
	fmt.Printf("  10%% discount: %s\n", discounted)

	a, _ := NewMoney(10.00, "USD")
	b, _ := NewMoney(10.00, "USD")
	fmt.Printf("  Equal? %v\n", a.Equal(b))

	fmt.Println()
	fmt.Println("=== Email value object ===")
	email, _ := NewEmail("Alice@Example.COM")
	fmt.Printf("  Normalized: %s\n", email.Address)

	fmt.Println()
	fmt.Println("=== Validation ===")
	_, err := NewMoney(-5, "USD")
	fmt.Printf("  Negative: %v\n", err)

	_, err = NewEmail("not-an-email")
	fmt.Printf("  Bad email: %v\n", err)
}

JavaScript Implementation

class Money {
  #amount;
  #currency;

  constructor(amount, currency) {
    if (amount < 0) throw new Error(`Amount cannot be negative: ${amount}`);
    if (currency.length !== 3) throw new Error(`Currency must be 3-letter code: ${currency}`);
    this.#amount = Math.round(amount * 100) / 100;
    this.#currency = currency;
    Object.freeze(this);
  }

  get amount() { return this.#amount; }
  get currency() { return this.#currency; }

  add(other) {
    if (this.#currency !== other.currency) {
      throw new Error(`Cannot add ${this.#currency} and ${other.currency}`);
    }
    return new Money(this.#amount + other.amount, this.#currency);
  }

  multiply(factor) {
    return new Money(this.#amount * factor, this.#currency);
  }

  equals(other) {
    return this.#amount === other.amount && this.#currency === other.currency;
  }

  toString() {
    return `${this.#currency} ${this.#amount.toFixed(2)}`;
  }
}

// --- Demo ---
console.log("=== Money value object ===");
const price = new Money(29.99, "USD");
const tax = new Money(2.40, "USD");
const total = price.add(tax);
console.log(`  ${price} + ${tax} = ${total}`);
console.log(`  10% discount: ${total.multiply(0.9)}`);

const a = new Money(10, "USD");
const b = new Money(10, "USD");
console.log(`  Equal? ${a.equals(b)}`);

console.log("\n=== Validation ===");
try { new Money(-5, "USD"); } catch (e) { console.log(`  Negative: ${e.message}`); }
try { new Money(10, "USD").add(new Money(5, "EUR")); } catch (e) { console.log(`  Mixed: ${e.message}`); }
  • Immutability — Value Objects must be immutable. Immutability is the key property that makes Value Objects safe to share.
  • Flyweight — When the same Value Object instance is shared across many places, Flyweight manages that sharing efficiently.
  • Guard Clause — Value Object constructors use Guard Clause to validate and reject invalid values at creation time.
  • Aggregate — Value Objects live inside Aggregates (as properties of entities). They are part of the Aggregate but are not entities.
  • DTO — A DTO transfers data; a Value Object models a domain concept. A Value Object can be a field type inside a DTO.