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
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
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
- Create a new class named after the method's job.
- Give it a constructor that takes the original object (if the body uses it) and every parameter. Store each as a
readonlyfield. - Declare a field for each local variable the body uses.
- Add a
run()method and copy the body into it. Replace parameter and local references with fields, and calls onthiswith calls on the stored original object. - Replace the original method body with
return new X(...).run(). Run the tests. - 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).
Related
- Fixes Long Method and helps with Large Class.
- Structurally the same as Command, but used for readability rather than queuing or undo.
- Inverse direction: 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
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
- Replace Temp with QueryMove the expression behind a temporary variable into its own method so every method in the class can ask for the value.
- CommandWrap an action and its arguments in an object so you can queue, log, retry or undo it separately from the code that triggers it.
- 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.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.