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.
- 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
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.
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)
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.
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.
- 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.