4.5.19 · D3Software Engineering

Worked examples — Refactoring — code smells, common refactorings (extract method, rename, etc.)

3,273 words15 min readBack to topic

Before we touch code, one promise: in refactoring there is exactly one thing that must never change — the observable behaviour. We will treat that like a physicist treats a conserved quantity. Every example computes behaviour before and after and checks they are equal. That equality is the whole game.


The scenario matrix

Think of "cases" here the way a geometry student thinks of quadrants: distinct regions where the rules or the failure modes differ. Here are the region-classes a refactoring problem can land in.

Cell What makes it different Danger it hides
C1 — Clean extract A self-contained block with no side effects (almost none — the easy case)
C2 — Duplication Same logic in ≥2 places Fix one, forget the other
C3 — Degenerate input: empty Loop over zero items, empty list Off-by-one, wrong "identity" value
C4 — Boundary flip An <= vs <, an inclusive/exclusive edge Silent behaviour change at the boundary
C5 — Side-effect / ordering Code that mutates or prints Reorder → different observable output
C6 — Magic number / units Unexplained literal, unit mismatch Meaning lost, unit bug
C7 — Data clump → object Same variables travel together Long parameter list, coupling
C8 — Over-refactor (limiting) Extraction pushed too far Indirection worse than the mess
C9 — Real-world word problem Business rule buried in a mess Getting the concept name right
C10 — Exam twist "Is this a valid refactoring?" trap Behaviour secretly changed

The examples below are labelled with the cell(s) they cover. Together they fill every row.


Example 1 — Clean extract (C1)

Forecast: guess the string receipt returns before and after the refactoring. (Both should be the string "Total: 11" — note the function returns this string; it does not print it.)

  1. Identify the self-contained idea. The loop computes a subtotal — one clear concept, no printing, no mutation of the input. Why this step? Extract Method is only safe when the block is a coherent chunk; here it is (cell C1, the clean case).
  2. Name it by intent. Call it subtotal(items). Why this step? A good name (see Naming Conventions) is the documentation — subtotal says what the number means.
  3. Move the block, replace with a call:
    def subtotal(items):
        return sum(it["price"] * it["qty"] for it in items)
     
    def receipt(items):
        return f"Total: {subtotal(items)}"
    Why this step? sum(...) is the same accumulation the for loop did, just named. Behaviour is preserved by construction.

Verify: Before: , so receipt returns "Total: 11". After: subtotal(items) = 11, so receipt returns "Total: 11". Equal ✅. This is the win Extract Method exists for (see Single Responsibility Principle).


Example 2 — Duplication, Rule of Three (C2)

def quote_line(price, qty): return price * qty * 1.08 # copy 2

def invoice_line(price, qty): return price * qty * 1.08 # copy 3

Apply the **Rule of Three**: the third copy triggers the refactor.

Forecast: what single named thing should replace all three literals?

  1. Spot the repeated concept. 1.08 = "price with 8% tax." It appears 3× → the wince becomes a refactor. Why this step? DRY Principle: one idea should live in one place, or a rate change means three edits (that's the Shotgun Surgery smell).
  2. Extract a named function + constant:
    TAX_RATE = 0.08
    def with_tax(amount):
        return amount * (1 + TAX_RATE)
    Why this step? The literal 1.08 was silently encoding two facts — the tax rate (0.08) and the "add it on" arithmetic (1 +). Splitting them into a named constant TAX_RATE and a named function with_tax makes the meaning explicit and gives the rate a single home to change (killing the Magic Number smell at the same time — see Clean Code).
  3. Replace all three call sites with with_tax(price * qty). Why this step? Now changing the rate is a one-line edit — the whole point of killing duplication.

Verify: For price=10, qty=2: old 10*2*1.08 = 21.6; new with_tax(20) = 20*1.08 = 21.6. Equal ✅ across all three sites. This directly pays down Technical Debt.


Example 3 — Empty / degenerate input (C3)

Forecast: what does each version return for an empty list?

Figure — Refactoring — code smells, common refactorings (extract method, rename, etc.)
Figure: the input items = [] (violet box) fans out to the two implementations — the for-loop version (magenta) whose body is skipped so total stays 0, and the sum() version (orange) which returns the identity element 0. Both arrows rejoin at the navy "returns 0" box, showing the degenerate case is preserved.

  1. Trace the loop version on []. total = 0; the for body never runs; return 0. Why this step? The most-missed case in all of programming is "the loop that iterates zero times" (cell C3). You must trace it explicitly.
  2. Trace the sum(...) version on []. sum of an empty sequence is 0 (its identity element — the value that changes nothing). Why this step? The refactoring only preserves behaviour if the extracted primitive uses the same identity the original loop started from (total = 0). It does.
  3. Match them up. Both return 0. Look at the figure: the two paths that split at "any items?" rejoin at the same value. Why this step? A refactoring is valid only if every branch — including the degenerate one — lands on the same output.

Verify: loop on []0; sum([])0. Equal ✅. Degenerate case safe.


Example 4 — Boundary flip: < vs <= (C4)

Forecast: at exactly age = 18, do the two agree?

Figure — Refactoring — code smells, common refactorings (extract method, rename, etc.)
Figure: two step-curves of is_adult versus age. The violet solid line (age >= 18, correct) jumps to True at 18; the dashed orange line (age > 18, the buggy flip) jumps to True only at 19. The magenta dots at age = 18 mark the single point where the two disagree — correct says True, flipped says False.

  1. Simplify legitimately first. if C: return True; return False is exactly return C, so return age >= 18 is a valid refactoring. Why this step? This part preserves the truth value on every age — verify by cases below.
  2. Now test the colleague's version at the boundary. The correct rule keeps >=. Changing to > flips the answer at the single point age == 18: correct says True, theirs says False. Why this step? Cell C4 lives entirely at one point — you must probe the exact edge, not just typical values.
  3. Verdict. return age >= 18 = refactoring ✅. return age > 18 = not a refactoring — it's a behaviour change (a bug). Why this step? The definition is behaviour-preserving; a difference at even one input disqualifies it.

Verify: at age=18 — correct 18>=18 = True; flipped 18>18 = False. Different, so the flip fails. At age=17 and age=19 both agree. The magenta dots in the figure mark the one point where the two curves diverge.


Example 5 — Side effects & ordering (C5)

Forecast: is the printed sequence part of the observable behaviour?

  1. List the observable behaviour. It's not just the return value — it's also the sequence of prints: Start, then Total 108.0. Why this step? For code with side effects, behaviour includes order and content of every effect (cell C5). This is the trap people forget.
  2. Compare orders. Correct order: Start, Total. The buggy extraction emits Total, Start. Why this step? Same numbers, but the stream differs — so behaviour is not preserved.
  3. Fix by preserving order. Extract without moving the print earlier:
    def total_with_tax(order):
        return order.amount * 1.08   # pure, no print
    def process(order):
        print("Start")
        t = total_with_tax(order)
        print(f"Total {t}")
        return t
    Why this step? Extract the pure part (the arithmetic) and leave the effects in the original order. Pure functions are the safe things to extract.

Verify: For amount=100: total = 100*1.08 = 108.0. Correct output stream ["Start","Total 108.0"]; buggy stream ["Total 108.0","Start"]not equal, so the reorder is invalid; the pure-extraction version reproduces the correct stream ✅.


Example 6 — Magic number & units (C6)

Forecast: what does 27.78 actually mean, and is the formula even right?

  1. Decode the literal. . So 27.78 is a speed in m/s, but the variable multiplies time_hours and stores km. Why this step? Cell C6 hides unit mismatches inside anonymous numbers — you can't audit units you can't name.
  2. Name it correctly for the units in play. If time is in hours and output in km, the constant should be the speed in km/h:
    SPEED_KMH = 100
    distance_km = time_hours * SPEED_KMH
    Why this step? Naming forces the unit question and exposes that 27.78 was the wrong unit here — a latent bug the magic number was hiding.
  3. Keep 27.78 only where units match. If you truly work in metres and seconds: SPEED_MS = 27.78; distance_m = time_s * SPEED_MS. Why this step? One source of truth per unit system; see Clean Code.

Verify: unit check — hours × (km/hour) = km ✅. Numeric: for 2 hours, correct 2 * 100 = 200 km. The old buggy 2 * 27.78 = 55.56 was neither km nor m for that time — confirming the magic number masked a real defect.


Example 7 — Data clump → object (C7)

Forecast: how many parameters does distance have after extracting a Point?

Figure — Refactoring — code smells, common refactorings (extract method, rename, etc.)
Figure: a right triangle on x–y axes from Point a (0,0) to Point b (3,4). The orange horizontal leg is dx = 3, the violet vertical leg is dy = 4, and the magenta hypotenuse is the distance = 5. The four loose coordinates become two named points, and the Pythagorean value is unchanged.

  1. Name the hidden structure. (x1,y1) and (x2,y2) are two points. Why this step? A Data Clump is variables that always travel together — that togetherness is an unnamed concept (cell C7).
  2. Introduce the object:
    class Point:
        def __init__(self, x, y):
            self.x, self.y = x, y
    def distance(a, b):
        return ((a.x-b.x)**2 + (a.y-b.y)**2) ** 0.5
    Why this step? Four loose args become two meaningful args — fixing the Long Parameter List smell at the same time (one refactoring, two smells cured).
  3. Behaviour must be identical. The formula is the same Pythagorean distance. Why this step? Extract Class is only a refactoring if the computed value is unchanged.

Verify: points (0,0) and (3,4). Old distance(0,0,3,4) and new distance(Point(0,0), Point(3,4)) both give . Equal ✅. The figure shows the 3-4-5 triangle both versions measure.


Example 8 — Over-refactoring, the limiting case (C8)

Forecast: does more extraction always improve readability? (Push the idea to its limit.)

  1. Take extraction to the extreme. In the limit of "extract everything," each method wraps one operation and you bounce through many files to follow one formula. Why this step? Testing a principle at its limiting value reveals where it stops helping (cell C8) — exactly like checking a formula as a variable → ∞.
  2. Apply the naming test. Extract Method earns its keep only when the fragment names a concept. add_one names nothing you didn't already read in x + 1. Why this step? The parent's Wrong Idea D: extract to name a concept, not to cut line count.
  3. Correct amount: keep it as one clear expression result = 2*(x+1) - 3, extracting only if a meaningful sub-concept (e.g. apply_discount) exists. Why this step? Over-indirection creates its own smell (Shotgun-style navigation) — a mess trading one problem for another.

Verify: for x=5, all versions must compute 2*(5+1) - 3 = 9. The over-extracted chain and the single expression both give 9behaviour equal, so it's still technically a valid refactoring, but strictly worse for readers. Correctness ≠ improvement.


Example 9 — Real-world word problem (C9)

Forecast: name the two sub-concepts before you read step 1.

  1. Extract the concepts as named booleans:
    member_qualifies = total >= 50 and is_member
    bulk_qualifies   = total >= 100
    free_shipping    = member_qualifies or bulk_qualifies
    if free_shipping:
        ship_free()
    Why this step? The tangled boolean hid two business rules. Naming them (member_qualifies, bulk_qualifies) turns cryptic arithmetic into a readable policy anyone can audit — this is Introduce Explaining Variable working on a real rule.
  2. Confirm the boundaries. Member at exactly $50 → member_qualifies uses >= → qualifies. Non-member at $100 → bulk_qualifies uses >= → qualifies. Why this step? Cell C4 lives inside C9 — word problems love boundary edges ("at least" means >=, not >). You must nail the inclusive edge or you change behaviour.
  3. Behaviour unchanged. The named version produces the same truth value for every (total, is_member) as the original inline expression. Why this step? It is only a refactoring if the shipping decision is identical for every input — verified below.

Verify — cover the cases:

  • Member, $60 → (60≥50 and True)=True → free ✅
  • Member, $40 → (40≥50)=False, (40≥100)=False → not free ✅
  • Non-member, $120 → False or (120≥100)=True → free ✅
  • Non-member, $50 → False or (50≥100)=False → not free ✅

All four match the original inline boolean.


Example 10 — Exam twist: "Is this a valid refactoring?" (C10)

Forecast: guess which one secretly changes behaviour, and for which inputs.

  1. Test (A) across signs and types. For n = 7: 7/2 = 3.5, 7*0.5 = 3.5. For n = -7: both -3.5. Why this step? Cell C10 demands checking every sign and edge, not one friendly value — the trap always hides where you didn't look.
  2. Test (B) — the bit shift. >> 1 is integer floor-division by 2. For n = 7: 7/2 = 3.5 but 7 >> 1 = 3. Different! For n = -7: -7/2 = -3.5 but -7 >> 1 = -4 (Python floors toward negative infinity). Why this step? The shift silently drops the fractional part and rounds negatives the "wrong" way — two hidden behaviour changes at once.
  3. Verdict. (A) is a valid refactoring (identical for all numeric n); (B) is not — it changes behaviour for odd and negative inputs. Why this step? The exam trap rewards the tidy-looking option (B); the definition rejects it because behaviour must be identical everywhere, not just on the value the marker showed you.

Verify: 7/2 = 3.5 = 7*0.5 ✅; 7/2 = 3.5 ≠ 7>>1 = 3 ✅ (mismatch confirmed); -7>>1 = -4 ≠ -3.5 ✅ (mismatch confirmed).


Recall The whole matrix in one breath

Clean extract (C1) ::: name a self-contained pure block, replace with a call. Duplication (C2) ::: Rule of Three — extract shared logic to one named place. Empty input (C3) ::: check the loop-runs-zero-times case; identity value must match. Boundary (C4) ::: probe the exact edge; < vs <= can silently change behaviour. Side effects (C5) ::: behaviour includes the order of prints/mutations. Magic number/units (C6) ::: name the literal; naming exposes unit bugs. Data clump (C7) ::: variables that travel together want to be an object. Over-refactor (C8) ::: extract to name a concept, not to cut lines. Word problem (C9) ::: name business rules as explaining variables. Exam twist (C10) ::: "valid?" = identical output for every input, all signs.