Relationships and multiplicity
Association, aggregation, composition, and the numbers on the lines.
01The intuition
The problem it solves
A school timetable program has students, classes, teachers and rooms. The first version gives every class a list of students and every student a list of classes, and every room a teacher and every teacher a room. When a teacher leaves, nobody knows which lists to clean, a student appears in a class that no longer exists, and deleting a room deletes a teacher with it because of how the code was written. Nobody decided which objects own which, or how many of each may be attached.
class Course:
def __init__(self) -> None:
self.students: list["Student"] = []
self.teacher: "Teacher | None" = None
class Teacher:
def __init__(self) -> None:
self.courses: list[Course] = []
self.room: "Room | None" = None
def remove_teacher(t: Teacher) -> None:
del t # courses still point at a teacher that is gone
Build it one piece at a time
Step one: for every pair of classes that refer to each other, ask two questions. Does one own the other's life, so that deleting the owner should delete the part? And how many of each can there be on each side? Write the answers on the lines between the boxes.
# Course -> Student : association, many to many (a student takes many courses) # Course -> Teacher : association, many to one (one teacher per course) # School -> Course : composition, one to many (delete the school, courses go) # Timetable -> Slot : composition, one to many # Department -> Teacher : aggregation, one to many (teacher outlives department)
Step two: let the code match the answers. Composition means the owner creates and destroys the part and nobody else holds it. Aggregation means the owner holds a reference it did not create. Association means a plain reference, often one-way.
class Timetable:
def __init__(self) -> None:
self._slots: list["Slot"] = [] # composition: made and kept here
def add_slot(self, day: str, hour: int) -> "Slot":
s = Slot(day, hour); self._slots.append(s); return s
class Department:
def __init__(self, teachers: list["Teacher"]) -> None:
self.teachers = teachers # aggregation: teachers exist already
class Course:
def __init__(self, teacher: "Teacher") -> None:
self.teacher = teacher # association, one-way
self.enrolments: list["Enrolment"] = []
Step three: when a relationship has its own facts (the date a student joined a course, their grade), it is an object of its own that sits between the two. A many-to-many link almost always wants one.
from dataclasses import dataclass
from datetime import date
@dataclass
class Enrolment: # the link itself carries data
student: "Student"
course: Course
joined: date
grade: str | None = None
def enrol(student: "Student", course: Course, today: date) -> Enrolment:
e = Enrolment(student, course, today)
course.enrolments.append(e)
student.enrolments.append(e)
return e
Why it works
The principle: a line between two boxes is a decision about ownership and count, and the code must agree with the line. The invariants are the multiplicities: a course always has exactly one teacher, a slot belongs to exactly one timetable. Where it breaks: two-way references that are updated on one side only, so the two lists disagree; composition where the part is handed out and kept by someone else; and a many-to-many stored as two lists with no link object, so the grade has nowhere to live. Interviewers read your diagram and ask "can a course have no teacher?" and "what happens to enrolments when a course is cancelled?"; the answer should be on the line.
The trade-off you must say out loud
Two-way links are convenient for navigation and dangerous for consistency; one-way links plus a lookup are safer and slightly slower. Link objects add a class but give a home to the facts about the relationship. Say which direction you need to navigate in the use cases and keep only that. The follow-up is usually deletion: what cascades, what is blocked, what becomes orphaned.
Where it shows up in the 100 Google problems
- Parking lot: lot composes floors, floors compose spots, a ticket associates a vehicle with a spot.
- Library management system: book composes copies; a loan is the link object between copy and member.
- Online shopping system: an order line is the link between an order and a product, carrying quantity and price at the time.
- Airline reservation system: flight, seat and passenger, with a booking as the link that carries the fare.
- Stack Overflow: a vote is a link object between a user and a post, one per pair.