Refactoring techniqueMoving Features between Objects
Move Method
Move a method to the class whose data it uses most, and leave a delegating call or update the callers.
- Also known as
- Move Function
Problem
A method lives in one class but reads mostly another class's fields. Every change to that other class's data ripples back to it, and the class it lives in has to know far more than it should.
Solution
Move the method to the class whose data it uses, passing in whatever it still needs from the old class. Then either delete the original or keep it as a one-line delegate.
Before
class BillingService {
constructor(private mailer: Mailer) {}
lateFee(invoice: Invoice, today: Date): number {
if (invoice.paidAt) return 0;
const daysLate = daysBetween(invoice.dueDate, today);
if (daysLate <= invoice.terms.graceDays) return 0;
const fee = invoice.balance * invoice.terms.dailyLateRate * daysLate;
return Math.min(fee, invoice.terms.maxLateFee);
}
sendReminder(invoice: Invoice, today: Date): void {
const fee = this.lateFee(invoice, today);
this.mailer.send(invoice.customerEmail, `Balance ${invoice.balance}, late fee ${fee}`);
}
}After
class Invoice {
// ...existing fields: paidAt, dueDate, balance, terms, customerEmail
lateFee(today: Date): number {
if (this.paidAt) return 0;
const daysLate = daysBetween(this.dueDate, today);
if (daysLate <= this.terms.graceDays) return 0;
const fee = this.balance * this.terms.dailyLateRate * daysLate;
return Math.min(fee, this.terms.maxLateFee);
}
}
class BillingService {
constructor(private mailer: Mailer) {}
sendReminder(invoice: Invoice, today: Date): void {
const fee = invoice.lateFee(today);
this.mailer.send(invoice.customerEmail, `Balance ${invoice.balance}, late fee ${fee}`);
}
}Why
- The fee rule sits next to the data it depends on. Changing
termsand the fee logic happens in one class. BillingServicestops reaching into invoice internals, which reduces coupling.- Other callers (the dashboard, the export job) can ask the invoice directly.
Steps
- Look at everything the method uses: fields, other methods, parameters. Decide whether related members should move with it.
- Check subclasses and superclasses of the source for overrides or declarations. Polymorphic methods need extra care.
- Declare the method in the target class. Copy the body and rewrite references: target fields become
this.x; source members it still needs become parameters (or pass the source object). - Compile the target. Make the original method delegate to the new one. Run the tests.
- Replace callers one by one to call the target directly, or keep the delegate if many callers exist. Run the tests.
- Delete the delegate when it has no callers.
Watch out
- Don't move a method that uses the source class's data as much as the target's. Split it with Extract Method first and move only the envious part.
- Moving behavior onto a plain data type (a DTO, a generated API model, a database row type) can be impossible or wrong. Those types may be regenerated or shared across services.
- Watch for overrides. If subclasses override the method, all variants must move together or you break dispatch.
- There is no dedicated inverse; you move it back the same way. If a whole class empties out, consider Inline Class.
Related
- Fixes Feature Envy, Shotgun Surgery, Inappropriate Intimacy, Data Class and Message Chains.
- Often paired with Move Field.
- Extract Class is a series of Move Field and Move Method steps.
Agent checklist
- The method uses more of the target's data than the source's.
- Overrides and interface declarations in the source hierarchy were checked.
- Source members still needed are passed as parameters, not reached through new back-references.
- All callers updated or delegating; the delegate is removed when unused.
- Tests pass after each step.
Treats these smells
- Feature EnvyA method reads and computes on another object's data more than its own. Move the logic to where the data lives.
- Shotgun SurgeryOne logical change forces small edits in many classes, so one missed edit ships an inconsistent system.
- Divergent ChangeOne class gets edited for many unrelated reasons, so every kind of change lands in the same file and collides with every other kind.
- Inappropriate IntimacyTwo classes reach into each other's private fields and internals. Cut the back-and-forth by moving members or narrowing what each exposes.
- Message ChainsClient code walks a chain like a.getB().getC().getD() to reach what it needs, coupling itself to every link in the path.
- Switch StatementsThe same switch or if-chain over a type code is repeated across the codebase, so adding one case means editing every copy.
- Parallel Inheritance HierarchiesEvery new subclass in one hierarchy forces a matching subclass in another, so two trees must always grow in lockstep.
- Data ClassA class holds fields and accessors but no behavior, while other classes reach in to do the work that belongs to it.
See also
- Move FieldMove a field to the class that uses it most, updating every reader and writer to reach it there.
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
- Inline ClassFold a class that does too little into the class that uses it, moving its fields and methods across and deleting it.
- Hide DelegateAdd a method on the server object that forwards to its delegate, so clients stop navigating through it to reach the delegate.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.