Move Function
Intent
Section titled “Intent”Take a function that, when you read it, talks mostly about another object — its fields, its methods, its rules — and move the function over to that object. The new home owns the data the function needs, so the function gets simpler and the coupling between the two classes drops.
The smell
Section titled “The smell”This is the cure for Feature Envy: a method that seems more interested in a class other than the one it lives in. The tell is the parameter list and the body. If a function takes a plan argument and then reads plan.tier, plan.dailyRate, and plan.discount while barely touching its own object, it envies plan. Move it there, and the reaches-across calls turn into plain field access.
Before → After
Section titled “Before → After”An overdraft charge that lives on Account but is computed almost entirely from the account’s plan. Before, the function reaches into the plan for everything. After, it lives on the plan, where the data already is.
// Beforeclass AccountPlan { constructor(public tier: string, public dailyRate: number) {}}
class Account { constructor(private plan: AccountPlan, private daysOverdrawn: number) {}
overdraftCharge(): number { if (this.plan.tier === "premium") { let base = 10; if (this.daysOverdrawn > 7) { base += (this.daysOverdrawn - 7) * this.plan.dailyRate * 0.85; } return base; } return this.daysOverdrawn * this.plan.dailyRate; }}
// Afterclass AccountPlan { constructor(public tier: string, public dailyRate: number) {}
overdraftCharge(daysOverdrawn: number): number { if (this.tier === "premium") { let base = 10; if (daysOverdrawn > 7) { base += (daysOverdrawn - 7) * this.dailyRate * 0.85; } return base; } return daysOverdrawn * this.dailyRate; }}
class Account { constructor(private plan: AccountPlan, private daysOverdrawn: number) {}
overdraftCharge(): number { return this.plan.overdraftCharge(this.daysOverdrawn); }}# Beforeclass AccountPlan: def __init__(self, tier, daily_rate): self.tier = tier self.daily_rate = daily_rate
class Account: def __init__(self, plan, days_overdrawn): self.plan = plan self.days_overdrawn = days_overdrawn
def overdraft_charge(self): if self.plan.tier == "premium": base = 10 if self.days_overdrawn > 7: base += (self.days_overdrawn - 7) * self.plan.daily_rate * 0.85 return base return self.days_overdrawn * self.plan.daily_rate
# Afterclass AccountPlan: def __init__(self, tier, daily_rate): self.tier = tier self.daily_rate = daily_rate
def overdraft_charge(self, days_overdrawn): if self.tier == "premium": base = 10 if days_overdrawn > 7: base += (days_overdrawn - 7) * self.daily_rate * 0.85 return base return days_overdrawn * self.daily_rate
class Account: def __init__(self, plan, days_overdrawn): self.plan = plan self.days_overdrawn = days_overdrawn
def overdraft_charge(self): return self.plan.overdraft_charge(self.days_overdrawn)// Beforetype AccountPlan struct { Tier string DailyRate float64}
type Account struct { Plan AccountPlan DaysOverdrawn int}
func (a Account) OverdraftCharge() float64 { if a.Plan.Tier == "premium" { base := 10.0 if a.DaysOverdrawn > 7 { base += float64(a.DaysOverdrawn-7) * a.Plan.DailyRate * 0.85 } return base } return float64(a.DaysOverdrawn) * a.Plan.DailyRate}
// Aftertype AccountPlan struct { Tier string DailyRate float64}
func (p AccountPlan) OverdraftCharge(daysOverdrawn int) float64 { if p.Tier == "premium" { base := 10.0 if daysOverdrawn > 7 { base += float64(daysOverdrawn-7) * p.DailyRate * 0.85 } return base } return float64(daysOverdrawn) * p.DailyRate}
type Account struct { Plan AccountPlan DaysOverdrawn int}
func (a Account) OverdraftCharge() float64 { return a.Plan.OverdraftCharge(a.DaysOverdrawn)}// Beforestruct AccountPlan { tier: String, daily_rate: f64,}
struct Account { plan: AccountPlan, days_overdrawn: i64,}
impl Account { fn overdraft_charge(&self) -> f64 { if self.plan.tier == "premium" { let mut base = 10.0; if self.days_overdrawn > 7 { base += (self.days_overdrawn - 7) as f64 * self.plan.daily_rate * 0.85; } base } else { self.days_overdrawn as f64 * self.plan.daily_rate } }}
// Afterstruct AccountPlan { tier: String, daily_rate: f64,}
impl AccountPlan { fn overdraft_charge(&self, days_overdrawn: i64) -> f64 { if self.tier == "premium" { let mut base = 10.0; if days_overdrawn > 7 { base += (days_overdrawn - 7) as f64 * self.daily_rate * 0.85; } base } else { days_overdrawn as f64 * self.daily_rate } }}
struct Account { plan: AccountPlan, days_overdrawn: i64,}
impl Account { fn overdraft_charge(&self) -> f64 { self.plan.overdraft_charge(self.days_overdrawn) }}flowchart LR
subgraph Before["Before"]
A["Account"]
A --> B["overdraftCharge()<br/>reads plan.tier<br/>reads plan.dailyRate"]
C["AccountPlan"]
end
subgraph After["After"]
D["Account"]
E["AccountPlan"]
E --> F["overdraftCharge()<br/>reads own tier<br/>reads own dailyRate"]
D -.->|"delegates"| F
end
Before -.->|"Move Function"| After Mechanics
Section titled “Mechanics”- Examine everything the function uses in its current home and confirm most of it belongs to the target class. List the few things that still come from the source.
- Check whether those source-side elements should travel with the function or be passed in as parameters.
- Copy the function into the target class and adjust the body to use the target’s own fields directly.
- Compile and resolve any references — the target may need a new parameter for the leftover source data.
- Turn the original function into a thin delegator that calls the new home, or replace its callers directly.
- Run your tests. Behaviour must be unchanged.
- Once every caller goes through the new location, remove the delegator if it no longer earns its place.
When to use / trade-offs
Section titled “When to use / trade-offs”Move a function when it consistently reaches across to another object, when several functions on one class would be simpler grouped on another, or when relocating it lets you delete a parameter that was only there to ferry data in.
The cost is that callers may now need a reference to the target object, and a function moved too eagerly can scatter related logic. The inverse is simply moving it back: if the function turns out to depend more on its original context after later changes, Move Function it home again.