Code smellBloaters
Long Method
A function that does several jobs in a row and has grown too long to read, name or test as one unit.
- Also known as
- God Function
Signs
- You have to scroll to see the whole function, or you need to keep more than a handful of local variables in your head at once.
- Blank lines and comments split the body into "paragraphs" such as
// validate,// compute totals,// send email. - The name is vague (
process,handle,run) because no specific name covers everything it does. - Tests for it need large fixtures, and each test asserts on many unrelated outputs.
- Diffs touch the same function for unrelated reasons: a tax change and a logging change land in the same 150 lines.
Why it happens
Adding a line to an existing function always feels cheaper than creating a new one. Each change is small and reasonable, so nobody notices the function crossing the line from "a step" to "a workflow". Local variables make it worse: once five temporaries are shared across the body, pulling out a piece means deciding what to pass in and out, so people keep appending instead.
Example
An order-placement handler that validates, prices, persists and notifies in one block.
async function placeOrder(req: OrderRequest, db: Db, mailer: Mailer): Promise<string> {
// validate
if (!req.items.length) throw new Error("empty order");
for (const item of req.items) {
if (item.qty <= 0) throw new Error(`bad qty for ${item.sku}`);
}
// price
let subtotal = 0;
for (const item of req.items) {
const product = await db.products.get(item.sku);
subtotal += product.priceCents * item.qty;
}
let discount = 0;
if (req.coupon === "SPRING10") discount = Math.round(subtotal * 0.1);
const tax = Math.round((subtotal - discount) * 0.08);
const total = subtotal - discount + tax;
// persist
const id = await db.orders.insert({ ...req, subtotal, discount, tax, total });
// notify
await mailer.send(req.email, "Order received", `Order ${id}: $${(total / 100).toFixed(2)}`);
return id;
}After extracting each paragraph into a named function, the top-level function reads as a table of contents.
async function placeOrder(req: OrderRequest, db: Db, mailer: Mailer): Promise<string> {
assertValid(req);
const price = await priceOrder(req, db.products);
const id = await db.orders.insert({ ...req, ...price });
await sendConfirmation(mailer, req.email, id, price.total);
return id;
}
function assertValid(req: OrderRequest): void {
if (!req.items.length) throw new Error("empty order");
const bad = req.items.find((i) => i.qty <= 0);
if (bad) throw new Error(`bad qty for ${bad.sku}`);
}
async function priceOrder(req: OrderRequest, products: ProductRepo): Promise<Price> {
const subtotal = await subtotalOf(req.items, products);
const discount = couponDiscount(req.coupon, subtotal);
const tax = Math.round((subtotal - discount) * TAX_RATE);
return { subtotal, discount, tax, total: subtotal - discount + tax };
}Treatment
- If the body has comment-headed paragraphs, use Extract Method on each one and name it after the comment.
- If temporaries block extraction, first apply Replace Temp with Query so the extracted code can recompute the value.
- If many parameters must flow into the extracted pieces, group them with Introduce Parameter Object or Preserve Whole Object.
- If locals are so entangled that no clean cut exists, use Replace Method with Method Object: the locals become fields and every step becomes a method.
- If conditionals dominate, apply Decompose Conditional to give each condition and branch a name.
- If a loop does one self-contained job, extract the loop into its own function.
Payoff
- Each function has one name that tells the truth, so callers and reviewers read intent, not mechanics.
- Small functions are testable in isolation and reusable elsewhere.
- Duplicate fragments become visible once pieces have names.
- Changes land in one small function, so diffs and merge conflicts shrink.
When to ignore
- The function is long but flat and linear, such as a configuration table, a route list or a generated mapping. Splitting it adds jumps without adding meaning.
- A hot loop where a profiler shows that call overhead matters and the compiler does not inline. This is rare; measure first.
- Test functions that set up one scenario end to end. Arrange-act-assert in one place can be clearer than helper soup.
Agent checklist
- Tests cover the function before you split it; add characterization tests if they don't.
- Each extracted function has a name specific enough that the comment above the old paragraph can be deleted.
- Extracted functions take the data they need as arguments and do not reach back into the caller's locals through closures or shared mutable state.
- The original function now reads as a short sequence of steps at one level of abstraction.
- Behavior is unchanged: the same tests pass with no edits to their assertions.
Treatments
- 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.
- Introduce Parameter ObjectReplace a group of parameters that always travel together with one object type, then move behavior on that group into it.
- Preserve Whole ObjectPass the object itself instead of several values pulled out of it, so the method signature stays stable as needs change.
- 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.
- Decompose ConditionalMove a complex condition and each of its branches into well-named functions so the decision reads as intent instead of arithmetic.
See also
- Large ClassA class with too many fields, methods or responsibilities, so every change risks breaking something unrelated inside it.
- CommentsComments that explain what unclear code does are a sign the code should be renamed or restructured so it explains itself.
- 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.
- Switch StatementsThe same switch or if-chain over a type code is repeated across the codebase, so adding one case means editing every copy.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.