d9e5668cec
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
9.0 KiB
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}`); }
Related Patterns
- 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.