Composition: Objects That Use Objects
About 18 minutes
Why this matters
Inheritance says “is a”. But a car is not a kind of engine: it has an engine. When one object is built from other objects, that is composition, and it is usually the more flexible choice. Prefer composition unless the “is a” relationship is really true.
The idea
class Battery:
def __init__(self, charge=100):
self.charge = charge
def use(self, amount):
self.charge -= amount
class Phone:
def __init__(self, battery): # the Phone is GIVEN a battery
self.battery = battery
def call(self, minutes):
self.battery.use(minutes * 2) # delegate the work to the part
phone = Phone(Battery())
phone.call(10)
print(phone.battery.charge) # 80
Passing the parts in (instead of creating them inside) makes the class easy to change and to test: any object that offers use() will do. Python cares about what an object can do, not what class it is. This is duck typing: “if it quacks like a duck…”.
Try it
Make a SolarBattery class whose use does nothing, and pass it to Phone. The phone code does not change.
Your turn
Write two classes:
Engine(horsepower)withrunning(startsFalse),start()andstop().Car(name, engine)with anodometer(starts at0) and:drive(km): raiseValueError("distance must be positive")forkm <= 0. If the engine is not running, start it first (once). Addkmto the odometer.park(): stop the engine.describe(): text likeMini: 90 hp, 15 km.
The checks give your Car a fake engine to prove it only relies on start(), stop(), running and horsepower.
Recap
- Composition: build objects from other objects (“has a”) and delegate to them.
- Pass collaborators in, so they can be swapped or faked.
- Duck typing: what matters is the methods an object has, not its class.
Common mistakes
- Creating the part inside the class (
self.engine = Engine(90)), which makes it hard to change or test. - Reaching deep into the part (
car.engine.running = Truefrom outside) instead of calling methods. - Using inheritance for a “has-a”, such as
class Car(Engine).