4.5.19 · D2Software Engineering

Visual walkthrough — Refactoring — code smells, common refactorings (extract method, rename, etc.)

1,952 words9 min readBack to topic

Before we touch any code, let us agree on two words that will follow us through every picture.

We will refactor this exact function from the parent note, from absolute zero:

def print_invoice(order):
    print("=== Invoice ===")
    total = 0
    for item in order.items:
        total += item.price * item.qty      # tax-free subtotal
    total *= 1.0825                          # add 8.25% tax
    print(f"Total: ${total:.2f}")

Our destination is the clean compute_total + print_invoice pair. But we will earn it move by move.


Step 1 — Nail down behaviour with a test (the safety net)

WHAT. Before changing anything, we write down one concrete input and the exact output the current code produces. That written-down pair is our net.

WHY this tool and not another? We cannot prove "same output" by staring — humans miss flipped signs and reordered lines. A test is a machine that re-checks the rulebook after every move, so a mistake screams instantly instead of hiding. This is the unit test doing its one job: pinning behaviour.

PICTURE. Below, an order of two items [($10, qty 2), ($5, qty 3)] goes into the sealed box. The subtotal is , then tax multiplies by , giving the fixed target.

The green frame is our net. If any future step makes $37.89 change to anything else, the net catches it — red — and we undo.


Step 2 — See the smell: two ideas trapped in one box

WHAT. We look at the function and mark, by colour, the two different jobs it is secretly doing.

WHY. A code smell called Long Method is exactly this: one function juggling separable ideas. Seeing the seam is what tells us where to cut. This is the single-responsibility idea in miniature — one box, one job.

PICTURE. The blue block is presentation (the two print lines — deciding how things look). The orange block is calculation (the loop + tax — deciding the number). They are tangled together in one box, yet they answer completely different questions.


Step 3 — Kill the magic number first (smallest safe move)

WHAT. Replace the bare literal 1.0825 with a named constant TAX_RATE = 0.0825, and write the arithmetic as subtotal * (1 + TAX_RATE).

WHY. 1.0825 is a Magic Number — a value whose meaning is invisible. Naming it makes the meaning explicit and gives us one source of truth (see DRY). We do the smallest move first because small moves are easy to check and easy to undo.

PICTURE. Watch the arithmetic transform without the number changing value: . Same factor, clearer story.

Run the test (Step 1's net). Still $37.89. Green. Keep it, commit.


Step 4 — Extract the orange block into its own box

WHAT. Cut the orange calculation block out of print_invoice, paste it into a brand-new function, and leave behind a call to that new function.

WHY this is the core move. This is Extract Method. By moving a self-contained idea into its own named box, the reader of print_invoice no longer has to reconstruct the tax logic — they just read the name. The new box also becomes independently testable and reusable. We pick Extract Method (not, say, deleting the loop) because the goal is to relocate logic unchanged, not alter it.

PICTURE. The orange block detaches and floats into a new sealed sub-box. Crucially, an arrow now runs from print_invoice down into it and a value flows back — the wiring, not the logic, is what moved.

TAX_RATE = 0.0825
 
def compute_total(order):                       # the new orange box
    subtotal = sum(item.price * item.qty for item in order.items)
    return subtotal * (1 + TAX_RATE)

Run the test. $37.89. Green. Commit.


Step 5 — Rewire the caller and read the result

WHAT. print_invoice now just prints, and asks compute_total(order) for the number.

WHY. With the orange box extracted, print_invoice finally does one job (present) and delegates the other (calculate). This is the payoff: each box is short, named, and single-purpose.

PICTURE. Two clean boxes side by side. Blue box (print_invoice) points to orange box (compute_total); the number 37.89 travels up the arrow. Compare to Step 2's single tangled box — same behaviour, untangled shape.

def print_invoice(order):
    print("=== Invoice ===")
    print(f"Total: ${compute_total(order):.2f}")

Run the test. $37.89. Green. Commit. Done.


Step 6 — The edge cases (what if the box gets weird inputs?)

A derivation isn't finished until every input is shown. Extract Method claims behaviour is preserved for all inputs, so we must check the strange ones.

WHAT & WHY & PICTURE — three degenerate orders:

The figure lines up all three edge inputs and shows the before-box output equals the after-box output for each. Because Extract Method only relocated the arithmetic (Step 4) and the magic-number rewrite was an algebraic identity (Step 3), the outputs must match on every input — including these degenerate ones.


The one-picture summary

One image, the whole journey: on the left, the tangled single box (Long Method + Magic Number); an arrow labelled behaviour-preserving crosses to the right, where two clean boxes stand — print_invoice (blue, presents) calling compute_total (orange, calculates) — with the green test net stretched underneath the entire transformation, holding the fixed output $37.89 steady from start to finish.

Recall Feynman: tell the whole walkthrough to a 12-year-old

Imagine a vending machine that works fine but has a wild mess of wires inside. First we tape a note on it: "Put in this order → get exactly $37.89" — that note is our test, our promise about what the machine does. Then we look inside and notice two totally different jobs tangled together: one part shows the receipt, the other adds up the money. We first give a name to a mysterious dial that just said 1.0825 — we relabel it "price plus 8.25% tax," same setting, clearer label. Next we carefully lift the whole add-up-the-money section out and put it in its own little labelled box called compute_total, running a wire so the receipt part can still ask it "hey, what's the total?" After every single move we feed the machine the test order and check \$37.89 still comes out — it always does, because we never changed what the wires compute, only where they live. Finally we test the weird cases too: an empty order, a free item, zero of something — all still give the same answers as before. The machine looks brand-new inside and behaves exactly the same outside. That is refactoring.

Recall Quick self-check

Why did we pin a test BEFORE moving anything? ::: The test is our proxy for behaviour; it's the only way to prove each move kept the output identical — and that proof is the definition of refactoring. In Step 3, why is behaviour guaranteed unchanged? ::: Because is an exact algebraic identity — same value, only spelled with a name. In Step 4 (Extract Method), why can't the output move? ::: The arithmetic was relocated, not rewritten — moving a computation to a new address never changes what it computes. Why do the empty / zero-price / zero-qty cases all give the same before-and-after output? ::: Steps 3 and 4 only relabelled and relocated identical arithmetic, so every input — including degenerate ones — maps to the identical result.

Related ideas to explore next: Clean Code, Naming Conventions, Cyclomatic Complexity, Technical Debt, Design Patterns.