Episode 9 — System Design / 9.1 — LLD Foundations
9.1.c — Class Relationships
In one sentence: Class relationships define how objects know about, use, contain, or inherit from each other — understanding these distinctions (association, aggregation, composition, dependency, inheritance, implementation) is the backbone of every LLD class diagram.
Table of Contents
- 1. Overview of Relationship Types
- 2. Association
- 3. Aggregation
- 4. Composition
- 5. Aggregation vs Composition — Deep Comparison
- 6. Dependency
- 7. Inheritance (IS-A)
- 8. Implementation (Interface Realization)
- 9. Multiplicity (Cardinality)
- 10. Master Comparison Table
- 11. Real-World Example — E-Commerce System
- 12. Key Takeaways
1. Overview of Relationship Types
┌─────────────────────────────────────────────────────────────────┐
│ CLASS RELATIONSHIP SPECTRUM │
│ │
│ Weakest coupling ◄────────────────────────► Strongest coupling │
│ │
│ Dependency Association Aggregation Composition │
│ ┄┄┄┄┄┄► ────────── ──────◇──── ──────◆──── │
│ "uses" "knows" "has-a" "owns-a" │
│ temporarily references (weak whole- (strong whole- │
│ part) part, same │
│ lifetime) │
│ │
│ Inheritance (IS-A) Implementation (CAN-DO) │
│ ──────────────▷ ┄┄┄┄┄┄┄┄┄┄┄▷ │
│ "is a type of" "fulfills contract of" │
└─────────────────────────────────────────────────────────────────┘
2. Association
Definition: A relationship where one class knows about another. Both objects exist independently. Neither controls the other's lifecycle.
Real-world analogy: A Teacher and a Student — they know about each other, but if the teacher leaves, the student still exists (and vice versa).
┌──────────┐ teaches ┌──────────┐
│ Teacher │ ────────────────────── │ Student │
└──────────┘ └──────────┘
Both exist independently. Removing one does NOT destroy the other.
Code Example
class Student {
private name: string;
private studentId: string;
constructor(name: string, studentId: string) {
this.name = name;
this.studentId = studentId;
}
getName(): string { return this.name; }
getStudentId(): string { return this.studentId; }
}
class Teacher {
private name: string;
private students: Student[]; // Teacher KNOWS ABOUT students
constructor(name: string) {
this.name = name;
this.students = [];
}
// Students are passed in — Teacher does NOT create them
addStudent(student: Student): void {
this.students.push(student);
}
removeStudent(studentId: string): void {
this.students = this.students.filter(s => s.getStudentId() !== studentId);
// Note: student still exists even after being removed from this teacher
}
listStudents(): string[] {
return this.students.map(s => s.getName());
}
}
// Both created independently
const alice = new Student("Alice", "S001");
const bob = new Student("Bob", "S002");
const mrSmith = new Teacher("Mr. Smith");
mrSmith.addStudent(alice);
mrSmith.addStudent(bob);
mrSmith.removeStudent("S001");
// alice still exists — it was not destroyed
console.log(alice.getName()); // "Alice"
Key Characteristics of Association
| Property | Value |
|---|---|
| Lifecycle coupling | None — both objects exist independently |
| Direction | Can be unidirectional or bidirectional |
| Strength | Weak |
| UML notation | Solid line with no diamond |
| Think | "knows about" / "is connected to" |
3. Aggregation
Definition: A special form of association where one class is the whole and another is a part, but the part can exist independently of the whole. It is a "has-a" with weak ownership.
Real-world analogy: A Department has Employees — if the department is dissolved, the employees still exist (they can join other departments).
┌────────────┐ has ┌────────────┐
│ Department │ ◇──────────────── │ Employee │
└────────────┘ └────────────┘
whole (hollow part
diamond)
If Department is destroyed, Employees SURVIVE.
Code Example
class Employee {
private name: string;
private employeeId: string;
constructor(name: string, employeeId: string) {
this.name = name;
this.employeeId = employeeId;
}
getName(): string { return this.name; }
getEmployeeId(): string { return this.employeeId; }
}
class Department {
private name: string;
private employees: Employee[]; // Department HAS employees (aggregation)
constructor(name: string) {
this.name = name;
this.employees = [];
}
// Employees are PASSED IN — not created by the Department
addEmployee(employee: Employee): void {
this.employees.push(employee);
}
removeEmployee(employeeId: string): Employee | undefined {
const index = this.employees.findIndex(
e => e.getEmployeeId() === employeeId
);
if (index === -1) return undefined;
return this.employees.splice(index, 1)[0];
// Employee is returned — it still exists
}
getEmployeeCount(): number {
return this.employees.length;
}
}
// Employees exist independently of departments
const alice = new Employee("Alice", "E001");
const bob = new Employee("Bob", "E002");
const engineering = new Department("Engineering");
engineering.addEmployee(alice);
engineering.addEmployee(bob);
// Department is "destroyed" — employees survive
const removedAlice = engineering.removeEmployee("E001");
console.log(removedAlice?.getName()); // "Alice" — she still exists
// alice can join another department
const marketing = new Department("Marketing");
marketing.addEmployee(alice);
Key Characteristics of Aggregation
| Property | Value |
|---|---|
| Lifecycle coupling | Weak — parts survive the whole's destruction |
| Ownership | Weak — whole references parts but does not create/destroy them |
| Direction | Whole → Part |
| UML notation | Solid line with hollow diamond (◇) on the whole side |
| Think | "has-a" where parts can exist alone |
4. Composition
Definition: A strong form of aggregation where the whole owns the part and the part cannot exist without the whole. If the whole is destroyed, all its parts are destroyed too.
Real-world analogy: A House has Rooms — if the house is demolished, the rooms cease to exist. You cannot have a floating room without a house.
┌────────────┐ owns ┌────────────┐
│ House │ ◆──────────────── │ Room │
└────────────┘ └────────────┘
whole (filled part
diamond)
If House is destroyed, ALL Rooms are destroyed with it.
Code Example
class Room {
private name: string;
private area: number; // in square meters
constructor(name: string, area: number) {
this.name = name;
this.area = area;
}
getName(): string { return this.name; }
getArea(): number { return this.area; }
}
class House {
private address: string;
private rooms: Room[]; // House OWNS rooms (composition)
constructor(address: string) {
this.address = address;
this.rooms = [];
}
// Rooms are CREATED by the House — not passed in from outside
addRoom(name: string, area: number): void {
const room = new Room(name, area); // House creates the room
this.rooms.push(room);
}
getTotalArea(): number {
return this.rooms.reduce((sum, room) => sum + room.getArea(), 0);
}
getRoomCount(): number {
return this.rooms.length;
}
// When House is destroyed (garbage collected), all rooms go with it.
// There is no method to "extract" a room and use it elsewhere.
}
const myHouse = new House("123 Main St");
myHouse.addRoom("Living Room", 30);
myHouse.addRoom("Bedroom", 20);
myHouse.addRoom("Kitchen", 15);
console.log(myHouse.getTotalArea()); // 65
console.log(myHouse.getRoomCount()); // 3
// If myHouse is set to null / garbage collected, all rooms are gone
// There is no way to access those Room objects independently
Another Example — Order and OrderLineItem
class OrderLineItem {
private productId: string;
private productName: string;
private quantity: number;
private unitPrice: number;
constructor(productId: string, productName: string, quantity: number, unitPrice: number) {
this.productId = productId;
this.productName = productName;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
getSubtotal(): number {
return this.quantity * this.unitPrice;
}
}
class Order {
private orderId: string;
private lineItems: OrderLineItem[]; // Composition — items belong to this order
private createdAt: Date;
constructor(orderId: string) {
this.orderId = orderId;
this.lineItems = [];
this.createdAt = new Date();
}
// Order CREATES the line items internally
addItem(productId: string, productName: string, quantity: number, unitPrice: number): void {
const item = new OrderLineItem(productId, productName, quantity, unitPrice);
this.lineItems.push(item);
}
getTotal(): number {
return this.lineItems.reduce((sum, item) => sum + item.getSubtotal(), 0);
}
getItemCount(): number {
return this.lineItems.length;
}
// If the Order is deleted, all OrderLineItems are deleted.
// An OrderLineItem has NO meaning outside its Order.
}
Key Characteristics of Composition
| Property | Value |
|---|---|
| Lifecycle coupling | Strong — parts die with the whole |
| Ownership | Strong — whole creates and destroys parts |
| Direction | Whole → Part (exclusive) |
| UML notation | Solid line with filled diamond (◆) on the whole side |
| Think | "owns-a" where parts cannot exist alone |
5. Aggregation vs Composition — Deep Comparison
This is one of the most commonly confused distinctions in LLD interviews.
| Dimension | Aggregation (◇) | Composition (◆) |
|---|---|---|
| Lifecycle | Part survives when whole is destroyed | Part is destroyed when whole is destroyed |
| Ownership | Weak — whole references parts | Strong — whole owns parts exclusively |
| Creation | Parts are typically created externally and passed in | Parts are typically created by the whole |
| Sharing | A part can belong to multiple wholes | A part belongs to exactly one whole |
| Real-world | Department ◇── Employee | House ◆── Room |
| Real-world | Playlist ◇── Song | Order ◆── OrderLineItem |
| Real-world | University ◇── Professor | Human ◆── Heart |
| Code signal | Constructor receives objects / add(existingObj) | Constructor/method creates objects internally |
| Destruction | remove() returns the part (still alive) | delete whole = parts gone (no return) |
The Litmus Test
Ask yourself two questions:
1. Can the PART exist without the WHOLE?
YES → Aggregation (Employee without Department = still a person)
NO → Composition (Room without House = meaningless)
2. Can the PART belong to MULTIPLE wholes at the same time?
YES → Aggregation (A Song can be in multiple Playlists)
NO → Composition (An OrderLineItem belongs to ONE Order)
Side-by-Side Code Pattern
// AGGREGATION pattern — parts passed in from outside
class Playlist {
private songs: Song[] = [];
addSong(song: Song): void { // Song created elsewhere, passed in
this.songs.push(song);
}
removeSong(songId: string): Song | undefined {
const idx = this.songs.findIndex(s => s.getId() === songId);
if (idx === -1) return undefined;
return this.songs.splice(idx, 1)[0]; // Song returned — still alive
}
}
// COMPOSITION pattern — parts created internally
class Invoice {
private lineItems: InvoiceLineItem[] = [];
addLineItem(description: string, amount: number): void {
// InvoiceLineItem created HERE by Invoice — not passed in
const item = new InvoiceLineItem(description, amount);
this.lineItems.push(item);
}
// No method to "extract" a line item for use elsewhere
// When Invoice is gone, line items are gone
}
6. Dependency
Definition: The weakest relationship. Class A uses Class B temporarily (in a method parameter, local variable, or return type) but does NOT hold a persistent reference to it.
Real-world analogy: You use a taxi to get somewhere. You do not own the taxi, you do not keep a reference to it. The taxi exists before you call it and after you leave.
┌────────────┐ ┌────────────┐
│ Order │ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄► │ Logger │
└────────────┘ uses └────────────┘
(dashed arrow)
Order USES Logger temporarily — does not store a reference to it.
Code Example
class Logger {
static log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
class EmailService {
static sendEmail(to: string, subject: string, body: string): boolean {
console.log(`Sending email to ${to}: ${subject}`);
return true;
}
}
class OrderProcessor {
// OrderProcessor DEPENDS on Logger and EmailService
// but does NOT hold a reference to them as properties
processOrder(order: Order): void {
// Logger used as a local dependency — not a field
Logger.log(`Processing order ${order.getId()}`);
// Validate
if (!this.validateOrder(order)) {
Logger.log(`Order ${order.getId()} validation failed`);
return;
}
// Process payment
this.chargePayment(order);
// EmailService used temporarily
EmailService.sendEmail(
order.getCustomerEmail(),
"Order Confirmed",
`Your order ${order.getId()} has been confirmed.`
);
Logger.log(`Order ${order.getId()} processed successfully`);
}
private validateOrder(order: Order): boolean {
return order.getItemCount() > 0;
}
private chargePayment(order: Order): void {
// payment logic
}
}
// OrderProcessor does NOT store Logger or EmailService as fields
// It uses them temporarily within methods — that is dependency
Dependency vs Association
| Aspect | Dependency (┄┄►) | Association (────) |
|---|---|---|
| Duration | Temporary (within a method call) | Persistent (stored as a field) |
| Storage | No field reference | Has a field reference |
| Coupling | Very weak | Weak to moderate |
| Code signal | Method parameter, local variable, static call | Class property / field |
7. Inheritance (IS-A)
Definition: A child class inherits all properties and methods from a parent class and can add or override them. This represents the "is-a" relationship.
┌────────────────┐
│ Vehicle │
│ ────────────── │
│ speed: number │
│ fuel: number │
│ ────────────── │
│ accelerate() │
│ brake() │
└───────┬────────┘
│ extends
┌────────┴────────┐
│ │
┌─────▼──────┐ ┌────▼────────┐
│ Car │ │ Motorcycle │
│ ────────── │ │ ─────────── │
│ doors: 4 │ │ sidecar: no│
│ ────────── │ │ ─────────── │
│ openTrunk()│ │ wheelie() │
└────────────┘ └─────────────┘
Car IS-A Vehicle. Motorcycle IS-A Vehicle.
Both inherit speed, fuel, accelerate(), brake().
Code Example
class Vehicle {
protected speed: number = 0;
protected fuel: number = 100;
protected brand: string;
constructor(brand: string) {
this.brand = brand;
}
accelerate(amount: number): void {
if (this.fuel <= 0) {
console.log("No fuel!");
return;
}
this.speed += amount;
this.fuel -= amount * 0.1;
}
brake(amount: number): void {
this.speed = Math.max(0, this.speed - amount);
}
getInfo(): string {
return `${this.brand} — Speed: ${this.speed}, Fuel: ${this.fuel.toFixed(1)}`;
}
}
class Car extends Vehicle {
private doors: number;
constructor(brand: string, doors: number = 4) {
super(brand);
this.doors = doors;
}
openTrunk(): void {
console.log(`${this.brand} trunk opened`);
}
// Override parent method for car-specific behavior
accelerate(amount: number): void {
const limited = Math.min(amount, 200 - this.speed); // Car speed limit
super.accelerate(limited);
}
}
class Motorcycle extends Vehicle {
constructor(brand: string) {
super(brand);
}
wheelie(): void {
if (this.speed > 30) {
console.log(`${this.brand} does a wheelie!`);
} else {
console.log("Need more speed for a wheelie");
}
}
}
const myCar = new Car("Toyota", 4);
myCar.accelerate(80); // Inherited + overridden method
myCar.openTrunk(); // Car-specific method
console.log(myCar.getInfo()); // Inherited method
const myBike = new Motorcycle("Ducati");
myBike.accelerate(50); // Inherited method
myBike.wheelie(); // Motorcycle-specific method
UML Notation for Inheritance
┌──────────┐
│ Parent │
└─────▲────┘
│ (solid line + hollow triangle arrow)
│ pointing FROM child TO parent
┌─────┴────┐
│ Child │
└──────────┘
8. Implementation (Interface Realization)
Definition: A class promises to provide the behavior defined by an interface. The class must implement all methods declared in the interface.
┌──────────────────┐
│ <<interface>> │
│ Printable │
│ ──────────────── │
│ print(): void │
└────────▲─────────┘
┆ (dashed line + hollow triangle arrow)
┆ pointing FROM class TO interface
┌────────┴─────────┐
│ Invoice │
│ ──────────────── │
│ print(): void │ ← must implement this
└──────────────────┘
Code Example
// Interface — the contract
interface Searchable {
search(query: string): SearchResult[];
indexDocument(doc: Document): void;
removeFromIndex(docId: string): void;
}
interface Cacheable {
getFromCache(key: string): unknown | null;
setInCache(key: string, value: unknown, ttlSeconds: number): void;
invalidateCache(key: string): void;
}
// A class can implement MULTIPLE interfaces
class ProductCatalog implements Searchable, Cacheable {
private products: Map<string, Product> = new Map();
private cache: Map<string, { value: unknown; expiresAt: number }> = new Map();
private searchIndex: Map<string, string[]> = new Map();
// Must implement ALL Searchable methods
search(query: string): SearchResult[] {
const matchingIds = this.searchIndex.get(query.toLowerCase()) ?? [];
return matchingIds
.map(id => this.products.get(id))
.filter(Boolean)
.map(p => ({ id: p!.id, title: p!.title, score: 1.0 }));
}
indexDocument(doc: Document): void {
const words = doc.content.toLowerCase().split(/\s+/);
words.forEach(word => {
const existing = this.searchIndex.get(word) ?? [];
existing.push(doc.id);
this.searchIndex.set(word, existing);
});
}
removeFromIndex(docId: string): void {
this.searchIndex.forEach((ids, key) => {
this.searchIndex.set(key, ids.filter(id => id !== docId));
});
}
// Must implement ALL Cacheable methods
getFromCache(key: string): unknown | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return null;
}
return entry.value;
}
setInCache(key: string, value: unknown, ttlSeconds: number): void {
this.cache.set(key, {
value,
expiresAt: Date.now() + ttlSeconds * 1000,
});
}
invalidateCache(key: string): void {
this.cache.delete(key);
}
}
Inheritance vs Implementation
| Aspect | Inheritance (extends) | Implementation (implements) |
|---|---|---|
| Keyword | extends | implements |
| What is inherited | Properties + methods + implementation | Only the contract (method signatures) |
| Count | Single parent only (in most languages) | Multiple interfaces allowed |
| UML arrow | Solid line + hollow triangle (▷) | Dashed line + hollow triangle (▷) |
| Purpose | Code reuse + specialization | Contract compliance + polymorphism |
| Think | "is a type of" | "can do / fulfills the contract of" |
9. Multiplicity (Cardinality)
Multiplicity describes how many instances of one class relate to instances of another class.
Common Multiplicity Notations
| Notation | Meaning | Example |
|---|---|---|
1 | Exactly one | A Person has exactly 1 Passport |
0..1 | Zero or one (optional) | A Student has 0 or 1 Advisor |
* or 0..* | Zero or more | A Customer has 0 or more Orders |
1..* | One or more | An Order has at least 1 LineItem |
m..n | Between m and n | A Car has 3..5 Passengers |
One-to-One (1:1)
┌──────────┐ 1 1 ┌──────────┐
│ Person │ ──────────────── │ Passport │
└──────────┘ └──────────┘
Each Person has exactly one Passport.
Each Passport belongs to exactly one Person.
class Passport {
private passportNumber: string;
private expiryDate: Date;
constructor(passportNumber: string, expiryDate: Date) {
this.passportNumber = passportNumber;
this.expiryDate = expiryDate;
}
getNumber(): string { return this.passportNumber; }
}
class Person {
private name: string;
private passport: Passport; // Exactly one passport
constructor(name: string, passport: Passport) {
this.name = name;
this.passport = passport;
}
getPassportNumber(): string {
return this.passport.getNumber();
}
}
One-to-Many (1:N)
┌──────────┐ 1 * ┌──────────┐
│ Customer │ ──────────────── │ Order │
└──────────┘ └──────────┘
One Customer can have many Orders.
Each Order belongs to exactly one Customer.
class Order {
private orderId: string;
private total: number;
constructor(orderId: string, total: number) {
this.orderId = orderId;
this.total = total;
}
getId(): string { return this.orderId; }
}
class Customer {
private name: string;
private orders: Order[]; // Zero or more orders
constructor(name: string) {
this.name = name;
this.orders = [];
}
placeOrder(order: Order): void {
this.orders.push(order);
}
getOrderCount(): number {
return this.orders.length;
}
getTotalSpent(): number {
return this.orders.reduce((sum, o) => sum + o.getTotal(), 0);
}
}
Many-to-Many (M:N)
┌──────────┐ * * ┌──────────┐
│ Student │ ──────────────── │ Course │
└──────────┘ └──────────┘
A Student can enroll in many Courses.
A Course can have many Students.
Many-to-many relationships are often resolved with a junction/association class:
┌──────────┐ 1 * ┌─────────────┐ * 1 ┌──────────┐
│ Student │ ────────── │ Enrollment │ ────────── │ Course │
└──────────┘ │ │ └──────────┘
│ enrollDate │
│ grade │
└─────────────┘
class Course {
private courseId: string;
private title: string;
constructor(courseId: string, title: string) {
this.courseId = courseId;
this.title = title;
}
getId(): string { return this.courseId; }
getTitle(): string { return this.title; }
}
class Student {
private studentId: string;
private name: string;
constructor(studentId: string, name: string) {
this.studentId = studentId;
this.name = name;
}
getId(): string { return this.studentId; }
getName(): string { return this.name; }
}
// Junction class resolves the many-to-many relationship
class Enrollment {
private student: Student;
private course: Course;
private enrollDate: Date;
private grade: string | null;
constructor(student: Student, course: Course) {
this.student = student;
this.course = course;
this.enrollDate = new Date();
this.grade = null;
}
assignGrade(grade: string): void {
this.grade = grade;
}
getStudent(): Student { return this.student; }
getCourse(): Course { return this.course; }
getGrade(): string | null { return this.grade; }
}
// University manages enrollments
class University {
private enrollments: Enrollment[] = [];
enroll(student: Student, course: Course): Enrollment {
const enrollment = new Enrollment(student, course);
this.enrollments.push(enrollment);
return enrollment;
}
getStudentsInCourse(courseId: string): Student[] {
return this.enrollments
.filter(e => e.getCourse().getId() === courseId)
.map(e => e.getStudent());
}
getCoursesForStudent(studentId: string): Course[] {
return this.enrollments
.filter(e => e.getStudent().getId() === studentId)
.map(e => e.getCourse());
}
}
10. Master Comparison Table
| Relationship | Symbol | Direction | Lifecycle Tied? | Strength | Code Signal |
|---|---|---|---|---|---|
| Dependency | ┄┄┄► | A uses B | No | Weakest | Method parameter, local variable, static call |
| Association | ───── | A knows B | No | Weak | Field reference, both exist independently |
| Aggregation | ◇──── | A has B | No (part survives) | Medium | Field reference, parts passed in from outside |
| Composition | ◆──── | A owns B | Yes (part dies) | Strong | Field reference, parts created internally |
| Inheritance | ──▷ | B is-a A | N/A | Strong | extends keyword |
| Implementation | ┄┄▷ | B can-do A | N/A | Medium | implements keyword |
Quick Decision Flowchart
Does Class A reference Class B at all?
├── NO → No relationship (or possibly Dependency if used in a method)
└── YES
├── Is it stored as a FIELD?
│ ├── NO → DEPENDENCY (temporary use)
│ └── YES
│ ├── Does A CREATE B internally?
│ │ ├── YES → Does B make sense without A?
│ │ │ ├── NO → COMPOSITION (◆)
│ │ │ └── YES → AGGREGATION (◇)
│ │ └── NO (B passed in) → ASSOCIATION or AGGREGATION
│ │ └── Is A a "whole" and B a "part"?
│ │ ├── YES → AGGREGATION (◇)
│ │ └── NO → ASSOCIATION
├── Does B extend A? → INHERITANCE
└── Does B implement A? → IMPLEMENTATION
11. Real-World Example — E-Commerce System
┌─────────────────────────────────────────────────────────────────────┐
│ E-COMMERCE CLASS RELATIONSHIPS │
│ │
│ ┌───────────────┐ │
│ │ <<interface>> │ │
│ │ PaymentMethod │◄┄┄┄┄┄┄┄┄┄┄┄┄┐ │
│ └───────────────┘ ┆ implements │
│ ┌──────┴──────┐ │
│ │ CreditCard │ │
│ │ PayPal │ │
│ │ Crypto │ │
│ └─────────────┘ │
│ │
│ ┌──────────┐ 1 * ┌──────────┐ ◆ 1..* ┌───────────────┐ │
│ │ Customer │──────────│ Order │◆─────────│ OrderLineItem │ │
│ └──────────┘ └────┬─────┘ └───────────────┘ │
│ │ │
│ │ ┄┄┄► uses │
│ │ │
│ ┌────▼──────────┐ │
│ │ PaymentMethod │ (dependency — used in │
│ └───────────────┘ processPayment method) │
│ │
│ ┌──────────┐ ◇ * ┌──────────┐ │
│ │ Category │◇──────────│ Product │ (aggregation — product │
│ └──────────┘ └──────────┘ can exist without category) │
│ │
│ ┌──────────┐ │
│ │ Product │──▷ BaseEntity (inheritance — Product IS-A │
│ │ Customer │──▷ BaseEntity BaseEntity with id, createdAt) │
│ │ Order │──▷ BaseEntity │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────────────┘
12. Key Takeaways
- Association = "knows about" — both objects exist independently. The weakest stored-reference relationship.
- Aggregation = "has-a" with weak ownership — parts survive the whole's destruction. Parts can belong to multiple wholes.
- Composition = "owns-a" with strong ownership — parts are destroyed with the whole. Parts belong to exactly one whole.
- Dependency = "uses temporarily" — no persistent reference. The weakest relationship overall.
- Inheritance = "is-a" — child reuses parent's code and can override behavior. Use sparingly; prefer composition for flexibility.
- Implementation = "can-do" — a class promises to fulfill an interface contract. Enables polymorphism with multiple interfaces.
- Multiplicity describes how many instances relate (1:1, 1:N, M:N). Many-to-many is often resolved with a junction class.
- The aggregation vs composition litmus test: Can the part exist without the whole? If no, it is composition.
Explain-It Challenge
Draw on paper (or describe verbally) the class relationships for a Music Streaming App: User, Playlist, Song, Artist, Album, Subscription. Label each relationship as association, aggregation, composition, or inheritance. Then justify each choice — "Why composition here and not aggregation?"
Previous → 9.1.b — OOP Fundamentals Next → 9.1.d — UML Diagrams