Oh My Learning

Working with Big, Tangled Methods

legacy-code/12 senior drafted · 8 min read

Working Effectively with Legacy Code (Michael C. Feathers), Chapter 22

Prerequisites legacy-code/03 legacy-code/11 refactoring/08

Working with Big, Tangled Methods#

TL;DR#

A genuinely huge, tangled method (hundreds of lines, deeply nested, mixing many concerns, with no tests) needs its own specific strategy: sketch its structure first (find the natural "seams" within it, even before any testability seam exists), characterize it piece by piece rather than all at once, and extract in an order that progressively reduces risk — starting with the parts that are easiest to isolate and verify, not necessarily the parts that seem most important.

The idea#

Every technique in this subject so far assumed a manageable unit of code to work with. This closing lesson addresses the more intimidating, but common, extreme case: a single method so large and tangled that even applying legacy-code/03's characterization-testing technique feels daunting, because you can't easily tell what inputs would even exercise which parts of its behavior. Feathers' strategy here is less about a single named technique and more about a disciplined sequence for making progress on something that initially looks unapproachable as a whole.

How it works#

Step 1: sketch the method's internal structure, without changing anything yet#

Before attempting any test or any extraction, produce a lightweight, informal sketch (echoing legacy-code/06 and legacy-code/10's task-scoped sketching technique) of the method's actual internal structure: what are its major sections, what local variables does each section read and write, where are the branch points, which sections seem to depend on which others. This sketch is purely for your own comprehension — a numbered list of "blocks" with brief notes on their apparent purpose is often enough, and it doesn't need to be polished or kept.

Worked example — sketching, not yet changing. A 300-line process_order() method might sketch out as: "(1) lines 1-40: validate input fields; (2) lines 41-90: look up customer and apply loyalty tier; (3) lines 91-180: calculate pricing including three different discount rules; (4) lines 181-230: persist the order and related records; (5) lines 231-300: send notifications and update analytics." This sketch, built by reading through once with this specific goal, immediately suggests five natural candidate boundaries for later extraction — informed by nothing more than reading, before a single line of code or test has been written.

Step 2: identify which section is safest to isolate and characterize first#

Rather than starting with whichever section seems most important or most in need of a fix, identify the section that's easiest to isolate with the fewest dependencies on the rest of the method's state — this minimizes the risk of your first, exploratory characterization attempt, and builds both confidence and real understanding before tackling harder, more entangled sections.

Continuing the example. Section (1), input validation, likely depends only on the method's raw input parameters and produces either a clean pass-through or an early error — a strong candidate to characterize and extract first, since it has the fewest entanglements with the rest of the method's internal state. Section (3), pricing calculation, likely depends on results from sections (1) and (2) and probably has the most internal branching (the "three different discount rules") — a harder, riskier target, better attempted once you have more confidence and comprehension from successfully handling the easier sections first.

Step 3: characterize and extract one section at a time, verifying at each step#

For each identified section, in the chosen order: write characterization tests (legacy-code/03) around the whole method first (to establish a baseline, coarse-grained safety net covering the method's overall behavior), then use refactoring/05's Extract Function to pull the specific section out into its own named method, re-run the whole-method characterization tests to confirm nothing changed, and only then write more precise, targeted characterization tests specifically for the newly-extracted piece in isolation.

Why the whole-method characterization comes first. Before you've extracted anything, the safest, most reliable safety net is a test verifying the entire tangled method's current, observable input-to-output behavior (even if you don't yet understand every internal detail) — this coarse-grained safety net catches any accidental behavior change introduced by the extraction itself, even though it can't yet tell you anything precise about the individual extracted piece's behavior in isolation. Once the extraction is verified safe against this coarse net, the newly-isolated piece can get its own, more precise, targeted tests — a two-level safety net that's specifically suited to the "I don't yet fully understand this" starting condition big, tangled methods present.

Step 4: repeat, and let understanding compound#

Each successful extraction (verified safe, now separately tested) both reduces the size of the remaining tangled method and improves your comprehension of how the remaining pieces relate — informing better decisions about the order and approach for the next section. This is deliberately iterative rather than planned exhaustively upfront: per legacy-code/06's guidance against over-comprehending before acting, you don't need (and often can't have) a complete plan for fully decomposing the method before starting — each step's success informs the next step's plan.

When a section resists easy isolation — apply the fuller dependency-breaking toolkit#

Some sections, once you attempt to extract them, reveal hidden dependencies on other sections' local state that aren't immediately obvious from the initial sketch — at this point, legacy-code/05 and legacy-code/11's fuller dependency-breaking toolkit (Extract and Override Call, Replace Global Reference with Getter, and the rest) becomes directly applicable, now at the finer grain of a single extracted section rather than a whole class.

Pros#

  • Starting with a lightweight sketch and the easiest, least-entangled section builds real comprehension and confidence progressively, rather than requiring you to understand the entire method before making any progress at all.
  • The two-level safety net (coarse whole-method characterization, then precise per-section characterization) is specifically well-suited to the genuine uncertainty a big, tangled, unfamiliar method presents.
  • Each successful extraction compounds — reducing remaining complexity and improving understanding simultaneously — making the overall task progressively easier rather than uniformly hard throughout.

Cons#

  • The initial sketch, however lightweight, still takes real time and effort for a genuinely large method, and may need revision as extraction reveals hidden dependencies the sketch didn't initially capture.
  • Choosing the "safest" section to start with is itself a judgment call that can be wrong — a section that looked simple in the initial sketch can turn out, once you actually attempt extraction, to have surprising hidden entanglements.
  • This whole strategy, done properly, takes real sustained time for a genuinely large method — there's no shortcut that avoids the fundamental cost of untangling substantial, longstanding complexity, only a disciplined way to make that cost manageable and progressive rather than overwhelming.

Alternatives#

  • A full rewrite of the tangled method from scratch, informed by understanding gained from reading it — riskier per refactoring/01's rewrite-versus-refactor distinction, since a rewrite risks silently dropping behavior the original method had (bugs and edge cases included) that other code may depend on.
  • Leaving the method entirely alone if it's never actually touched — per refactoring/02's "don't refactor code you're not touching," a genuinely stable, rarely-modified tangled method that works may not be worth this investment at all.
  • A team pairing/mob-programming session for tackling a particularly gnarly tangled method — spreads the comprehension-building cost across multiple people simultaneously (echoing code-complete/12's review-catches-what-solo-work-misses argument), potentially faster than one person working through the sequence alone.

When to use it#

Apply this full sequence specifically for genuinely large, tangled methods that resist a quick, direct application of legacy-code/03's characterization technique or refactoring/05's simple Extract Function — when the method is too large and unfamiliar to know where to start without first sketching its structure.

When NOT to use it#

Don't apply this heavier, more deliberate sequence to a merely moderately-long method that a direct characterization-test-then-extract approach (legacy-code/03, refactoring/05) can already handle without needing an initial structural sketch first — reserve this lesson's fuller strategy for genuinely intimidating, large-scale cases.

Key takeaways / mental model#

For a big, tangled, unfamiliar method: sketch its structure first without changing anything, start with whatever section is easiest to isolate (not necessarily most important), build a coarse whole-method safety net before extracting anything, then extract and verify one section at a time — letting each success inform and simplify the next step, rather than trying to plan the entire decomposition upfront.

Self-check questions#

  1. Walk through sketching a large, tangled method you've encountered (or can imagine), identifying its natural candidate sections and which one you'd tackle first, and why.
  2. Explain why a coarse, whole-method characterization test should come before any extraction, even though it can't verify the extracted piece's behavior precisely on its own.
  3. Describe a case where a section that looked easy to isolate in your initial sketch turned out to have hidden entanglements once you attempted the extraction. How did you adapt?
  4. Why does this lesson argue against planning the entire decomposition of a tangled method upfront, in favor of letting each step's outcome inform the next?

References#

  • Working Effectively with Legacy Code (Michael C. Feathers), Chapter 22: "I Need to Change a Monster Method and I Can't Write Tests for It".