Extract Function
Extract to give a meaningful concept a name, not to reduce a line count. The two motivations produce different code, and only one of them helps.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
When does pulling a block of code into its own function make the code easier to change, and when does it just move the problem behind a name?
A reviewer says a 60-line function is too long and asks for it to be split. The author asks what it should be split into, and nobody has an answer beyond "smaller pieces".
Long functions are hard to read, so split them. Take the 60-line function, cut it into three 20-line functions, and the lint warning goes away along with the problem.
Splitting by length produces functions named for their position rather than their meaning — processOrderPart2, handleRequestHelper — and a reader now has to hold three names that mean nothing plus the original algorithm (Naming).
- Splitting by length produces functions named for their position rather than their meaning —
processOrderPart2,handleRequestHelper— and a reader now has to hold three names that mean nothing plus the original algorithm (Naming). - It scatters a sequence. Six steps in one place read top to bottom; the same six steps behind three calls read as three jumps, and the reader has to reassemble the order themselves (Local Reasoning).
- The extracted pieces usually need most of the local state, so they acquire five- and six-parameter signatures — trading a long function for a long parameter list, which is the more expensive problem (Long Parameter List).
- Worst, extracting a block that two call sites happen to share creates a dependency between them. When one caller's needs change, the shared function grows a flag, and the flag is the beginning of a coupling nobody chose (Boolean Parameters).
- And the underlying claim is false as stated. Function length is not what makes code hard to change; the number of reasons the function has to change is, and a 60-line function with one reason is easier to work with than three 20-line functions with three (Long Functions).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- The function is correct and covered by tests; there is no defect motivating the change.
- The team has a lint warning at 25 lines, which is where the review comment came from (What to Automate Out of Review).
- The 60 lines are one coherent algorithm with six named steps, not six unrelated things.
- A name must be true. An extracted function whose name does not fully describe what it does is worse than the inline block, because now the reader is misled instead of merely inconvenienced (Naming).
- Extraction preserves behaviour exactly, including the order of side effects.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- An extracted function owns one named concept — something a person in the domain would recognise as a thing (Ubiquitous Language).
- The calling function owns the sequence and the decisions; after a good extraction it reads as a summary of what happens.
- The name owns the whole contract, including side effects.
calculateTotalthat also writes to the database has a name that lies.
- The seam is a concept boundary, not a length boundary. If you cannot name what comes out, there is no boundary there.
- The parameter list is the signal. A clean extraction takes few arguments because the concept is genuinely separable; an extraction that needs seven is telling you the seam is in the wrong place (Introduce Parameter Object).
- Extraction inside one function is cheap and local. Extraction into a shared helper crosses a different boundary entirely, because it creates a dependency between callers (Duplicate Knowledge).
Two extractions of the same function
Both versions satisfy the lint rule. One of them left the code easier to change and the other left it slightly worse, and the difference is entirely in whether the pieces have names that mean something in the domain.
function priceOrder(order: Order): Money {
const a = priceOrderPart1(order)
const b = priceOrderPart2(order, a)
return priceOrderPart3(order, a, b)
}
function priceOrderPart2(
order: Order, subtotal: Money,
): Money { ... }
// To know what happens, you open all three.
// The parameters carry the sequence that the
// original code carried by being in order.function priceOrder(order: Order): Money {
const subtotal = sumLineItems(order.items)
const discount = bestApplicableDiscount(order)
const taxable = subtotal.minus(discount)
return taxable.plus(vatFor(order.shipTo, taxable))
}
// Each name is a thing the finance team says out
// loud. The function now reads as the definition
// of what an order costs.The right-hand version is not better because it is shorter — both are four lines. It is better because each name is a concept that exists in the business, which means a change to "how discounts are chosen" has an obvious address, and a reader who only needs to know the order of operations never has to open anything. The left-hand version has the same line count and none of that: the names carry no information, so every question requires opening every function, and the parameter chaining has made the sequence explicit in the worst possible place (Naming and Domain Language).
The extraction that makes coupling worse
Extraction is usually described as neutral or positive because behaviour does not change. That is true of a private helper. It is not true of a shared one, which converts two independent pieces of code into two dependents of a single definition.
This is the case worth pricing explicitly, because it is invisible at both call sites and it does not become expensive until the two callers' requirements diverge — which is exactly when nobody is looking at the extraction that caused it.
Usernames must now allow two characters. Product tags must not.
The duplication is visible and slightly annoying. The change touches one module, because the two blocks were never connected — they merely looked alike.
The shared function now needs a flag or a split. Whichever is chosen, the author of the username change has to understand tag validation and search indexing to make a change to account policy — and nothing in users/ says so.
When the parameter list tells you the seam is wrong
The most reliable signal that an extraction is in the wrong place is its signature. A concept that is genuinely separable needs little context; one that needs six pieces of local state was not separable, and the parameters are the tangle made visible.
The response is not to bundle the parameters into an object to hide the count. It is to notice that the state and the behaviour belong together, which usually means an object or a module rather than a function (Extract Module).
1// the extraction that says "wrong seam"2function applyAdjustments(3 subtotal: Money, customer: Customer, region: Region,4 promos: Promo[], now: Date, isRetry: boolean,5): Money { ... }6 7// six parameters, one of them a boolean flag,8// one of them a clock. The block was not separable.9 10// what it was telling you11class OrderPricing {12 constructor(private readonly clock: Clock,13 private readonly promos: PromoCatalog) {}14 15 adjust(subtotal: Money, order: Order): Money { ... }16}The boolean is the loudest signal — isRetry means the function does two things and the caller picks which (Boolean Parameters). The now: Date says the block depends on time and nobody decided where that dependency lives (Time as a Dependency). Six parameters was not a style problem to be fixed by bundling them; it was the code declining to be a function.
How to build it
Most important first.
- Extract when you can name it. "Validate the shipping address", "convert to the settlement currency", "decide whether this order needs manual review" — if a name in the domain's vocabulary fits, there is a concept there.
- Extract to replace a comment. A block preceded by
// work out the delivery windowis a function calleddeliveryWindowwaiting to happen, and the comment stops being able to go stale (Comments). - Extract when the block has a different level of abstraction from the code around it. A loop doing bit arithmetic in the middle of a business workflow is a candidate regardless of its length.
- Extract to make something testable that could not be reached before — but notice that this is a design finding, not just a convenience (Testing as Design Feedback).
- Do not extract to shorten. If the only reason is a number, the extraction will be named after its position, and the code gets worse (Long Functions).
- Be much more careful extracting a *shared* function than a private one. Two blocks that look alike may encode different knowledge, and merging them creates coupling that is invisible in both call sites (DRY: Knowledge, Not Lines).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- A well-named private extraction: the next change to that concept costs one edit in a place the name makes findable. This is a small, real, cheap win.
- A length-motivated extraction: the next change costs the same edits as before plus the navigation between three functions, and the reader must reconstruct the sequence each time. Slightly worse, permanently.
- A shared extraction across two callers with diverging needs: the next change costs an edit *plus* checking the other caller, and the change after that adds a flag. This is the case that gets steadily more expensive (Change Amplification).
- What does not get cheaper by extracting anything: if the 60 lines have one reason to change, they cost one edit before and one edit after. Extraction bought readability, which is real, and bought no change locality at all (The Cost of Change).
- Every extraction adds a jump. For a reader following a sequence once, indirection is a pure cost, and a design of many small functions is genuinely harder to read linearly than one long one.
- Naming is hard and takes time, and a bad name is worse than no name at all — which means the cheap version of this refactoring is the harmful one.
- Extractions are easy to make and hard to reverse socially: once a function exists and has three callers, inlining it back feels like an attack on whoever created it.
What can go wrong
- Names that describe position rather than meaning, after which the reader must open each one to know anything.
- Six-parameter extracted functions, which have traded one problem for a costlier one (Long Parameter List).
- The shared helper with a boolean: two callers, one function, one flag, and a permanent coupling between two features that have nothing to do with each other (Boolean Parameters).
- Extraction of the wrong slice, so the concept is split across the caller and the callee and neither can be understood alone (Over-Decomposition).
- The mitigation fails: a team bans extraction below some size, and the genuine concept extractions stop happening too.
- A private extracted function creates no new dependency — it is a local naming change and is close to free.
- A shared extracted function creates a dependency from every caller onto one definition, and from that definition onto the union of all callers' requirements. That is the whole cost, and it is usually not counted.
- Extraction depends on the block not being tangled with control flow. A block containing an early return from the enclosing function does not extract cleanly, and forcing it produces flag-and-check code that is worse than the original.
- "So functions should be short." Short is a frequent side effect of one-concept-per-function, not the goal. A 60-line function implementing one algorithm with no separable concepts inside it is fine (Long Functions).
- "Extract every repeated block." Repetition of *lines* is not repetition of *knowledge*, and merging two blocks that merely look alike creates a coupling that is invisible at both call sites (DRY: Knowledge, Not Lines).
- "Extraction is free because behaviour does not change." Behaviour does not change; coupling can, and a shared extraction is a structural commitment disguised as a tidy-up.
- "If it is hard to name, extract it anyway and name it later." A name you cannot find usually means there is no concept there. The difficulty is information (Naming).
- long-parameter-list
- duplicate-knowledge
Testing it, and how it ages
- A behaviour-preserving extraction must not require a single test to change. If the tests moved, something else moved with them (What Refactoring Actually Is).
- Resist writing a test for every newly extracted private function. Testing the caller still covers them, and a test per private helper welds the tests to the structure you are about to keep changing (What a Unit Is).
- For a shared extraction, add a test for the union of what the callers need — that suite is now the contract between them, and it is the only place that coupling is visible.
- Good extractions accumulate into a vocabulary. After a dozen, the calling function reads as a description of the process, which is when the technique has actually paid off (Naming and Domain Language).
- Shared extractions age worse than private ones. The usual trajectory is one shared helper, then a flag, then two flags, then a split back into two functions — and the split back is the correct move, made years late (Duplicate Knowledge).
- What forces a rethink: an extracted concept that starts needing state between calls is telling you it wants to be a module rather than a function (Extract Module).
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALNaming a concept is a property of how people read code rather than of any language; what differs is the syntactic cost of an extraction, which is near-zero for a local closure and higher where a helper must become a top-level declaration or a class member.
- PARADIGM-SPECIFICIn a functional idiom, extracting a pure function is close to free and composes cleanly, so the bar for doing it is genuinely lower. In an OO codebase the same extraction often needs several fields as arguments, which is a signal that the concept wants to be an object rather than a function — the identical refactoring produces a different next step.
- CONTESTEDThe strongest opposing view is that many small functions are harder to read than one long one, because reading requires jumping and holding a stack of names, and that this cost is systematically underweighted by people who have already memorised the codebase. Practitioners who hold this position write long, linear, well-commented functions and defend them well. The distinction that survives the argument is whether each extracted piece is a concept a domain expert would recognise: extractions that name real concepts are widely accepted, extractions that name positions are what the objection is actually about.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Programming Languages & Runtime Internals — how cheap an extraction is depends on whether closures, inlining and zero-cost abstraction are available, which is why the same advice carries different weight in different runtimes.