Skip to content
aipatterns.dev

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

TypeScript
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

TypeScript
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 lineTotal can 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

  1. Pick a fragment that does one thing you can name. If you cannot name it, pick a smaller or different fragment.
  2. Create an empty function with that name. Name it for what it does, not how.
  3. Copy the fragment into it. List every variable the fragment reads from the outer scope; make each one a parameter.
  4. 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.
  5. Replace the original fragment with a call. Compile and run the tests.
  6. 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) for x + 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, break or continue inside 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.

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

See also

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