Card 04/ 08

ExampleDifficulty: Intermediate2 min

Composite Example: Pricing a Product Bundle

A bundle holds products and other bundles, and its price is the sum of what it holds.

A shop that sells bundles is a natural Composite, and this example maps the template onto it. A desk kit contains a keyboard, a mouse and a cable pack, and the cable pack is itself a bundle of two cables. The checkout needs a price for whatever is in the basket, and should not care whether it is one product or a bundle of bundles.

The desk kit as a tree
The template's participants in the bundle example
TemplateBundle example
ComponentItem, with price_pence
LeafProduct, which returns its own price
CompositeBundle, which sums the prices of its items
Clientshow, which prints any item's price
py
from abc import ABC, abstractmethod


class Item(ABC):
    @abstractmethod
    def price_pence(self) -> int: ...


class Product(Item):
    def __init__(self, name: str, price_pence: int) -> None:
        self.name = name
        self._price_pence = price_pence

    def price_pence(self) -> int:
        return self._price_pence


class Bundle(Item):
    def __init__(self, name: str, items: list[Item]) -> None:
        self.name = name
        self._items = items

    def price_pence(self) -> int:
        return sum(item.price_pence() for item in self._items)


def show(item: Item) -> None:
    pence = item.price_pence()
    print(f"{item.name}: £{pence // 100}.{pence % 100:02d}")


cables = Bundle("Cable pack", [Product("USB-C cable", 899), Product("HDMI cable", 1299)])
desk_kit = Bundle("Desk kit", [Product("Keyboard", 4500), Product("Mouse", 2000), cables])

show(Product("Mouse", 2000))
show(cables)
show(desk_kit)
OutputRan with python:3.12-slim (Python 3.12.13)
Mouse: £20.00
Cable pack: £21.98
Desk kit: £86.98
`Bundle.price_pence` sums its items, and the cable pack is one of the desk kit's items.

The cable pack's £21.98 is the sum of its two cables. The desk kit's £86.98 is the keyboard, the mouse and the cable pack's total, so the recursion went one level deeper without any code saying so. Prices are held in pence, whole numbers, because a sum of floating-point pounds can come out a fraction of a penny wrong.

A rule that applies to bundles alone, such as ten per cent off any bundle, is a change to Bundle.price_pence. A new kind of item, such as a gift card with a fixed price, is a new leaf, and show and Bundle stay as they are.