Change Reference to Value (and back)
Intent
Section titled “Intent”A small object can be modelled two ways. As a reference, one shared instance is held by many owners; updating it updates everyone’s view, and identity (which object) matters. As a value, the object is immutable, equal to any other with the same contents, and copied freely. Change Reference to Value turns a shared, mutable object into an immutable value compared by its fields. The reverse — Change Value to Reference — applies when many places really must share and observe a single, updatable entity.
The smell
Section titled “The smell”The reference-when-it-should-be-value smell shows up as aliasing bugs: two parts of the program hold the same little object, one mutates it, and the other is surprised by the change it never asked for. A Money, a Coordinate, a DateRange — these have no meaningful identity, so sharing them only invites action at a distance. The opposite smell is divergent copies: you have many separate instances of something that is conceptually one entity (a single Customer record), and an update to one copy fails to reach the others.
Before → After
Section titled “Before → After”A Money object shared by reference, where mutating it leaks into every alias. After, it is an immutable value: operations return new instances and equality compares contents.
// Before — mutable, shared by referenceclass Money { constructor(public amount: number, public currency: string) {} add(other: Money): void { this.amount += other.amount; // mutates in place, surprising aliases }}
// After — immutable value: operations return new instancesclass Money { constructor( readonly amount: number, readonly currency: string, ) {} add(other: Money): Money { return new Money(this.amount + other.amount, this.currency); } equals(other: Money): boolean { return this.amount === other.amount && this.currency === other.currency; }}# Before — mutable, shared by referenceclass Money: def __init__(self, amount, currency): self.amount = amount self.currency = currency def add(self, other): self.amount += other.amount # mutates in place
# After — frozen value: equality and hashing come from contentsfrom dataclasses import dataclass
@dataclass(frozen=True)class Money: amount: int currency: str def add(self, other): return Money(self.amount + other.amount, self.currency)// Before — pointer methods mutate a shared instancetype Money struct { Amount int Currency string}func (m *Money) Add(other Money) { m.Amount += other.Amount // mutates through the pointer}
// After — value semantics: pass and return by value, never mutatetype Money struct { Amount int Currency string}func (m Money) Add(other Money) Money { return Money{Amount: m.Amount + other.Amount, Currency: m.Currency}}// Comparable structs compare by content with ==.// Before — shared and mutated through a referencestruct Money { amount: i64, currency: String,}impl Money { fn add(&mut self, other: &Money) { self.amount += other.amount; // mutates in place }}
// After — a Copy/Clone value compared by contents#[derive(Clone, PartialEq, Eq)]struct Money { amount: i64, currency: String,}impl Money { fn add(&self, other: &Money) -> Money { Money { amount: self.amount + other.amount, currency: self.currency.clone() } }}Mechanics
Section titled “Mechanics”To change a reference into a value:
- Make the object’s fields immutable —
readonly,frozen, value receivers, no&mut. - Replace any mutating method with one that returns a new instance carrying the changed contents.
- Provide content-based equality (
equals,__eq__/frozen,==on a comparable struct,PartialEq), so two objects with the same fields are equal. - Remove sharing: let each owner hold its own copy, since copies are now indistinguishable.
- Run your tests after each step.
To change a value into a reference (the reverse), introduce a single source — a repository or registry that hands out the instance for a given identity — so every caller fetches the same object and an update is seen by all.
When to use / trade-offs
Section titled “When to use / trade-offs”Prefer a value for small, self-contained concepts — money, dates, points, ranges — where two equal contents should be interchangeable and nobody needs to observe in-place changes. Immutable values are safe to share across threads, easy to reason about, and free of aliasing surprises; their cost is allocating a new instance per “change,” which is negligible for small objects.
Choose a reference when the object models a single real-world entity that several parts of the system must share and watch as it updates — a logged-in user, an account whose balance many views reflect. Here, divergent copies would be a correctness bug. The decisive question: if I change this object, should everyone holding it see the change? Yes means reference; no — and equal-contents-are-the-same — means value.