runtime
134 / 250
lesson 134 Inheritance & Composition · advanced

Composition: objects that hold objects

An order has a customer and lines; a customer has an address. Storing objects as attributes of other objects builds a model of the real thing, and each object does its own part of the work.

You'll be able to
  • Build objects from other objects: an Order that holds a Customer and OrderLines
  • Read through the parts (order.customer.address.city) and handle a missing part
  • Delegate work to the parts, and swap a part to change behaviour
The idea

An order isn't a kind of customer, and an order line isn't a kind of order: these objects have each other. An Order holds a Customer object and a list of OrderLine objects; a Customer holds an Address. This is composition: building an object out of other objects.

You reach the parts with a chain of dots: order.customer.address.city. Each part keeps its own job: the line knows its amount, the order adds up its lines by asking each one (delegation), and nothing has to know how the others work inside.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · An order with its customer and lines
class Address:
    def __init__(self, street, city, postcode):
        self.street = street
        self.city = city
        self.postcode = postcode

class Customer:
    def __init__(self, customer_id, name, email, address):
        self.customer_id = customer_id
        self.name = name
        self.email = email
        self.address = address

class OrderLine:
    def __init__(self, sku, quantity, unit_price):
        self.sku = sku
        self.quantity = quantity
        self.unit_price = unit_price

    def amount(self):
        return self.quantity * self.unit_price

class Order:
    def __init__(self, order_id, customer, lines):
        self.order_id = order_id
        self.customer = customer
        self.lines = lines

    def total(self):
        return round(sum(line.amount() for line in self.lines), 2)

    def shipping_label(self):
        address = self.customer.address
        return f"{self.customer.name}, {address.street}, {address.city} {address.postcode}"

home = Address("12 Bank Street", "Houston", "77002")
jamal = Customer("C00077", "Jamal Walker", "jamal.walker62@icloud.com", home)
order = Order("O100282", jamal, [OrderLine("SPO-11681", 2, 17.92), OrderLine("HOM-22847", 1, 4.99)])
print(order.total())
print(order.shipping_label())
print(order.customer.address.city)
example-2.py · A part that isn't there
class Customer:
    def __init__(self, customer_id, email):
        self.customer_id = customer_id
        self.email = email

class Order:
    def __init__(self, order_id, customer):
        self.order_id = order_id
        self.customer = customer

orders = [Order("O100282", Customer("C00077", "jamal.walker62@icloud.com")), Order("O100918", None)]
for order in orders:
    print(order.order_id, order.customer.email)

Because a part is just an attribute, it can be swapped. A Shipment that holds a carrier object and asks it for a quote works with any carrier that has a quote method (polymorphism, the Inheritance & Composition module), and changing carrier is one assignment.

example-3.py · Change carrier, keep the shipment
class FlatRateCarrier:
    def __init__(self, name, rate):
        self.name = name
        self.rate = rate

    def quote(self, weight_kg):
        return self.rate

class PerKiloCarrier:
    def __init__(self, name, base, per_kg):
        self.name = name
        self.base = base
        self.per_kg = per_kg

    def quote(self, weight_kg):
        return round(self.base + self.per_kg * weight_kg, 2)

class Shipment:
    def __init__(self, order_id, weight_kg, carrier):
        self.order_id = order_id
        self.weight_kg = weight_kg
        self.carrier = carrier

    def cost(self):
        return self.carrier.quote(self.weight_kg)

parcel = Shipment("O100282", 3.5, FlatRateCarrier("RoyalPost", 4.2))
print(parcel.carrier.name, parcel.cost())
parcel.carrier = PerKiloCarrier("FastFreight", 2.0, 0.8)
print(parcel.carrier.name, parcel.cost())

When to use it: whenever one thing *has* another: orders have lines, customers have addresses, shipments have carriers. Mistakes to avoid: long chains in code far away from the objects (a.b.c.d.e), which tie that code to every class in the chain (give the outer object a method instead, like shipping_label); reading through an optional part without checking for None; and storing copies of a part's data (the customer's email copied onto the order) that go stale when the part changes.

Review
  • Composition stores objects as attributes of other objects: an order has a customer and lines.
  • Read through parts with dots, and check optional parts for None first.
  • Delegate: the outer object asks its parts to do their own work, and parts can be swapped.
  • Next: choosing between inheritance and composition.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Composition: objects that hold objects — Inheritance & Composition · Runtime