Card 08/ 09
Practice: Discount Rules at a Checkout
No discount, a percentage off, and buy two get one free, chosen per customer.
Discounts at a checkout are a natural Strategy, and this exercise asks you to write them both ways. A basket holds items at £5, £3, £2 and £4, in pence: [500, 300, 200, 400]. Each customer gets one of three rules: no discount; a percentage off the whole basket, set per promotion; or buy two get one free, where in every three items, taken from most to least expensive, the third is free.
- Write the rules as classes behind a
Discountinterface with one method,apply(prices). - Write the same rules as functions.
- What does each rule charge for the basket, with 10 per cent off for the second?
- Which form handles the percentage more cleanly, and why?
Show a solution
from abc import ABC, abstractmethod
from collections.abc import Callable
class Discount(ABC):
@abstractmethod
def apply(self, prices: list[int]) -> int: ...
class NoDiscount(Discount):
def apply(self, prices: list[int]) -> int:
return sum(prices)
class PercentageOff(Discount):
def __init__(self, percent: int) -> None:
self._percent = percent
def apply(self, prices: list[int]) -> int:
return sum(prices) * (100 - self._percent) // 100
class BuyTwoGetOneFree(Discount):
def apply(self, prices: list[int]) -> int:
ordered = sorted(prices, reverse=True)
return sum(price for i, price in enumerate(ordered) if i % 3 != 2)
basket = [500, 300, 200, 400]
for discount in (NoDiscount(), PercentageOff(10), BuyTwoGetOneFree()):
print(f"{type(discount).__name__}: {discount.apply(basket)}")
def percentage_off(percent: int) -> Callable[[list[int]], int]:
return lambda prices: sum(prices) * (100 - percent) // 100
def buy_two_get_one_free(prices: list[int]) -> int:
ordered = sorted(prices, reverse=True)
return sum(price for i, price in enumerate(ordered) if i % 3 != 2)
for name, rule in (("none", sum), ("10% off", percentage_off(10)), ("3 for 2", buy_two_get_one_free)):
print(f"{name}: {rule(basket)}")NoDiscount: 1400
PercentageOff: 1260
BuyTwoGetOneFree: 1100
none: 1400
10% off: 1260
3 for 2: 1100The basket costs 1400 with no discount, 1260 with 10 per cent off, and 1100 with buy two get one free: sorted, it is 500, 400, 300 and 200, and the third item, 300, is free.
The percentage is the one rule with configuration. As a class it is a constructor argument held in a field; as a function it is percentage_off(10), a function that returns the rule with the percentage captured in a closure. Both are clean here, and the function needs less code. The class would win if each rule also had to describe itself on the receipt, because that is a second method. No discount needs no code of its own as a function: the built-in sum already fits the slot.
← → move through sections and cards