CodeRigor

FREE LESSON

Composition & collaborator ownership

Compose an Order with a shared Catalog while keeping each order’s mutable quantities independent.

An order knows which SKUs were requested and in what quantities. A catalog knows the current unit price of a SKU. Those responsibilities cooperate, but an order is not a kind of catalog. Composition expresses the relationship directly: an Order has a Catalog collaborator and asks it for prices.

This separation lets several orders use the same catalog while maintaining independent quantities. It also makes the price policy visible. In this lab, total() is a live quote: changing a catalog price changes existing orders’ next totals. A historical invoice would need captured prices and a different contract. Never let a convenient implementation accidentally decide that business rule.

An Order has a Catalog
start / 2
Two orders retain one live Catalog but own separate quantity collections; the Catalog copies its constructor mapping.
An Order has a Catalog
Two orders retain one live Catalog but own separate quantity collections; the Catalog copies its constructor mapping.

Pass a collaborator into the constructor

Dependency injection can be this small: accept the object needed to do the job as a constructor parameter. There is no framework involved. Order retains that object and calls its public interface; it does not reach into its internal price dictionary or copy the collaborator behind the caller’s back.

Python
class Catalog:
    def __init__(self, prices):
        self._prices = dict(prices)

    def price(self, sku):
        return self._prices[sku]

    def set_price(self, sku, cents):
        self._prices[sku] = cents

class Quote:
    def __init__(self, catalog):
        self._catalog = catalog
        self._units = {}

    def add(self, sku, units):
        self._catalog.price(sku)
        self._units[sku] = self._units.get(sku, 0) + units

    def total(self):
        return sum(self._catalog.price(sku) * units
                   for sku, units in self._units.items())

catalog = Catalog({"tea": 100})
quote = Quote(catalog)
quote.add("tea", 2)
print(quote.total())
catalog.set_price("tea", 125)
print(quote.total())

This runnable example assumes valid counts and prices. The graded classes add strict type/range checks, deterministic line snapshots and failure guarantees. Unknown SKU lookup happens before the order stores a new line, so a failed addition cannot leave a ghost entry behind.

Ownership is a separate choice for each edge

The catalog copies the caller’s initial mapping. That blocks later edits through an alias from bypassing catalog validation. The order instead retains the Catalog object itself: this sharing is the declared live-pricing behavior. Each order also creates its own quantities dictionary. “Always copy everything” and “always retain references” would both violate some part of this contract.

Live price, stable line snapshots
start / 4
Updating the catalog changes a quote without changing ordered units. An earlier line tuple remains stable.
Live price, stable line snapshots
Updating the catalog changes a quote without changing ordered units. An earlier line tuple remains stable.

The lines() query returns sorted immutable pairs. A caller can retain a tuple without gaining write access to current quantities. Sorting makes the result independent of insertion order and easier to compare in tests. The total uses integer cents, avoiding floating rounding questions in this introductory model. A zero price and an empty order are valid and both can produce total zero.

Do not cache total unless you also design an invalidation rule. Catalog prices can change without any call to the order, so a cached value could silently become stale. Looking up each price when queried is the simpler correct implementation.

Before coding, sketch two orders pointing to one catalog. Change the original input mapping, then change the catalog, then add a second line to one order. Predict each total and snapshot. The bug hunts target lost quantities and unit prices counted only once; the fill practices delegation. The complete build must make these ownership and failure decisions observable through public methods.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Revisit methods that group related data and behavior. Apply that mechanism to two cooperating classes: Order delegates pricing to Catalog rather than inheriting its storage.