open atlas
↑ Back to track
Code patterns & craft CP · 08 · 01

Long method, large class

Bloater smells — the long method and the large class — are cohesion decay: responsibilities pile up until no one can hold the unit in their head. Detect by signals, cure by extracting along responsibility seams, not at arbitrary line counts.

CP Senior ◷ 19 min
Level
FoundationsJuniorMiddleSenior

You open handleCheckout to add a coupon code. It is 240 lines. You scroll. There is validation, then inventory checks, then tax math, then a payment call, then an email, then analytics, then a retry loop wrapped around something you can’t see from where the loop opens. To find the one place a coupon should slot in, you have to load the entire method into your head — and you can’t, because it does not fit. So you read it three times, place your change where it seems safe, and hope.

That method works. It has worked for two years. And every change to it costs a day, because the unit of code you must understand to change one thing is the whole thing. That is a bloater: a method or class that has accumulated so many responsibilities that no one can hold it in their head — and the cost of every change scales with the size you have to comprehend.

Goal

After this lesson you can name the detection signals that flag a long method or large class before they become unmaintainable; apply extract method and extract class driven by responsibilities rather than line counts; turn a long handler into a short narrative of named steps; and recognise the failure mode where you shatter a bloater into trivia without grouping it — moving complexity instead of reducing it.

1

Bloaters are cohesion decay, not a size problem. “Cohesion” is how strongly the parts of a unit belong together — how single its purpose is. A long method or large class is what low cohesion looks like once it has run for a while: every new requirement got bolted onto the nearest existing place instead of its own, so unrelated concerns now share one scope. The length is the symptom; the disease is that several responsibilities are tangled into one unit.

That reframing matters because it tells you where to cut. If the problem were size, you would split at the midpoint. Because the problem is mixed responsibilities, you split where responsibilities change — even if that leaves one piece at 80 lines and another at 8. The seam, not the line count, is the target.

2

Learn the detection signals — they fire before the code is unmanageable. You do not need a line threshold; you need to notice the smell early. The reliable signals:

// Signal 1: you scroll to read one logical unit (it doesn't fit on a screen)
// Signal 2: "and" hides in the name → two jobs in one
function validateAndPersistAndNotify(order: Order) { /* ... */ }

// Signal 3: a class hoards instance fields that few methods share
class OrderService {
  private db; private mailer; private taxRates; private inventory;
  private pdfRenderer; private analytics; private retryPolicy;
  // 7 collaborators → it is at least 7 things
}

// Signal 4: deep nesting — blocks 4+ levels in mean hidden sub-procedures
if (a) { for (const x of xs) { if (b) { try { /* ... */ } catch {} } } }

A name with “and”, a method you must scroll, a class with many loosely-shared fields, and arrow-shaped nesting are not style nits. Each says a smaller, named unit is hiding in here — the field clusters and the nested blocks are literally pointing at the seams.

3

Extract method: give every block a name, and the handler becomes a story. The cure for a long method is to replace each chunk-with-a-purpose with a call to a well-named function. The senior move is that the names form a readable summary — after extraction the top-level method should read like a table of contents, not like an implementation.

// before: one wall, you read line-by-line to learn what it does
function checkout(cart: Cart, user: User) {
  // ...40 lines of validation...
  // ...30 lines of pricing...
  // ...25 lines of payment...
  // ...20 lines of fulfilment...
}

// after: the body is the narrative; detail moves down a level
function checkout(cart: Cart, user: User) {
  validate(cart, user);
  const price = priceOf(cart, user);
  const charge = chargePayment(user, price);
  fulfil(cart, charge);
}

You haven’t deleted complexity — validate still does the work. But you have layered it: the reader who wants the shape reads four lines; the reader chasing a bug in pricing opens priceOf and ignores the rest. Comprehension cost now scales with the question being asked, not with the whole method.

4

Extract class: when fields cluster, a second object is asking to exist. A large class is usually two or three classes wearing one name. The tell is in Step 2’s signal 3: if half the fields and the methods that touch them concern tax, and the other half concern email, those are two cohesive subsets sharing a scope by accident. Extract the cluster into its own class and let the original delegate.

// before: OrderService knows tax math AND email formatting AND analytics
class OrderService { /* taxRates, mailer, templates, analytics, ...  */ }

// after: each responsibility is its own cohesive unit
class TaxCalculator { taxFor(amount: number, region: Region): number { /* ... */ } }
class OrderMailer   { sendConfirmation(order: Order): void { /* ... */ } }

class OrderService {
  constructor(private tax: TaxCalculator, private mailer: OrderMailer) {}
  place(order: Order) {
    const total = this.tax.taxFor(order.subtotal, order.region) + order.subtotal;
    this.mailer.sendConfirmation({ ...order, total });
  }
}

Now a tax-rule change touches TaxCalculator and nothing else; the email team works in OrderMailer without reading tax code. The blast radius of each likely change collapsed to one cohesive unit — which is the whole economic point of breaking the bloater up.

5

The failure mode: shattering, not extracting. The amateur reading of “split long methods” is make everything short. So a 200-line method becomes twenty one-line methods — step1, step2, doThing, doThingHelper — and now you trace twenty hops to follow one flow, with no conceptual level you can stop at. You moved the complexity into the call graph; you did not reduce it. Worse, fragments that share no responsibility now masquerade as siblings, which actively misleads the next reader.

The discipline that prevents this: every extracted unit must name a responsibility a domain person would recognise — validate, priceOf, chargePayment — not a position in the original file. If you cannot give the chunk an honest, single-purpose name, you have found a piece of code that does not yet correspond to one idea, and slicing it just hides that. Cut along seams that mean something. When no real seam exists, leaving a cohesive 60-line method intact beats fragmenting it into noise.

Worked example

A long handler becomes a short narrative of named steps. Here is a registration handler that grew one responsibility at a time:

async function register(req: Request): Promise<Response> {
  const body = await req.json();
  if (!body.email || !body.email.includes("@")) return bad("email");
  if (!body.password || body.password.length < 8) return bad("password");
  const existing = await db.users.findByEmail(body.email);
  if (existing) return conflict("email taken");

  const hash = await bcrypt.hash(body.password, 12);
  const user = await db.users.insert({ email: body.email, hash });

  const token = jwt.sign({ sub: user.id }, SECRET, { expiresIn: "15m" });
  const link = `${BASE_URL}/verify?token=${token}`;
  await mailer.send(user.email, "Verify your email",
    `Welcome! Confirm here: ${link}`);

  analytics.track("signup", { userId: user.id, source: req.query.utm });
  return created({ id: user.id });
}

Four responsibilities share one scope: validate input, create the account, send verification, record analytics. The detection signals are all here — you scroll to read it, the implicit name is “validate and create and email and track”, and the nesting hides early-return logic. Extract along those four seams:

async function register(req: Request): Promise<Response> {
  const input = await parseAndValidate(req);            // validation seam
  if (!input.ok) return bad(input.field);

  const user = await createAccount(input.email, input.password); // account seam
  if (!user.ok) return conflict("email taken");

  await sendVerificationEmail(user.value);              // notification seam
  trackSignup(user.value.id, req.query.utm);            // analytics seam
  return created({ id: user.value.id });
}

The handler is now a five-line narrative; each name is a responsibility a product person would recognise. A change to the password policy lives entirely in parseAndValidate; a change to the email copy lives in sendVerificationEmail. Note what we did not do: we did not extract req.json() into getBody() or created(...) into respond201(). Those are not responsibilities — they are one-liners. Extracting them would be the shattering failure mode: more hops, no clearer meaning.

Why this works

Why responsibilities and not lines? Because the line count is a lagging, arbitrary proxy and the responsibility is the actual cause. Two methods of identical length can be opposite smells: a 90-line state machine that does one thing with many cases may be perfectly cohesive and should be left whole, while a 30-line method that validates, saves, and emails is three things and should be cut. If you tune to a line threshold, you will split cohesive methods (creating the shattering failure mode) and leave short-but-tangled ones alone (missing the real smell). The threshold is a smoke alarm telling you to look — the decision to cut, and where, is always “how many responsibilities live here, and where do they change?”

Common mistake

The most common way to “fix” a bloater wrong is to trade one 200-line method for twenty one-liners with no conceptual grouping. It feels like progress — every method is now short, the linter is happy — but you measured the wrong thing. You optimised for small units when the goal was cohesive units you can reason about at a level. The reader now bounces through step1 → step2 → helperA → helperB to follow a single flow, with no resting altitude where the whole operation is summarised. You did not reduce the complexity; you smeared it across the call graph and added the cost of twenty new names. The fix is honest naming: if an extracted piece can’t take a single-purpose name from the domain, it isn’t a seam — don’t cut there.

Check yourself
Quiz

A reviewer refactors a 180-line method into 18 ten-line methods named step1…step18, each called once in order. Every method is short and the line-length linter passes. Is the bloater cured?

Recap

The long method and large class are bloater smells, and a bloater is cohesion decay: responsibilities accumulate in one unit until no one can hold it in their head, and the cost of every change scales with the size you must comprehend. Detect them by signals that fire early — scrolling to read one unit, “and” in the name, many loosely-shared instance fields, deep nesting — and cure them with extract method and extract class driven by responsibilities. A good extraction turns a long handler into a short narrative of named steps, each delegating a level down, so comprehension cost scales with the question, not the whole. The failure mode that looks like success is shattering — trading one 200-line method for twenty one-liners with no conceptual grouping — which moves complexity into the call graph instead of reducing it. The rule that prevents it: cut along seams that name a real responsibility, never at arbitrary line counts, and leave a cohesive method whole when no honest seam exists.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.