Move Field
Intent
Section titled “Intent”A field lives on one record but is consulted and updated mostly through another. Move it to the record where it is actually used, so the data sits beside the behaviour that depends on it and the two records stop reaching across an artificial boundary.
The smell
Section titled “The smell”Data structures are the skeleton of a program, and a misplaced field warps everything built on it. The tell-tale signs: a field on record A that is always passed together with record B, a field that B’s methods read and write while A merely stores it, or two records that must be updated in lockstep to keep one value consistent. Each is a hint that the field’s true owner is B.
Before → After
Section titled “Before → After”A discountRate stored on Customer but governed entirely by the customer’s PricingPlan. Before, the rate sits on the customer and the plan reaches over to compute prices. After, the rate lives on the plan, where its rules already are.
// Beforeclass PricingPlan { constructor(public name: string) {}
finalPrice(base: number, discountRate: number): number { return base * (1 - discountRate); }}
class Customer { constructor( public name: string, public plan: PricingPlan, public discountRate: number, ) {}
quote(base: number): number { return this.plan.finalPrice(base, this.discountRate); }}
// Afterclass PricingPlan { constructor(public name: string, public discountRate: number) {}
finalPrice(base: number): number { return base * (1 - this.discountRate); }}
class Customer { constructor(public name: string, public plan: PricingPlan) {}
quote(base: number): number { return this.plan.finalPrice(base); }}# Beforeclass PricingPlan: def __init__(self, name): self.name = name
def final_price(self, base, discount_rate): return base * (1 - discount_rate)
class Customer: def __init__(self, name, plan, discount_rate): self.name = name self.plan = plan self.discount_rate = discount_rate
def quote(self, base): return self.plan.final_price(base, self.discount_rate)
# Afterclass PricingPlan: def __init__(self, name, discount_rate): self.name = name self.discount_rate = discount_rate
def final_price(self, base): return base * (1 - self.discount_rate)
class Customer: def __init__(self, name, plan): self.name = name self.plan = plan
def quote(self, base): return self.plan.final_price(base)// Beforetype PricingPlan struct { Name string}
func (p PricingPlan) FinalPrice(base, discountRate float64) float64 { return base * (1 - discountRate)}
type Customer struct { Name string Plan PricingPlan DiscountRate float64}
func (c Customer) Quote(base float64) float64 { return c.Plan.FinalPrice(base, c.DiscountRate)}
// Aftertype PricingPlan struct { Name string DiscountRate float64}
func (p PricingPlan) FinalPrice(base float64) float64 { return base * (1 - p.DiscountRate)}
type Customer struct { Name string Plan PricingPlan}
func (c Customer) Quote(base float64) float64 { return c.Plan.FinalPrice(base)}// Beforestruct PricingPlan { name: String,}
impl PricingPlan { fn final_price(&self, base: f64, discount_rate: f64) -> f64 { base * (1.0 - discount_rate) }}
struct Customer { name: String, plan: PricingPlan, discount_rate: f64,}
impl Customer { fn quote(&self, base: f64) -> f64 { self.plan.final_price(base, self.discount_rate) }}
// Afterstruct PricingPlan { name: String, discount_rate: f64,}
impl PricingPlan { fn final_price(&self, base: f64) -> f64 { base * (1.0 - self.discount_rate) }}
struct Customer { name: String, plan: PricingPlan,}
impl Customer { fn quote(&self, base: f64) -> f64 { self.plan.final_price(base) }}flowchart LR
subgraph Before["Before"]
A["Customer<br/>name<br/>discountRate"]
B["PricingPlan"]
A -.->|"plan logic reads<br/>discountRate"| B
end
subgraph After["After"]
C["Customer<br/>name<br/>plan"]
D["PricingPlan<br/>discountRate"]
C --> D
end
Before -.->|"Move Field"| After Mechanics
Section titled “Mechanics”- If the field is public, encapsulate it behind accessors first so every read and write goes through a single point. This makes the later move a one-place change.
- Add the field to the target record, along with an accessor there.
- Decide how the target reaches the value: it may compute it, store it on construction, or accept it as a parameter.
- Redirect each accessor on the source to delegate to the target’s copy of the field.
- Run your tests. Behaviour must be unchanged at this checkpoint.
- Remove the field from the source once nothing reads it locally any more, and drop the parameters that only existed to carry it.
- Run your tests again. A green suite confirms the value now lives in one place.
When to use / trade-offs
Section titled “When to use / trade-offs”Move a field when another record’s methods use it more than its current owner does, when the field is always passed alongside another object, or when keeping two copies in sync is a recurring source of bugs.
The cost is touching every caller that read the field directly — which is exactly why encapsulating first pays off. There is no separate named inverse: if later changes make the original record the heavier user, you simply Move Field back.