Card 04/ 09

ExampleDifficulty: Intermediate2 min

Strategy Example: Shipping Costs per Carrier

Standard, express and collection-point delivery, chosen at checkout.

Shipping costs at a checkout are a natural Strategy, and this example maps the template onto it. Each carrier prices a parcel its own way: standard charges £3.99 plus £1 per kilogram, express charges £8.99 plus £2 per kilogram, and a collection point charges a flat £1.99. Weight is rounded up to the next whole kilogram, and money is held in pence so no total suffers from floating-point rounding.

The template's participants in the shipping example
TemplateShipping example
StrategyShipping, with one method, cost_pence(weight_kg)
ConcreteStrategyStandard, Express and CollectionPoint
ContextCheckout, which adds the shipping cost to the price of the goods
ClientThe loop at the bottom, which looks up the customer's choice and passes it in
py
import math
from abc import ABC, abstractmethod


class Shipping(ABC):
    @abstractmethod
    def cost_pence(self, weight_kg: float) -> int: ...


class Standard(Shipping):
    def cost_pence(self, weight_kg: float) -> int:
        return 399 + 100 * math.ceil(weight_kg)


class Express(Shipping):
    def cost_pence(self, weight_kg: float) -> int:
        return 899 + 200 * math.ceil(weight_kg)


class CollectionPoint(Shipping):
    def cost_pence(self, weight_kg: float) -> int:
        return 199


class Checkout:
    def __init__(self, shipping: Shipping) -> None:
        self._shipping = shipping

    def total_pence(self, goods_pence: int, weight_kg: float) -> int:
        return goods_pence + self._shipping.cost_pence(weight_kg)


CARRIERS: dict[str, Shipping] = {
    "standard": Standard(),
    "express": Express(),
    "collection": CollectionPoint(),
}

for choice in ("standard", "express", "collection"):
    total = Checkout(CARRIERS[choice]).total_pence(goods_pence=4200, weight_kg=2.5)
    print(f"{choice}: £{total // 100}.{total % 100:02d}")
OutputRan with python:3.12-slim (Python 3.12.13)
standard: £48.99
express: £56.99
collection: £43.99
A 2.5 kg order of £42 of goods, priced by each carrier.

The 2.5 kg parcel is charged as 3 kg, so standard costs 399 + 300 pence and express 899 + 600, while the collection point ignores the weight. Checkout.total_pence is one line, and it is the same line for all three.

Notice where the customer's choice is turned into an object: the CARRIERS dictionary maps the name chosen at checkout to a strategy. The lookup replaces the if chain, and an unknown name raises a KeyError at once rather than returning None. Adding a fourth carrier means one class and one dictionary entry.