Refactoring: Simplifying Conditionals
Tame nested ifs with guard clauses, named conditions, merged branches, polymorphism, and null objects.
Big idea
Conditionals are where logic gets hard to read and easy to break. Simplifying them means making each condition say what it means, getting edge cases out of the way early, and — when the same branching repeats — letting objects decide instead of if statements.
The techniques
| Technique | Move |
|---|---|
| Decompose Conditional | Extract the condition and each branch into named functions |
| Consolidate Conditional Expression | Several checks with the same result → one named check |
| Consolidate Duplicate Conditional Fragments | Code repeated in every branch → move it outside |
| Replace Nested Conditional with Guard Clauses | Handle special cases first with early returns; keep the main path flat |
| Replace Conditional with Polymorphism | A switch on type → a method each class overrides |
| Introduce Null Object | Repeated if x is None → an object with harmless default behaviour |
| Introduce Assertion | An assumption the code relies on → an explicit check |
| Remove Control Flag | A done = True flag → break / return |
Think of it like
A nightclub door. A bad bouncer checks your shoes, then if the shoes are fine checks your age, then if you're old enough checks the guest list, then — inside all of that — finally lets you in. A good bouncer turns people away as soon as one rule fails ("no ID? not tonight"), and whoever's still standing walks straight in. Same rules, far less nesting.
In code
def pay_before(emp):
if emp["dead"]:
result = 0
else:
if emp["separated"]:
result = 500
else:
if emp["retired"]:
result = 800
else:
result = emp["salary"] / 12
return result
def pay(emp): # guard clauses: special cases leave early
if emp["dead"]:
return 0
if emp["separated"]:
return 500
if emp["retired"]:
return 800
return emp["salary"] / 12
class Customer:
def __init__(self, name, plan): self.name, self.plan = name, plan
def discount(self): return 0.1 if self.plan == "gold" else 0.0
class GuestCustomer: # null object: no more "if customer is None"
name, plan = "guest", "none"
def discount(self): return 0.0
def checkout(customer, amount):
return f"{customer.name}: {amount * (1 - customer.discount()):.2f}"
staff = [{"dead": False, "separated": False, "retired": False, "salary": 60000},
{"dead": False, "separated": True, "retired": False, "salary": 0},
{"dead": True, "separated": False, "retired": True, "salary": 0}]
print([pay(e) for e in staff], "same:", [pay(e) for e in staff] == [pay_before(e) for e in staff])
for c in (Customer("Ana", "gold"), GuestCustomer()):
print(checkout(c, 50))output
[5000.0, 500, 0] same: True
Ana: 45.00
guest: 50.00Run and edit it in the app.
Watch it
Hands on: replace the conditional with polymorphism
Bird speeds are computed with a switch on kind, duplicated in describe(). The duplicate forgot the load penalty for laden swallows. Give each kind its own class with a speed() method so the rule exists once, matching the output.
def speed(bird):
if bird["kind"] == "swallow":
return 35 - 2 * bird["load"]
elif bird["kind"] == "parrot":
return 0 if bird["nailed"] else 20
return 10
def describe(bird):
if bird["kind"] == "swallow":
s = 35
elif bird["kind"] == "parrot":
s = 0 if bird["nailed"] else 20
else:
s = 10
return f"{bird['kind']} flies at {s}"
flock = [{"kind": "swallow", "load": 3}, {"kind": "parrot", "nailed": False}, {"kind": "duck"}]
for b in flock:
print(describe(b), "| speed()", speed(b))output
swallow flies at 29 | speed() 29
parrot flies at 20 | speed() 20
duck flies at 10 | speed() 10Run and edit it in the app.
What does replacing nested conditionals with guard clauses achieve?
When does Introduce Null Object help?
How it relates
- Replace Conditional with Polymorphism is the route to Polymorphism, Strategy, and State, and the cure for Switch Statements.
- Decompose Conditional uses Extract Method from Composing Methods.
- Refactoring Safely: test the edges when you rearrange conditions.
Key terms
- Guard clause — An early return that handles a special case before the main logic.
- Decompose conditional — Extracting a condition and its branches into well-named functions.
- Null object — An object with neutral behaviour used instead of None.
- Replace Conditional with Polymorphism — Moving each branch of a type switch into a class's method.