Skip to content
aipatterns.dev

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

TypeScript
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

TypeScript
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 terms and the fee logic happens in one class.
  • BillingService stops reaching into invoice internals, which reduces coupling.
  • Other callers (the dashboard, the export job) can ask the invoice directly.

Steps

  1. Look at everything the method uses: fields, other methods, parameters. Decide whether related members should move with it.
  2. Check subclasses and superclasses of the source for overrides or declarations. Polymorphic methods need extra care.
  3. 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).
  4. Compile the target. Make the original method delegate to the new one. Run the tests.
  5. Replace callers one by one to call the target directly, or keep the delegate if many callers exist. Run the tests.
  6. 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.

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

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.