Functions: Size, Purity & a Single Level of Abstraction

Clean Code, SOLID & Refactoring

Chapter 3 · Functions: Size, Purity & a Single Level of Abstraction

Three properties of a function that turn out to be genuinely measurable, not matters of taste: how much it depends on and changes shared state (purity), and whether every line inside it operates at the same conceptual altitude (a single level of abstraction). Both connect directly to bugs this site has already verified elsewhere — this chapter reproduces one of them on purpose.

Purity: Why an Impure Function Reproduces a Bug You've Already Seen

# IMPURE — mutates shared state inventory = {'PROD-1': 100} def sell_item_impure(product_id, qty): inventory[product_id] -= qty return inventory[product_id] # PURE — takes everything as input, touches nothing outside itself def sell_item_pure(current_stock, qty): return current_stock - qty
Verified directly — the impure version reproduces Design Patterns' own test-pollution bug
Two "independent" tests: sell 30 units from a stock of 100 (expect 70), then sell 50 units from a fresh stock of 100 (expect 50). Against sell_item_impure, test 1 correctly returns 70 — but test 2 returns 20, not 50, because it silently operated on the 30-units-already-sold state test 1 left behind. This is exactly Design Patterns Chapter 2's own SingletonCounter finding, reproduced here at the plain-function level: shared mutable state makes correctness depend on call order.
Verified directly — the pure version is immune, by construction
The identical two calls against sell_item_pure(100, 30) and sell_item_pure(100, 50), in the identical order: both correctly return 70 and 50. Neither call could see anything left behind by the other, because neither call reads or writes anything outside its own parameters and return value.
Purity as a testability property, not a purity contest
A pure function's own result is fully determined by its own arguments — nothing else needs to be set up, reset, or reasoned about between calls. This is the same property Software Architecture Fundamentals Chapter 7 verified paying off at a larger scale (~18,111× faster testing, because a fake adapter could stand in cleanly): purity is what makes standing in for something else — or simply calling a function twice — safe.

A Single Level of Abstraction, Measured

A function mixing "what should happen" (business steps) with "how exactly it happens" (raw arithmetic, string formatting, file I/O) forces a reader to switch mental altitude line by line.

# MIXED — business logic and low-level detail tangled together def process_order_mixed(order): total = 0 for item in order['items']: total += item['price'] * item['qty'] if order['customer_tier'] == 'gold': total *= 0.8 log_entry = f"Order {order['id']}: total=${total:.2f}" with open('orders.log', 'a') as f: f.write(log_entry + '\n') send_confirmation_email(order['customer_email'], total) return total # SINGLE LEVEL — the top function reads as one consistent list of business steps def process_order_clean(order): total = calculate_order_total(order) log_order(order, total) send_confirmation_email(order['customer_email'], total) return total
Verified directly — the mixed version tangles 5 low-level operations with 1 high-level call
Scanning process_order_mixed's own body for raw operators, string formatting, and file I/O versus named business-function calls: 5 low-level operations (arithmetic, an f-string, a file write) sit alongside just 1 high-level call. A reader has to switch between "what does this business rule mean" and "how exactly does string formatting work" within the same six lines.
Verified directly — the single-level version has zero low-level operations at the top
process_order_clean's own body: 3 high-level calls, 0 low-level operations. Every line reads at the identical altitude — "calculate the total, log it, email it" — with all the raw detail pushed down into the three functions that own it individually.
Verified directly — both versions compute the identical result
For a gold-tier order totaling $100: both process_order_mixed and process_order_clean return 80.0. Separating the levels of abstraction changed nothing about what the function computes.

Where This Connects

This chapter's findingWhat it connects to
Impurity reproducing a real test-order-dependent bugDesign Patterns Chapter 2's own SingletonCounter — the identical failure mode, verified twice in two different courses
A single-level function's own testability payoffSoftware Architecture Fundamentals Chapter 7's own ~18,111× fake-adapter speedup — purity is the property that makes swapping in a fake safe in the first place
process_order_mixed's own tangled responsibilitiesChapter 4's own "Bloaters" code smells — a function this tangled is a direct instance of the long-method smell that chapter names and catalogs

Hands-On Exercises

Exercise 1

Write a third "test" against this chapter's own sell_item_impure — selling 10 units from a fresh stock of 50 (expect 40) — run immediately after this chapter's own two tests, with no reset in between. Verify what it actually returns and explain why, tracing the state left behind by each prior call.

📄 View solution
Exercise 2

Add a fourth business step to process_order_clean — a call to a new award_loyalty_points(order, total) function — following the same pattern as the existing three calls. Verify the top-level function still has 0 low-level operations after the addition.

📄 View solution
Exercise 3

Using this chapter's own two verified findings, explain why calculate_order_total, log_order, and send_confirmation_email (the three functions process_order_clean delegates to) are each individually easier to write as pure functions than process_order_mixed ever could be — what property of mixing abstraction levels makes purity harder to achieve?

📄 View solution

Chapter 3 Quick Reference

  • Purity: a function whose result depends only on its own arguments — verified: an impure function reproduced a real test-pollution bug (20 instead of 50); the pure equivalent was immune by construction
  • Single level of abstraction: every line in a function operating at the same conceptual altitude — verified: a mixed function tangled 5 low-level operations with 1 high-level call; a single-level version had 3 high-level calls and 0 low-level operations, computing the identical result
  • Next chapter: Code Smells I: Bloaters — naming and catalogging exactly the kind of tangled function this chapter's own process_order_mixed already demonstrated