Skip to content
aipatterns.dev

Refactoring techniqueComposing Methods

Replace Method with Method Object

Turn a long method with tangled local variables into its own class, so the locals become fields and the body can be split freely.

Also known as
Replace Function with Command

Problem

A long method uses many local variables that each fragment reads and writes. Extract Method would need helpers with six parameters and several return values, so nobody splits it.

Solution

Create a class for the method. Make the original object, the parameters and every local variable fields of that class, and move the body into a single run() method. Fragments can now be extracted into private methods that share state through fields instead of parameters.

Before

TypeScript
class PayrollService {
  constructor(private taxBands: TaxBand[]) {}

  netPay(employee: Employee, period: PayPeriod): number {
    let gross = employee.hourlyRate * period.regularHours;
    let overtime = 0;
    if (period.overtimeHours > 0) {
      overtime = employee.hourlyRate * 1.5 * period.overtimeHours;
      gross += overtime;
    }
    let taxable = gross - employee.pensionRate * gross;
    let tax = 0;
    for (const band of this.taxBands) {
      const inBand = Math.max(0, Math.min(taxable, band.upTo) - band.from);
      tax += inBand * band.rate;
    }
    const deductions = employee.benefits.reduce((s, b) => s + b.costPerPeriod, 0);
    return gross - tax - employee.pensionRate * gross - deductions;
  }
}

After

TypeScript
class PayrollService {
  constructor(private taxBands: TaxBand[]) {}

  netPay(employee: Employee, period: PayPeriod): number {
    return new NetPayCalculation(this.taxBands, employee, period).run();
  }
}

class NetPayCalculation {
  private gross = 0;

  constructor(
    private readonly taxBands: TaxBand[],
    private readonly employee: Employee,
    private readonly period: PayPeriod,
  ) {}

  run(): number {
    this.gross = this.regularPay() + this.overtimePay();
    return this.gross - this.incomeTax() - this.pension() - this.benefitDeductions();
  }

  private regularPay() { return this.employee.hourlyRate * this.period.regularHours; }
  private overtimePay() { return this.employee.hourlyRate * 1.5 * this.period.overtimeHours; }
  private pension() { return this.employee.pensionRate * this.gross; }

  private incomeTax(): number {
    const taxable = this.gross - this.pension();
    return this.taxBands.reduce(
      (tax, b) => tax + Math.max(0, Math.min(taxable, b.upTo) - b.from) * b.rate,
      0,
    );
  }

  private benefitDeductions(): number {
    return this.employee.benefits.reduce((s, b) => s + b.costPerPeriod, 0);
  }
}

Why

  • Fields replace parameter lists between helpers, so the method splits along natural seams.
  • The calculation is testable without the rest of PayrollService.
  • The service shrinks to an entry point; the algorithm has a home it can grow in.

Steps

  1. Create a new class named after the method's job.
  2. Give it a constructor that takes the original object (if the body uses it) and every parameter. Store each as a readonly field.
  3. Declare a field for each local variable the body uses.
  4. Add a run() method and copy the body into it. Replace parameter and local references with fields, and calls on this with calls on the stored original object.
  5. Replace the original method body with return new X(...).run(). Run the tests.
  6. Now apply Extract Method inside the new class, one fragment at a time, running tests after each.

Watch out

  • Don't use this when plain Extract Method with Replace Temp with Query works. A new class is heavier.
  • The method object is single-use. Don't cache it or call run() twice; mutable fields make a second run unreliable.
  • If the original method touched private members of its class, you must expose them or pass them in. Exposing too much creates Inappropriate Intimacy.
  • In languages with closures, a nested function sharing locals can achieve the same split with less ceremony.
  • To undo, inline run() back into the caller and delete the class (Inline Class).

Agent checklist

  • Plain extraction was tried first and failed because of shared locals.
  • The new class receives everything through its constructor and exposes only run().
  • The original method delegates in one line and its signature is unchanged.
  • Extracted helpers inside the new class are small and named.
  • 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.