Concepts / Modeling and API shape / Relationships and multiplicity

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.

02Used in these problems