Concepts / Behavioral patterns / Strategy

Strategy

Swapping an algorithm at runtime by passing an object.

01The intuition

The problem it solves

A shop program computes the discount on a bill. On day one there is one rule. By the end of the month there are five: a festival discount, a student card, a bulk rule, a coupon, a loyalty points rule. The obvious code is one function with an if-else ladder, and every new rule means editing the function that already works and retesting all of it.

def discount(bill: float, kind: str, extra: dict) -> float:
    if kind == "festival":
        return bill * 0.10
    elif kind == "student":
        return min(bill * 0.15, 200)
    elif kind == "bulk":
        return 50 if extra["items"] >= 10 else 0
    elif kind == "coupon":
        return extra["coupon_value"]
    # next week: loyalty points ... and the function grows again
    return 0

The pain shows when two rules need different inputs (one needs the item count, one needs a coupon code), when the same ladder is copied into the invoice printer, and when a tester asks for a test of the student rule alone.

Build it one piece at a time

Step one: write down the one question every rule answers, as a tiny interface. Every rule takes a bill and returns a discount; nothing else is shared.

from typing import Protocol

class DiscountRule(Protocol):
    def apply(self, bill: "Bill") -> float: ...

Step two: move each branch of the ladder into its own small class that satisfies that interface. Each class carries the data only it needs, so the extra dictionary disappears.

class Festival:
    def apply(self, bill: "Bill") -> float:
        return bill.total * 0.10

class StudentCard:
    def apply(self, bill: "Bill") -> float:
        return min(bill.total * 0.15, 200)

class Bulk:
    def __init__(self, min_items: int, off: float) -> None:
        self.min_items, self.off = min_items, off
    def apply(self, bill: "Bill") -> float:
        return self.off if bill.item_count >= self.min_items else 0

Step three: the checkout no longer decides which rule to use; it is handed one. Choosing happens once, at the edge of the program, and the code in the middle just calls apply. Programmers call the swappable object a strategy.

class Checkout:
    def __init__(self, rule: DiscountRule) -> None:
        self.rule = rule
    def pay(self, bill: "Bill") -> float:
        return bill.total - self.rule.apply(bill)

rule = StudentCard() if customer.is_student else Bulk(10, 50)
print(Checkout(rule).pay(bill))

Why it works

The principle: separate the part that stays the same (computing a final price) from the part that changes (how the discount is found), and let the changing part be an object you pass in. The invariant is that Checkout works with any object that honours apply, so adding a rule touches no existing class. Where it breaks: a strategy that reaches back into the checkout to change its state, or a strategy interface so wide that half the rules leave methods empty. Interviewers probe by asking you to add a rule mid-interview; if you open the checkout class to do it, you have not finished the refactor.

The trade-off you must say out loud

Five rules become five classes plus an interface, and someone must still choose the rule somewhere; the ladder has moved, not vanished. For two stable rules, an if is fine and clearer. Say that choosing is a separate responsibility (a factory or a registry) and that strategies should be stateless so one instance can be shared. The follow-up is nearly always "what if several rules apply at once?", which leads to composing strategies or to the specification pattern.

Where it shows up in the 100 Google problems

02Used in these problems