Functions Should Do One Thing, at One Level of Abstraction
What This Concept Is
Clean Code's first and second rules for functions:
The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that.
With two operational heuristics:
- Do one thing. A function serves a coherent contract; a sentence containing "and" or "then" can still describe one operation. Grammar is not a design test.
- One level of abstraction. A function mixes levels when some lines talk in domain language and others poke at primitives (
order.total()next toorder.items.get(0).price).
The practical shape that follows: functions that read top-down, where each line is a named operation at the same conceptual level, and lower-level details live in functions below ("the stepdown rule").
Why It Matters Here
Function-level decomposition is the smallest design decision you make hundreds of times a day. It determines whether a class is a cohesive whole or a bag of tangled procedures. Long methods can hide useful boundaries, but some algorithms are clearer as a single local calculation. Diagnose dependency and comprehension costs before extracting.
Function decomposition is also the backbone of every refactoring move in Module 2: Extract Function, Inline Function, Move Function, Replace Temp with Query. None of them work if you cannot see "one thing."
Concrete Example
Mixed levels and doing five things:
def render_report(orders, out):
total = 0
for o in orders:
for i in o.items:
total += i.price * i.qty
out.write(f"<html><body><h1>Report</h1>")
for o in orders:
out.write(f"<p>{o.id}: {o.customer}</p>")
out.write(f"<h2>Total: {total}</h2></body></html>")
Three different concerns are interleaved: totaling, HTML scaffolding, and per-order rendering. Each is at a different level of abstraction.
One thing per function, top-down:
def render_report(orders, out):
write_header(out)
write_orders(orders, out)
write_total(total_of(orders), out)
write_footer(out)
def total_of(orders):
return sum(i.price * i.qty for o in orders for i in o.items)
def write_header(out): out.write("<html><body><h1>Report</h1>")
def write_footer(out): out.write("</body></html>")
def write_orders(orders, out):
for o in orders: out.write(f"<p>{o.id}: {o.customer}</p>")
def write_total(total, out): out.write(f"<h2>Total: {total}</h2>")
The extracted sketch changes observable order: the original computes the total before any write, while the new version writes rows first. A failure during totaling now leaves partial output. Both snippets also iterate orders more than once and interpolate customer text without HTML escaping. They illustrate a tempting but unverified restructuring, not a safe completed refactor. The worked examples and lab specify a one-pass input and failure-before-output contract.
Common Confusion / Misconception
"Functions should be under N lines." Line count is a smell detector, not the rule. The real rule is "one thing at one level of abstraction." A 30-line pure function that describes one coherent calculation can be cleaner than five jagged five-liners.
A second misconception: "extracting functions always improves readability." Extraction helps when the new function can be given a name that explains itself. If the only name you can write is step2 or helper, the extraction is hiding complexity, not reducing it.
A third: "short functions mean many small files." Short functions typically live together on a class, with the caller on top and the callees below, following the stepdown rule.
How To Use It
When writing or reviewing a function, apply:
- Describe its contract and effects. Check whether splitting would clarify decisions or merely spread one invariant across functions.
- Scan for mixed levels: domain-level lines next to primitive manipulation are a tell.
- Extract the lower level into a well-named function whose body lives below the caller.
- Re-read the top-level function. It should now describe intent without distraction.
- Stop when further extraction makes the name more confusing than the body.
Check Yourself
- Why is "line count" a symptom but not the rule?
- What does the stepdown rule say about where sub-functions live?
- When is extracting a function actively harmful?
Mini Drill or Application
Pick a function in your code over 30 lines. Do all four:
- Describe it out loud and count the "ands."
- Mark caller-language and detail, then select one candidate boundary; mixed levels are evidence to discuss, not an automatic command.
- Compare traces, exceptions, mutation, iterator consumption and output order before accepting an extraction.
- Reread the top-level function; it should read like a plan, not a procedure.
Read This Only If Stuck
- Clean Code: Functions -- Small, Blocks and Indenting
- Clean Code: Do One Thing, top-to-bottom
- Clean Code: Functions -- Switch statements and arguments
- Refactoring: Extract Function (Part 1)
- Refactoring: Combine Functions into Class
Engineering extension: Trace effects before extracting
For the report example, write a trace containing iterator consumption, calculation, output calls and exceptions. Reordering pure arithmetic can become observable when attributes have effects, input is a one-shot iterator or a failure occurs mid-stream. The lab specifies materialising/validating before one output call. That policy intentionally differs from a streaming legacy renderer and uses linear memory. Try: a generator yields one valid row and then raises. Check: the lab sink receives no calls; the contract still does not promise rollback if the sink itself partially writes and fails.
Use the design contracts and practice feedback after an independent attempt.