Card 04/ 08
Open/Closed Example: Discount Rules at Checkout
Black Friday adds a rule on Thursday, and the price calculator is not touched.
A checkout's discounts are a natural place for the open/closed principle, because marketing invents new ones every season. This example keeps the price calculator closed by giving it a list of rule objects, each of which says whether it applies to an order and how much it takes off. New discounts are new rules.
Two rules exist today: members get 10 percent off, and orders of ten items or more get 5 pounds off.
class MemberDiscount:
def applies(self, order):
return order["member"]
def amount(self, order):
return order["subtotal"] * 0.10
class BulkDiscount:
def applies(self, order):
return order["items"] >= 10
def amount(self, order):
return 5.00
class PriceCalculator:
def __init__(self, rules):
self.rules = rules
def price(self, order):
discount = sum(rule.amount(order) for rule in self.rules if rule.applies(order))
return round(order["subtotal"] - discount, 2)
if __name__ == "__main__":
order = {"subtotal": 120.00, "items": 12, "member": True}
print(f"{PriceCalculator([MemberDiscount(), BulkDiscount()]).price(order):.2f}")103.00A 120 pound order of twelve items from a member gets 12 pounds and 5 pounds off, so it costs 103. On the Thursday before Black Friday, marketing asks for 20 percent off everything on the day. The new rule is a new class in a new file, and it is passed to the calculator alongside the others.
from ocp_discounts import BulkDiscount, MemberDiscount, PriceCalculator
class BlackFridayDiscount:
def applies(self, order):
return order["date"] == "2026-11-27"
def amount(self, order):
return order["subtotal"] * 0.20
order = {"subtotal": 120.00, "items": 12, "member": True, "date": "2026-11-27"}
rules = [MemberDiscount(), BulkDiscount(), BlackFridayDiscount()]
print(f"{PriceCalculator(rules).price(order):.2f}")79.00The same order on Black Friday costs 79: the three rules take off 12, 5 and 24. PriceCalculator was not edited, so its tests did not need to be rerun to trust it, and nobody had to read its loop to add the rule.
Two design choices make this work. The calculator depends only on two methods, applies and amount, so any object with those methods fits. And the list of rules is handed in from outside rather than written inside the calculator, so the decision about which rules are active lives with whoever builds the calculator. When the sale ends, the rule is removed from that list, and again the calculator is not touched.
← → move through sections and cards