Skip to main content

SOLID & design patterns (factory, strategy)

Level 4: Level 4: Engineeringhard20 minsolidstrategy-patternfactory-patterndesign

Refactor toward SOLID with pluggable strategies selected by a factory.

Why SOLID shows up in code review

Almost every service has a spot where behavior branches by a key: pick a discount by customer tier, a parser by file type, a shipping rate by region. It usually starts as a three-line if/elif. A year later it is forty branches, every teammate edits the same function, and every edit risks breaking a case that already worked. SOLID is five design principles for arranging code so that new behavior is additive instead of invasive. Two of them do most of the work here.

  • Single responsibility: each function or class has one reason to change.
  • Open/closed: code is open to extension but closed to modification. You add a case by adding code, not by editing code that already passed its tests.

The other three matter less here but come up in review, so it is worth being able to name all five:

Table
The highlighted column is the useful half. Almost nobody recalls the five letters under pressure, but everyone recognises the smells, and each smell is what the principle was written to describe.
PrincipleIn one lineThe smell it names
S: single responsibilityone reason to changea class that both parses and saves, so two teams edit it
O: open/closedextend by adding, not by editingthe forty-branch if/elif nobody dares touch
L: Liskov substitutiona subtype must work anywhere its base doesa subclass that raises NotImplementedError on an inherited method
I: interface segregationmany small interfaces beat one fat oneimplementers stubbing methods they never needed
D: dependency inversiondepend on the abstraction, not the concrete typea service that builds its own DB client, so tests cannot swap it
The highlighted column is the useful half. Almost nobody recalls the five letters under pressure, but everyone recognises the smells, and each smell is what the principle was written to describe.
Check yourself
OrderService builds its own Postgres client inside __init__, so a unit test has no way to swap in a fake. Which principle does that break first?

A long if/elif chain violates open/closed: every new tier reopens price_for and puts a tested function back on the table. Two patterns remove the chain.

Strategy: behavior as a value

A strategy is one interchangeable unit of behavior. In Python, functions are first-class objects, so the simplest strategy is just a function you can store and pass around:

def regular(a):
    return round(a, 2)

def member(a):
    return round(a * 0.9, 2)   # 10% off
Check yourself
A teammate adds a bulk tier whose discount depends on the item count, so they write def bulk(amount, count) and register it next to the others. The caller still does strategy(amount). What happens for a bulk customer?

Each strategy has the same shape (amount in, price out), so a caller can swap one for another without knowing which one it holds.

Factory: pick the strategy by key

A factory maps a key to a strategy so the caller never sees the choices:

STRATEGIES = {"regular": regular, "member": member}

def price_for(kind, amount):
    strategy = STRATEGIES.get(kind, regular)  # default to full price
    return strategy(amount)

print(price_for("member", 100))   # 90.0

Adding a vip tier is one new function plus one dict entry. price_for itself never changes. That is open/closed in three lines, and it is exactly what you will build: first inline, then behind a pricing package.

Check yourself

Four versions of the same dispatch, all with price_for(kind, amount) calling strategy(amount). Sort each by what the customer sees.

STRATEGIES = {'member': member}, then STRATEGIES.get('member', regular)(100)
STRATEGIES = {'member': member(100)}, then STRATEGIES.get('member', regular)(100)
STRATEGIES['student'](100), where no student key was ever registered
STRATEGIES.get('student', regular)(100), where no student key was ever registered

Two traps interns hit

Store the function, not its result. {"member": member} stores the callable; {"member": member(100)} calls it immediately and stores the number 90.0, so a later strategy(amount) raises TypeError: 'float' object is not callable.

Handle the unknown key. STRATEGIES[kind] raises KeyError for a tier you have not registered. Use STRATEGIES.get(kind, regular) so an unknown kind falls back to full price, which is what the exercises require.

Check yourself
You are in review defending the dispatch table over the if/elif chain, and there are only five tiers. What is the strongest argument?

Interview nuance: an if/elif chain does up to k comparisons for k branches, while the dict dispatch is one average-case O(1) hash lookup no matter how many strategies exist. But interviewers care more about the design consequence than the constant factor: with the table, adding a strategy touches zero existing lines, so nothing that already passed can regress. Named dispatch tables like this are how real routers, command handlers, and plugin registries stay open for extension as they grow.

Worked example (Python)
def regular(a):
    return round(a, 2)


def member(a):
    return round(a * 0.9, 2)


STRATEGIES = {"regular": regular, "member": member}
print(STRATEGIES.get("member", regular)(100))   # 90.0

Apply

Your turn

The task this lesson builds to.

Warm-up (one file): implement price_for(kind, amount) that applies a discount by kind. regular is full price, member is 10% off, vip is 20% off, and any unknown kind is full price. Round to 2 decimals.

price_for("member", 100) is 90; price_for("vip", 100) is 80.

3 hints and 4 automated checks are waiting in the workspace.

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Implement price_for(kind, amount) in pricing/checkout.py: use the STRATEGIES factory dict to pick the strategy for kind (defaulting to regular) and apply it to amount. Adding a strategy must not require editing price_for. Some tests are hidden.

3 hints and 2 automated checks are waiting in the workspace.