Refactoring techniqueComposing Methods
Extract Method
Move a coherent fragment of a long function into its own well-named function and call it from the original place.
- Also known as
- Extract Function
Problem
A function does several things in sequence, and you have to read every line to learn what each block is for. Comments mark the blocks ("// compute tax", "// format lines"), which is a sign the blocks want names of their own.
Solution
Move the fragment into a new function whose name says what it does, and replace the fragment with a call. Variables the fragment reads become parameters. A single variable it writes and the caller still needs becomes the return value.
Before
function renderInvoice(invoice: Invoice): string {
let out = `Invoice ${invoice.number}\n`;
out += `Customer: ${invoice.customer.name}\n\n`;
// line items
let subtotal = 0;
for (const line of invoice.lines) {
const amount = line.quantity * line.unitPrice;
subtotal += amount;
out += `${line.sku.padEnd(12)} ${line.quantity} x ${line.unitPrice} = ${amount}\n`;
}
// tax and total
const tax = Math.round(subtotal * invoice.taxRate);
out += `\nSubtotal: ${subtotal}\nTax: ${tax}\nTotal: ${subtotal + tax}\n`;
return out;
}After
function renderInvoice(invoice: Invoice): string {
const subtotal = lineTotal(invoice.lines);
return (
renderHeader(invoice) +
renderLines(invoice.lines) +
renderTotals(subtotal, invoice.taxRate)
);
}
function renderHeader(invoice: Invoice): string {
return `Invoice ${invoice.number}\nCustomer: ${invoice.customer.name}\n\n`;
}
function renderLines(lines: InvoiceLine[]): string {
return lines
.map((l) => `${l.sku.padEnd(12)} ${l.quantity} x ${l.unitPrice} = ${l.quantity * l.unitPrice}\n`)
.join("");
}
function lineTotal(lines: InvoiceLine[]): number {
return lines.reduce((sum, l) => sum + l.quantity * l.unitPrice, 0);
}
function renderTotals(subtotal: number, taxRate: number): string {
const tax = Math.round(subtotal * taxRate);
return `\nSubtotal: ${subtotal}\nTax: ${tax}\nTotal: ${subtotal + tax}\n`;
}Why
- The top-level function now reads as a summary. Readers drill into a helper only when they need its detail.
- Each helper is testable alone, and
lineTotalcan be reused by the PDF renderer and the API serializer. - Names replace comments, and names stay accurate because the compiler sees them while it ignores comments.
Steps
- Pick a fragment that does one thing you can name. If you cannot name it, pick a smaller or different fragment.
- Create an empty function with that name. Name it for what it does, not how.
- Copy the fragment into it. List every variable the fragment reads from the outer scope; make each one a parameter.
- Find variables the fragment assigns. If exactly one is used afterward, return it. If several are, extract a smaller fragment first, or use Replace Temp with Query.
- Replace the original fragment with a call. Compile and run the tests.
- Look for other copies of the same fragment and replace them with the call too.
Watch out
- Don't extract when the name you would give is no clearer than the code itself (
addOne(x)forx + 1). The indirection costs more than it saves. - A fragment that writes many locals resists extraction. Use Replace Method with Method Object instead of returning a tuple of five values.
- Watch for early
return,breakorcontinueinside the fragment. They change meaning once moved; convert them to a returned flag or value first. - In hot loops, a function call is usually free after inlining by the JIT or compiler, but measure before arguing either way.
- The inverse is Inline Method. Use it when a helper's body is as clear as its name.
Related
- Fixes Long Method, Duplicate Code, Comments and Feature Envy (extract, then Move Method).
- Inverse: Inline Method.
- Extract Variable is the lighter option for a single expression.
- Template Method often starts as a series of extracted steps.
Agent checklist
- The new function has a name that states intent, and the fragment had no clearer name already.
- Every outer variable the fragment reads is now a parameter; no hidden shared mutable state was introduced.
- At most one value is returned; early exits were handled before moving.
- Other duplicates of the fragment now call the new function.
- Tests pass unchanged after the extraction.
Treats these smells
- Long MethodA function that does several jobs in a row and has grown too long to read, name or test as one unit.
- Duplicate CodeThe same logic appears in two or more places, so every fix and change must be made several times and the copies drift apart.
- CommentsComments that explain what unclear code does are a sign the code should be renamed or restructured so it explains itself.
- Feature EnvyA method reads and computes on another object's data more than its own. Move the logic to where the data lives.
- Switch StatementsThe same switch or if-chain over a type code is repeated across the codebase, so adding one case means editing every copy.
- Data ClumpsThe same group of variables travels together through fields and parameters, signaling a missing object that should own them.
See also
- Inline MethodReplace calls to a function whose body is as clear as its name with the body itself, then delete the function.
- 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.
- Replace Method with Method ObjectTurn a long method with tangled local variables into its own class, so the locals become fields and the body can be split freely.
- Extract VariableName a hard-to-read expression or sub-expression by assigning it to a well-named local variable.
- Template MethodFix an algorithm's sequence of steps in a base class and let subclasses fill in or override individual steps without changing the order.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.