runtime
199 / 250
lesson 199 Software Architecture · advanced

Refactoring: change the structure, keep the behaviour

Refactoring improves code without changing what it does. The safety net is a set of tests that pin the current behaviour first; then small steps (extract a function, replace a chain of ifs with a table, remove duplication), checking after each.

You'll be able to
  • Write characterization tests that record what code does now, before changing it
  • Replace if/elif chains with a lookup table and extract duplicated logic into a helper
  • Prove a refactor kept the behaviour by comparing old and new over many inputs
The idea

A refactor must not change behaviour, including the odd parts users rely on. So first pin the current behaviour with characterization tests: call the old code with representative inputs (normal, boundaries, odd cases) and record its answers as expected values. Then change the structure in small steps, re-running the tests after each. Common steps: extract a function for a named idea, replace an if/elif chain with a table when the branches only differ in data, and remove duplication by moving the shared part into one helper.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · A shipping chain becomes a table
def shipping_old(zone, weight_kg):
    if zone == "domestic":
        if weight_kg <= 1:
            return 4.99
        else:
            return 4.99 + (weight_kg - 1) * 1.5
    elif zone == "canada":
        if weight_kg <= 1:
            return 12.5
        else:
            return 12.5 + (weight_kg - 1) * 3.0
    elif zone == "international":
        if weight_kg <= 1:
            return 24.0
        else:
            return 24.0 + (weight_kg - 1) * 6.0
    else:
        raise ValueError(f"unknown zone {zone!r}")

RATES = {"domestic": (4.99, 1.5), "canada": (12.5, 3.0), "international": (24.0, 6.0)}

def shipping_new(zone, weight_kg):
    if zone not in RATES:
        raise ValueError(f"unknown zone {zone!r}")
    base, per_kg = RATES[zone]
    return base + max(0, weight_kg - 1) * per_kg

cases = [(zone, w) for zone in RATES for w in [0.2, 1, 1.5, 3, 12.75]]
print(all(shipping_old(z, w) == shipping_new(z, w) for z, w in cases), len(cases))
for function in (shipping_old, shipping_new):
    try:
        function("mars", 1)
    except ValueError as error:
        print(function.__name__, error)

The second classic is duplication: two functions that validate an email the same way will drift apart the first time someone fixes one of them. Extract the shared part into a helper with a name that says what it does, and call it from both.

example-2.py · Extracting a shared helper
def clean_email(raw):
    email = raw.strip().lower()
    if email.count("@") != 1 or email.startswith("@") or email.endswith("@"):
        raise ValueError(f"invalid email {raw!r}")
    return email

def register(customers, raw_email, name):
    email = clean_email(raw_email)
    customers[email] = {"name": name}
    return email

def change_email(customers, old, raw_new):
    new = clean_email(raw_new)
    customers[new] = customers.pop(old)
    return new

customers = {}
print(register(customers, "  Nora_Garcia@Fastmail.com ", "Nora"))
print(change_email(customers, "nora_garcia@fastmail.com", "nora@runtime.market"))
print(customers)
try:
    register(customers, "jamal.walker62", "Jamal")
except ValueError as error:
    print(error)

When to refactor: before adding a feature to code that is hard to change, and when you see the same change being made in several places. Mistakes to avoid: refactoring without tests; mixing a refactor and a behaviour change in one step (when a test fails, you can't tell which one broke it); "fixing" odd behaviour silently during a refactor (change behaviour deliberately, in a separate step, with its own test); and big-bang rewrites.

Review
  • Pin the current behaviour with characterization tests before changing structure.
  • Small steps: extract functions, replace data-only if/elif chains with tables, remove duplication.
  • Prove equality on many inputs; keep behaviour changes in separate steps.
  • Next: testing the modules together, through the real entry point.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Refactoring: change the structure, keep the behaviour — Software Architecture · Runtime