Part 01 · Low Level Design
Class Relationships and UML
Knows-about, has-a and owns: association, aggregation vs composition, why has-a usually beats is-a, and UML class diagrams quick enough to draw on a whiteboard.
⏱ 2-3 hours · Station 3 of 80
The batch that took a teacher with it
A coaching centre's app has batches, teachers and students. When the admin cancels the HSC-Physics batch, the code deletes the batch row "and everything inside it": its class sessions, its enrolments, and its teacher.
Karim sir also teaches HSC-Math. The next morning his Math batch shows "teacher: unknown".
The bug isn't in the delete query. It's in a question nobody asked when designing the classes: does a batch own its teacher, or just point at him? Sessions belong to the batch and should go with it. A teacher doesn't.
This chapter is about the four ways classes can be connected, how to tell them apart, and how to draw them so the next developer doesn't have to guess.
The problem: "has a" means four different things
"A batch has a teacher", "a batch has sessions", "a batch has students", "the report printer has a batch". Same words, four different relationships:
| Relationship | What happens when the batch is deleted? |
|---|---|
| Batch has sessions | Sessions go too: they make no sense alone |
| Batch has a teacher | Teacher stays: he exists on his own and teaches other batches |
| Batch has students | Students stay, and their list of batches must be updated |
| Report printer has a batch | Nothing: the printer only borrowed it for one call |
If the code treats all four the same way, either things get deleted that shouldn't be (the teacher), or things are left dangling (enrolments pointing at a batch that no longer exists).
Think of a flat and its furniture
You rent a flat. The rooms belong to the flat: when the building is demolished, the rooms are gone. That's composition.
Your furniture is in the flat, but it's yours: when you move out, the sofa goes with you to the next flat. That's aggregation.
You and your neighbours know each other, and either of you could move away without the other disappearing. That's an association.
The plumber comes for an hour, uses your kitchen and leaves. He doesn't live there. That's a dependency.
Where the comparison breaks down. In real life ownership is obvious. In code it's just a field holding a reference, and every kind of relationship looks the same at the syntax level. The difference lives in who creates the object, who deletes it, and who else may hold it. You have to decide that on purpose and write it down.
A Batch is cancelled. Which objects should disappear with it?
Four relationships, and why has-a usually beats is-a
- Association ("knows about"): two objects are linked, and each can exist without the other. Students and batches: many-to-many, and both sides may need to find each other. Drawn as a plain line, often with numbers like
*to*. - Aggregation ("has, but shares"): a whole holds parts that live on their own and may be shared. A batch has a teacher; the teacher has other batches too. Drawn with a hollow diamond on the whole's side.
- Composition ("owns"): the whole creates its parts and they die with it. Nobody else holds them. A batch owns its sessions. Drawn with a filled diamond. In code, the owner usually builds the part itself and never hands out a reference that outlives it.
- Dependency ("uses for a moment"): one class uses another as a parameter or local variable and keeps no reference.
PrintSummary(batch)depends on Batch. Drawn as a dashed arrow. It's the weakest link and the cheapest one. - Inheritance ("is a"):
TeachingAssistant is a Teacher. It's the strongest link of all: the child gets every public method of the parent, forever. That's why composition over inheritance is the default advice: give a class a field holding the behaviour it needs, instead of becoming a subclass to get it. A person who is both a student and a teaching assistant is easy with composition (a person with roles) and awkward with inheritance (which class do you extend?). - UML class diagrams are just boxes (name, key fields, key methods) and these lines. In an interview, a quick sketch with the right line types answers "who owns what" before you write any code.
Cancelling a batch, step by step
school.Cancel(HSC-Physics)
Step 1/4 The school decides to cancel
School is where batches are opened and cancelled, so this is where the clean-up rules live.
- open
- HSC-Physics, HSC-Math
One question per line you draw
For every line between two classes, ask: if this one goes away, what happens to that one?
- It goes too → composition. The owner creates it and no one else holds it.
- It stays, and others share it → aggregation.
- It stays, and both sides keep track of each other → association.
- Nothing, because no reference was kept → dependency.
And before you draw an inheritance arrow, try saying the sentence out loud with "always": "a teaching assistant is always, in every way, a teacher." If you hesitate, use composition.
Try it: which relationship is this?
Name the relationship
order.lines = [line1, line2]
The coaching centre in code
A School opens batches. Each Batch owns its sessions, shares a teacher, and is associated with many students. PrintSummary only depends on a batch. Then HSC-Physics is cancelled.
package main
import (
"fmt"
"strings"
)
// Teacher and Student live on their own; batches only point at them.
type Teacher struct {
Name string
batches []*Batch
}
type Student struct {
Name string
batches []*Batch
}
// Session is owned by its batch (composition): it is created inside the batch and
// has no meaning outside it.
type Session struct{ Topic string }
type Batch struct {
Name string
teacher *Teacher // aggregation: shared, outlives the batch
sessions []Session // composition: owned, dies with the batch
students []*Student // association: many-to-many, both sides know
}
// School is where batches are created and cancelled.
type School struct{ batches []*Batch }
func (s *School) Open(name string, t *Teacher, topics ...string) *Batch {
b := &Batch{Name: name, teacher: t}
for _, topic := range topics {
b.sessions = append(b.sessions, Session{Topic: topic})
}
t.batches = append(t.batches, b)
s.batches = append(s.batches, b)
return b
}
func (b *Batch) Enroll(st *Student) {
b.students = append(b.students, st)
st.batches = append(st.batches, b)
}
// Cancel removes the batch everywhere it is referenced. Its sessions need no clean-up:
// nothing else holds them.
func (s *School) Cancel(b *Batch) {
s.batches = without(s.batches, b)
b.teacher.batches = without(b.teacher.batches, b)
for _, st := range b.students {
st.batches = without(st.batches, b)
}
}
func without(list []*Batch, b *Batch) []*Batch {
out := []*Batch{}
for _, x := range list {
if x != b {
out = append(out, x)
}
}
return out
}
func names(list []*Batch) string {
if len(list) == 0 {
return "(none)"
}
n := []string{}
for _, b := range list {
n = append(n, b.Name)
}
return strings.Join(n, ", ")
}
// PrintSummary only uses a batch for a moment (dependency): it keeps no reference.
func PrintSummary(b *Batch) {
st := []string{}
for _, s := range b.students {
st = append(st, s.Name)
}
fmt.Printf("%s: teacher %s, %d sessions, students %s\n",
b.Name, b.teacher.Name, len(b.sessions), strings.Join(st, ", "))
}
func main() {
school := &School{}
karim := &Teacher{Name: "Karim"}
rahim, nadia := &Student{Name: "Rahim"}, &Student{Name: "Nadia"}
physics := school.Open("HSC-Physics", karim, "Vectors", "Motion", "Waves")
math := school.Open("HSC-Math", karim, "Matrices", "Calculus")
physics.Enroll(rahim)
physics.Enroll(nadia)
math.Enroll(rahim)
PrintSummary(physics)
PrintSummary(math)
fmt.Println("Rahim's batches:", names(rahim.batches))
fmt.Printf("cancel %s: its %d sessions go with it\n", physics.Name, len(physics.sessions))
school.Cancel(physics)
fmt.Println("Karim still exists, teaches:", names(karim.batches))
fmt.Println("Rahim still exists, batches:", names(rahim.batches))
fmt.Println("Nadia still exists, batches:", names(nadia.batches))
fmt.Println("open batches:", names(school.batches))
}from __future__ import annotations
from dataclasses import dataclass, field
@dataclass(eq=False)
class Teacher:
"""Lives on its own; batches only point at it."""
name: str
batches: list[Batch] = field(default_factory=list)
@dataclass(eq=False)
class Student:
name: str
batches: list[Batch] = field(default_factory=list)
@dataclass(frozen=True)
class Session:
"""Owned by its batch (composition): no meaning outside it."""
topic: str
@dataclass(eq=False)
class Batch:
name: str
teacher: Teacher # aggregation: shared, outlives the batch
sessions: list[Session] = field(default_factory=list) # composition: owned, dies with the batch
students: list[Student] = field(default_factory=list) # association: many-to-many
def enroll(self, st: Student) -> None:
self.students.append(st)
st.batches.append(self)
class School:
def __init__(self) -> None:
self.batches: list[Batch] = []
def open(self, name: str, t: Teacher, *topics: str) -> Batch:
b = Batch(name, t, [Session(x) for x in topics])
t.batches.append(b)
self.batches.append(b)
return b
def cancel(self, b: Batch) -> None:
"""Remove the batch everywhere it is referenced. Its sessions need no clean-up."""
self.batches.remove(b)
b.teacher.batches.remove(b)
for st in b.students:
st.batches.remove(b)
def names(batches: list[Batch]) -> str:
return ", ".join(b.name for b in batches) or "(none)"
def print_summary(b: Batch) -> None:
"""Uses a batch only for a moment (dependency): keeps no reference."""
students = ", ".join(s.name for s in b.students)
print(f"{b.name}: teacher {b.teacher.name}, {len(b.sessions)} sessions, students {students}")
school = School()
karim = Teacher("Karim")
rahim, nadia = Student("Rahim"), Student("Nadia")
physics = school.open("HSC-Physics", karim, "Vectors", "Motion", "Waves")
math = school.open("HSC-Math", karim, "Matrices", "Calculus")
physics.enroll(rahim)
physics.enroll(nadia)
math.enroll(rahim)
print_summary(physics)
print_summary(math)
print("Rahim's batches:", names(rahim.batches))
print(f"cancel {physics.name}: its {len(physics.sessions)} sessions go with it")
school.cancel(physics)
print("Karim still exists, teaches:", names(karim.batches))
print("Rahim still exists, batches:", names(rahim.batches))
print("Nadia still exists, batches:", names(nadia.batches))
print("open batches:", names(school.batches))// Teacher and Student live on their own; batches only point at them.
class Teacher {
readonly name: string;
batches: Batch[] = [];
constructor(name: string) {
this.name = name;
}
}
class Student {
readonly name: string;
batches: Batch[] = [];
constructor(name: string) {
this.name = name;
}
}
// Owned by its batch (composition): no meaning outside it.
interface Session {
readonly topic: string;
}
class Batch {
readonly name: string;
readonly teacher: Teacher; // aggregation: shared, outlives the batch
readonly sessions: Session[]; // composition: owned, dies with the batch
readonly students: Student[] = []; // association: many-to-many
constructor(name: string, teacher: Teacher, topics: string[]) {
this.name = name;
this.teacher = teacher;
this.sessions = topics.map((topic) => ({ topic }));
}
enroll(st: Student): void {
this.students.push(st);
st.batches.push(this);
}
}
const without = (list: Batch[], b: Batch) => list.filter((x) => x !== b);
class School {
batches: Batch[] = [];
open(name: string, t: Teacher, ...topics: string[]): Batch {
const b = new Batch(name, t, topics);
t.batches.push(b);
this.batches.push(b);
return b;
}
// Remove the batch everywhere it is referenced. Its sessions need no clean-up.
cancel(b: Batch): void {
this.batches = without(this.batches, b);
b.teacher.batches = without(b.teacher.batches, b);
for (const st of b.students) st.batches = without(st.batches, b);
}
}
const names = (list: Batch[]) => list.map((b) => b.name).join(", ") || "(none)";
// Uses a batch only for a moment (dependency): keeps no reference.
function printSummary(b: Batch): void {
const students = b.students.map((s) => s.name).join(", ");
console.log(`${b.name}: teacher ${b.teacher.name}, ${b.sessions.length} sessions, students ${students}`);
}
const school = new School();
const karim = new Teacher("Karim");
const rahim = new Student("Rahim"), nadia = new Student("Nadia");
const physics = school.open("HSC-Physics", karim, "Vectors", "Motion", "Waves");
const math = school.open("HSC-Math", karim, "Matrices", "Calculus");
physics.enroll(rahim);
physics.enroll(nadia);
math.enroll(rahim);
printSummary(physics);
printSummary(math);
console.log("Rahim's batches:", names(rahim.batches));
console.log(`cancel ${physics.name}: its ${physics.sessions.length} sessions go with it`);
school.cancel(physics);
console.log("Karim still exists, teaches:", names(karim.batches));
console.log("Rahim still exists, batches:", names(rahim.batches));
console.log("Nadia still exists, batches:", names(nadia.batches));
console.log("open batches:", names(school.batches));// School owns everyone. Relationships are stored as IDs (indexes), which is how Rust
// models shared, many-to-many links without fighting the borrow checker.
type Id = usize;
struct Teacher { name: String }
struct Student { name: String }
// Owned by its batch (composition): a plain value inside the batch.
struct Session { _topic: String }
struct Batch {
name: String,
teacher: Id, // aggregation: shared, outlives the batch
sessions: Vec<Session>, // composition: owned, dropped with the batch
students: Vec<Id>, // association: many-to-many (Student side is looked up below)
}
struct School { teachers: Vec<Teacher>, students: Vec<Student>, batches: Vec<Option<Batch>> }
impl School {
fn open(&mut self, name: &str, t: Id, topics: &[&str]) -> Id {
let sessions = topics.iter().map(|x| Session { _topic: x.to_string() }).collect();
self.batches.push(Some(Batch { name: name.into(), teacher: t, sessions, students: vec![] }));
self.batches.len() - 1
}
fn enroll(&mut self, b: Id, st: Id) { self.batches[b].as_mut().unwrap().students.push(st); }
// Cancelling drops the Batch value, and its sessions are dropped with it.
fn cancel(&mut self, b: Id) { self.batches[b] = None; }
fn open_batches(&self) -> impl Iterator<Item = &Batch> { self.batches.iter().flatten() }
fn names<'a>(&self, it: impl Iterator<Item = &'a Batch>) -> String {
let v: Vec<&str> = it.map(|b| b.name.as_str()).collect();
if v.is_empty() { "(none)".into() } else { v.join(", ") }
}
fn of_teacher(&self, t: Id) -> String { self.names(self.open_batches().filter(|b| b.teacher == t)) }
fn of_student(&self, s: Id) -> String { self.names(self.open_batches().filter(|b| b.students.contains(&s))) }
}
// Uses a batch only for a moment (dependency): borrows it, keeps nothing.
fn print_summary(school: &School, b: &Batch) {
let st: Vec<&str> = b.students.iter().map(|&s| school.students[s].name.as_str()).collect();
println!("{}: teacher {}, {} sessions, students {}",
b.name, school.teachers[b.teacher].name, b.sessions.len(), st.join(", "));
}
fn main() {
let mut school = School {
teachers: vec![Teacher { name: "Karim".into() }],
students: vec![Student { name: "Rahim".into() }, Student { name: "Nadia".into() }],
batches: vec![],
};
let (karim, rahim, nadia) = (0, 0, 1);
let physics = school.open("HSC-Physics", karim, &["Vectors", "Motion", "Waves"]);
let math = school.open("HSC-Math", karim, &["Matrices", "Calculus"]);
school.enroll(physics, rahim);
school.enroll(physics, nadia);
school.enroll(math, rahim);
print_summary(&school, school.batches[physics].as_ref().unwrap());
print_summary(&school, school.batches[math].as_ref().unwrap());
println!("Rahim's batches: {}", school.of_student(rahim));
let p = school.batches[physics].as_ref().unwrap();
println!("cancel {}: its {} sessions go with it", p.name, p.sessions.len());
school.cancel(physics);
println!("Karim still exists, teaches: {}", school.of_teacher(karim));
println!("Rahim still exists, batches: {}", school.of_student(rahim));
println!("Nadia still exists, batches: {}", school.of_student(nadia));
println!("open batches: {}", school.names(school.open_batches()));
}import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
// Teacher and Student live on their own; batches only point at them.
static final class Teacher {
final String name;
final List<Batch> batches = new ArrayList<>();
Teacher(String name) { this.name = name; }
}
static final class Student {
final String name;
final List<Batch> batches = new ArrayList<>();
Student(String name) { this.name = name; }
}
// Owned by its batch (composition): no meaning outside it.
record Session(String topic) {}
static final class Batch {
final String name;
final Teacher teacher; // aggregation: shared, outlives the batch
final List<Session> sessions = new ArrayList<>(); // composition: owned, dies with the batch
final List<Student> students = new ArrayList<>(); // association: many-to-many
Batch(String name, Teacher teacher, String... topics) {
this.name = name;
this.teacher = teacher;
for (String t : topics) sessions.add(new Session(t));
}
void enroll(Student st) {
students.add(st);
st.batches.add(this);
}
}
static final class School {
final List<Batch> batches = new ArrayList<>();
Batch open(String name, Teacher t, String... topics) {
Batch b = new Batch(name, t, topics);
t.batches.add(b);
batches.add(b);
return b;
}
// Remove the batch everywhere it is referenced. Its sessions need no clean-up.
void cancel(Batch b) {
batches.remove(b);
b.teacher.batches.remove(b);
for (Student st : b.students) st.batches.remove(b);
}
}
static String names(List<Batch> list) {
return list.isEmpty() ? "(none)" : list.stream().map(b -> b.name).collect(Collectors.joining(", "));
}
// Uses a batch only for a moment (dependency): keeps no reference.
static void printSummary(Batch b) {
String st = b.students.stream().map(s -> s.name).collect(Collectors.joining(", "));
System.out.printf("%s: teacher %s, %d sessions, students %s%n", b.name, b.teacher.name, b.sessions.size(), st);
}
public static void main(String[] args) {
School school = new School();
Teacher karim = new Teacher("Karim");
Student rahim = new Student("Rahim"), nadia = new Student("Nadia");
Batch physics = school.open("HSC-Physics", karim, "Vectors", "Motion", "Waves");
Batch math = school.open("HSC-Math", karim, "Matrices", "Calculus");
physics.enroll(rahim);
physics.enroll(nadia);
math.enroll(rahim);
printSummary(physics);
printSummary(math);
System.out.println("Rahim's batches: " + names(rahim.batches));
System.out.printf("cancel %s: its %d sessions go with it%n", physics.name, physics.sessions.size());
school.cancel(physics);
System.out.println("Karim still exists, teaches: " + names(karim.batches));
System.out.println("Rahim still exists, batches: " + names(rahim.batches));
System.out.println("Nadia still exists, batches: " + names(nadia.batches));
System.out.println("open batches: " + names(school.batches));
}
}#include <algorithm>
#include <cstdio>
#include <memory>
#include <string>
#include <vector>
struct Batch;
// Teacher and Student live on their own; batches only point at them (raw, non-owning pointers).
struct Teacher {
std::string name;
std::vector<Batch*> batches;
};
struct Student {
std::string name;
std::vector<Batch*> batches;
};
// Owned by its batch (composition): stored by value, destroyed with the batch.
struct Session {
std::string topic;
};
struct Batch {
std::string name;
Teacher* teacher; // aggregation: shared, outlives the batch
std::vector<Session> sessions; // composition: owned, dies with the batch
std::vector<Student*> students; // association: many-to-many
void enroll(Student& st) {
students.push_back(&st);
st.batches.push_back(this);
}
};
void erase(std::vector<Batch*>& v, Batch* b) { v.erase(std::remove(v.begin(), v.end(), b), v.end()); }
// The school owns batches (unique_ptr): cancelling destroys the batch and its sessions.
struct School {
std::vector<std::unique_ptr<Batch>> batches;
Batch& open(const std::string& name, Teacher& t, std::vector<std::string> topics) {
auto b = std::make_unique<Batch>(Batch{name, &t, {}, {}});
for (auto& x : topics) b->sessions.push_back({x});
t.batches.push_back(b.get());
batches.push_back(std::move(b));
return *batches.back();
}
void cancel(Batch& b) {
erase(b.teacher->batches, &b);
for (Student* st : b.students) erase(st->batches, &b);
std::erase_if(batches, [&](const std::unique_ptr<Batch>& p) { return p.get() == &b; });
}
};
template <class List>
std::string names(const List& list) {
std::string out;
for (const auto& b : list) out += (out.empty() ? "" : ", ") + b->name;
return out.empty() ? "(none)" : out;
}
// Uses a batch only for a moment (dependency): keeps no reference.
void printSummary(const Batch& b) {
std::string st;
for (const Student* s : b.students) st += (st.empty() ? "" : ", ") + s->name;
std::printf("%s: teacher %s, %zu sessions, students %s\n", b.name.c_str(), b.teacher->name.c_str(),
b.sessions.size(), st.c_str());
}
int main() {
School school;
Teacher karim{"Karim", {}};
Student rahim{"Rahim", {}}, nadia{"Nadia", {}};
Batch& physics = school.open("HSC-Physics", karim, {"Vectors", "Motion", "Waves"});
Batch& math = school.open("HSC-Math", karim, {"Matrices", "Calculus"});
physics.enroll(rahim);
physics.enroll(nadia);
math.enroll(rahim);
printSummary(physics);
printSummary(math);
std::printf("Rahim's batches: %s\n", names(rahim.batches).c_str());
std::printf("cancel %s: its %zu sessions go with it\n", physics.name.c_str(), physics.sessions.size());
school.cancel(physics); // physics is destroyed here; don't touch it again
std::printf("Karim still exists, teaches: %s\n", names(karim.batches).c_str());
std::printf("Rahim still exists, batches: %s\n", names(rahim.batches).c_str());
std::printf("Nadia still exists, batches: %s\n", names(nadia.batches).c_str());
std::printf("open batches: %s\n", names(school.batches).c_str());
}All six print exactly this:
HSC-Physics: teacher Karim, 3 sessions, students Rahim, Nadia
HSC-Math: teacher Karim, 2 sessions, students Rahim
Rahim's batches: HSC-Physics, HSC-Math
cancel HSC-Physics: its 3 sessions go with it
Karim still exists, teaches: HSC-Math
Rahim still exists, batches: HSC-Math
Nadia still exists, batches: (none)
open batches: HSC-Math
Reading the output:
- The first three lines show the links: Karim teaches both batches, Rahim is in both, Nadia only in Physics.
- Cancel: Physics had 3 sessions. After cancelling, nothing anywhere refers to them; they were never shared.
- Karim still exists and still teaches Math. Only his link to Physics was removed.
- Rahim and Nadia still exist. Nadia now has no batches, which is a valid state for a student.
- What each language changed: in Go, Python, TypeScript and Java the garbage collector frees the sessions once the batch is unreachable. C++ makes ownership explicit: the school holds batches in
unique_ptr, sessions are stored by value inside the batch, and teachers/students are raw non-owning pointers, so cancelling destroys the batch and its sessions at that line. Rust stores links as IDs and the school owns everything; setting the batch slot toNonedrops the batch and its sessions, and the borrow checker won't let anything keep a reference to them.
Getting relationships right in real code
- The owner creates, the owner deletes. If a class builds its parts in its own constructor or factory method, that's a strong sign of composition, and it should be the only one to remove them.
- Many-to-many usually becomes its own object.
Enrollment{student, batch, joinedOn, fee}soon needs data of its own. In a database it's a join table; in code it's often a class. - Keep both sides in sync in one method.
Enrollupdates the batch and the student together. If two separate calls do it, one will eventually be forgotten. - Database foreign keys mirror these choices:
ON DELETE CASCADEfor composition (sessions),ON DELETE RESTRICTorSET NULLfor aggregation (a batch must not delete its teacher). - Draw before you code in interviews. Five boxes and the right lines in two minutes make the conversation about the design, not about syntax.
Designs that cause problems later
Pick the broken relationship
DELETE batch → also DELETE its teacher
Pause and think
The product team says: "Teachers can also enrol as students in other batches, to learn new subjects." With Teacher and Student as separate classes, how would you model Karim taking an English batch?
Reveal
Don't make Teacher extends Student or the other way round. Pull out a Person (name, phone, ID) and give batches links to a person in a role: a batch has one teaching person and many enrolled persons. Or keep Teacher and Student as role objects that each point at the same Person. Either way, Karim is one person with two roles, which is composition, and his data isn't duplicated.
Quick quiz
1. What's the one-sentence test that separates composition from aggregation?
Answer
If the whole is deleted, does the part go with it? Yes, and nobody else holds it: composition. No, it lives on and may be shared: aggregation.
2. Why is a many-to-many association often turned into its own class?
Answer
Because the link soon needs its own data (join date, fee, status). An Enrollment object holds that, and both sides refer to it.
3. What does "composition over inheritance" mean in practice?
Answer
Give a class a field that holds the behaviour or data it needs, instead of subclassing to inherit it. It keeps classes loosely tied, lets behaviour change at run time, and avoids awkward "is a" claims.
4. In UML, what do a filled diamond and a dashed arrow mean?
Answer
A filled diamond is composition (the whole owns the part). A dashed arrow is a dependency (one class uses another briefly).
Your challenge: a teacher who leaves
Add School.Resign(teacher, replacement): every batch taught by the leaving teacher is handed to the replacement.
- Add a teacher Salma at the start of the story.
- At the end, Karim resigns and Salma takes over.
- Print
Salma teaches:andKarim still exists, teaches:after it.
Before you run it, predict both lines.
You've succeeded when the new lines are Salma teaches: HSC-Math and Karim still exists, teaches: (none), Karim still exists as an object, and the batch's sessions and students are unchanged.
In one line
For every link, ask what happens to one side when the other goes away: gone with it is composition, stays and is shared is aggregation, stays and both sides know each other is association, and nothing because nothing was kept is dependency. Prefer has-a over is-a, and let the owner be the only one who creates and deletes its parts.
Station stamp
Finish the chapter
Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.
Go Pro to earn XP →