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 does one thing when the reader can describe what it does in a single sentence that does not contain "and" or "then."
  • 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. Most long methods are not complicated problems; they are easy problems whose author never paused to give the steps names.

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>")

render_report now reads as a four-step script. The details are one level below, in the same order they were mentioned above.

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 it out loud. If you say "and" or "then," there are at least two things.
  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 each line as level L0 (caller-language) or L1 (detail). Mixed = extract.
  3. Extract each L1 run into a named function directly below the caller.
  4. Reread the top-level function; it should read like a plan, not a procedure.

Read This Only If Stuck