Skip to main content

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 to order.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:

  1. Describe its contract and effects. Check whether splitting would clarify decisions or merely spread one invariant across functions.
  2. Scan for mixed levels: domain-level lines next to primitive manipulation are a tell.
  3. Extract the lower level into a well-named function whose body lives below the caller.
  4. Re-read the top-level function. It should now describe intent without distraction.
  5. Stop when further extraction makes the name more confusing than the body.

Check Yourself​

  1. Why is "line count" a symptom but not the rule?
  2. What does the stepdown rule say about where sub-functions live?
  3. When is extracting a function actively harmful?

Mini Drill or Application​

Pick a function in your code over 30 lines. Do all four:

  1. Describe it out loud and count the "ands."
  2. Mark caller-language and detail, then select one candidate boundary; mixed levels are evidence to discuss, not an automatic command.
  3. Compare traces, exceptions, mutation, iterator consumption and output order before accepting an extraction.
  4. Reread the top-level function; it should read like a plan, not a procedure.

Read This Only If Stuck​

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.