A side street in Flatbush, Brooklyn. Mike’s Auto Repair. The kind of shop with the garage door always open and a coffee maker that never stops.
Walk in on a Tuesday and you’ll see it all at once: a Honda up on the lift, two mechanics talking under the hood, the AutoZone rep dropping off parts, a diagnostic scanner getting passed across the floor, a blue-and-white AAA plaque hanging on the wall.
Five things happening at once. Five different kinds of relationships.
1. Association
Mike and Sarah have been doing business for three years.
Sarah runs the counter at the AutoZone three blocks down. Whenever Mike needs a part — alternator, water pump, brake pads — he calls her. She pulls it. Someone walks over to pick it up.
Neither of them created the other. Mike could order from another supplier tomorrow. Sarah has a hundred other accounts. But they know each other. They interact. The relationship is real.
In code, this is called Association.
Two classes are associated when one holds a reference to the other. Neither creates the other. Neither is responsible for what happens to the other when things end.
class Supplier:
def __init__(self, name: str):
self.name = name
def provide(self, item: str) -> str:
print(f"{self.name}: preparing {item}")
return item
class Worker:
def __init__(self, name: str, supplier: Supplier): # Association
self.name = name
self.supplier = supplier # holds a reference, but did not create it
def request_item(self, item: str):
received = self.supplier.provide(item)
print(f"{self.name}: received {received}")
sarah = Supplier("Sarah")
james = Worker("James", supplier=sarah)
james.request_item("brake pads")
# Sarah: preparing brake pads
# James: received brake pads
james holds a reference to sarah. But if james ceases to exist, sarah doesn’t disappear with him. That’s the key: independent lifecycles.
2. Aggregation
The shop has three mechanics: James, Carlos, and Derek.
Mike schedules their shifts. Signs their checks. But if Mike closed up tomorrow — all three would still be mechanics. They’d find work at another shop. They existed before Mike hired them. They’ll exist after.
This is Aggregation.
The whole (the Shop) has parts (the Workers). But those parts can survive on their own. They aren’t born inside the shop and they don’t die with it.
class Worker:
def __init__(self, name: str):
self.name = name
def work(self):
print(f"{self.name} is working")
class Shop:
def __init__(self, name: str, workers: list): # Aggregation
self.name = name
self.workers = workers # passed in from outside, not created here
def open(self):
for w in self.workers:
w.work()
james = Worker("James")
carlos = Worker("Carlos")
derek = Worker("Derek")
mikes = Shop("Mike's Auto Repair", workers=[james, carlos, derek])
mikes.open()
# James is working
# Carlos is working
# Derek is working
The workers list is passed in from outside. Shop didn’t create those objects. If mikes goes away, James, Carlos, and Derek are still standing. The whole holds the parts, but the parts survive on their own.
But the service bays built into the garage floor? That’s a different story.
3. Composition
The garage has four service bays.
Bay 1 through Bay 4. Each has a lift, a drain pan, a tool rack, overhead lights. They’re physically built into the building.
If Mike sold the shop and someone tore the building down, those bays don’t just relocate. They don’t exist independently. The moment the shop is gone, the bays are gone too.
This is Composition.
Same “has-a” shape as Aggregation on the surface. But now the whole creates the parts. The whole owns the parts. The whole’s death is the parts’ death.
class WorkStation:
def __init__(self, number: int):
self.number = number
self.occupied = False
def start_job(self, job_id: str):
self.occupied = True
print(f"Station #{self.number}: working on {job_id}")
def finish(self):
self.occupied = False
print(f"Station #{self.number}: cleared")
class Shop:
def __init__(self, name: str):
self.name = name
self.stations = [WorkStation(i) for i in range(1, 5)] # Composition — created here
def accept_job(self, job_id: str):
for station in self.stations:
if not station.occupied:
station.start_job(job_id)
return
print("All stations busy — please wait")
mikes = Shop("Mike's Auto Repair")
mikes.accept_job("Honda-NYC-4782")
# Station #1: working on Honda-NYC-4782
Shop builds the WorkStation objects inside its own constructor. When the Shop is gone, those station objects go with it.
Aggregation vs Composition — one question decides it: did the whole create the part, or was the part brought in from outside?
4. Dependency
A customer pulls in driving a beat-up Camry. Carlos takes the keys.
First thing he does: grabs the diagnostic spec sheet from the front desk. Checks the error codes, maps them to the repair needed, writes it up. Puts the sheet back.
He doesn’t own it. Doesn’t store it anywhere. Just borrowed it long enough to do the job.
In code, this is called Dependency.
It’s the weakest relationship of the five. One class uses another temporarily — as a method parameter, or a local variable. No reference stored on the object. No shared lifecycle. Pick it up, use it, let it go.
class Specification:
def get_measurements(self, order_id: str) -> dict:
print(f"Looking up spec for {order_id}...")
return {"measurement_a": 40, "measurement_b": 34, "measurement_c": 16}
class Worker:
def __init__(self, name: str):
self.name = name
def process_order(self, order_id: str, spec: Specification): # Dependency
measurements = spec.get_measurements(order_id)
print(f"{self.name}: processing order {order_id}")
for key, val in measurements.items():
print(f" {key}: {val}")
spec = Specification()
carlos = Worker("Carlos")
carlos.process_order("Camry-NYC-9812", spec)
# Looking up spec for Camry-NYC-9812...
# Carlos: processing order Camry-NYC-9812
# measurement_a: 40
# measurement_b: 34
# measurement_c: 16
Specification doesn’t appear anywhere in Worker.__init__. It shows up only inside process_order — passed in, used, done. The dependency exists only for the duration of that method call.
In UML, this is drawn as a dashed arrow. The lightest touch.
5. Realization
On the wall near the entrance: a blue-and-white plaque. AAA Approved Auto Repair.
Getting that plaque wasn’t free. Mike signed an agreement. A contract. Transparency in written estimates. OEM-quality parts. ASE-certified technicians. A loaner vehicle policy.
The plaque is a statement: “We have fulfilled every item on that list.”
In code, this is Realization.
A class realizes an interface — it signs the contract. The interface says what must be done. The class decides how. No implementation lives in the interface. Just the commitment.
from abc import ABC, abstractmethod
class CertifiedWorkshop(ABC): # Interface — defines the contract
@abstractmethod
def safe_environment(self) -> bool:
pass
@abstractmethod
def fair_wages(self, employee_name: str) -> float:
pass
@abstractmethod
def quality_output(self, order_id: str) -> str:
pass
class Workshop(CertifiedWorkshop): # Realization — fulfills the contract
def safe_environment(self) -> bool:
print("Fire exits clear, safety equipment in place")
return True
def fair_wages(self, employee_name: str) -> float:
print(f"{employee_name}: paid on time")
return 15000.0
def quality_output(self, order_id: str) -> str:
print(f"Order {order_id}: completed to standard")
return "approved"
mikes = Workshop()
mikes.safe_environment()
mikes.fair_wages("James")
mikes.quality_output("Honda-NYC-4782")
# Fire exits clear, safety equipment in place
# James: paid on time
# Order Honda-NYC-4782: completed to standard
Miss even one method and Python raises a TypeError at instantiation — the contract isn’t fulfilled. In Java it’s called implements. In Swift it’s a protocol. Same idea everywhere: a class that realizes an interface is making a promise it cannot break.
All Five at Once
Five arrows. Five different weights.
Solid line: Association. Hollow diamond: Aggregation. Filled diamond: Composition. Dashed arrow: Dependency. Dashed arrow with hollow triangle: Realization.
The diamond tells you ownership. The fill tells you how deep that ownership goes. The dashes tell you it’s temporary.
Summary
| In the story | In code |
|---|---|
| Mike knowing Sarah at AutoZone | Association — one class holds a reference to another |
| James, Carlos, Derek working at the shop | Aggregation — whole holds parts that exist independently |
| Service bays built into the building | Composition — whole creates and owns its parts |
| Carlos borrowing the spec sheet | Dependency — one class uses another only inside a method |
| The AAA certification plaque | Realization — a class implements an interface’s contract |
| Shop closes, mechanics move on | Aggregation: parts survive the whole |
| Shop closes, bays disappear | Composition: parts die with the whole |
Association is the loosest bond — two objects know each other, but neither owns the other.
Aggregation and Composition both say “has-a” — the difference is who created the part and whether it can live on its own.
Dependency is temporary — it exists only inside a method call, never stored on the object.
Realization is a promise — the class commits to every method in the interface, no exceptions.
Next question: now that objects have relationships — how do we structure the code that wires them all together, without things falling apart every time something changes? That idea lives in the SOLID Principles.
Next in the OOP series: SOLID — five rules for code that holds up