Welcome to Day 4! Up until now, we’ve focused on individual classes, encapsulation, and inheritance hierarchies. Today, we step into system architecture by exploring how independent objects interact and relate to one another in memory.
Understanding these four core relationships allows you to model complex real-world domains without forcing everything into rigid inheritance trees.
1. The Relationship Spectrum 📊
Object relationships differ primarily by coupling strength and lifecycle dependency (whether one object controls the creation and destruction of another).
┌─────────────────────────────────────────────────────────────────────────────┐
│ OBJECT RELATIONSHIPS │
├─────────────┬─────────────┬──────────────────────┬──────────────────────────┤
│ Relationship│ Link Type │ Lifecycle Bound? │ Reference Storage │
├─────────────┼─────────────┼──────────────────────┼──────────────────────────┤
│ Dependency │ Uses-a │ No (Temporary scope) │ Passed as method arg │
│ Association │ Knows-a │ No (Independent) │ Stored field reference │
│ Aggregation │ Has-a (Weak)│ No (Independent) │ Stored reference list │
│ Composition │ Has-a(Strong│ Yes (Parent controls)│ Created inside parent │
└─────────────┴─────────────┴──────────────────────┴──────────────────────────┘
2. Breaking Down the 4 Relationships 🛠️
1. Dependency ("Uses-a")
The weakest relationship. Object A uses Object B temporarily to complete an operation (e.g., as a parameter in a method). Object A does not store a persistent reference to Object B.
class Printer:
def print_document(self, content: str) -> None:
print(f"🖨️ Printing: {content}")
class Report:
def __init__(self, title: str):
self.title = title
# Dependency: Report uses Printer temporarily inside a method
def export(self, printer: Printer) -> None:
printer.print_document(f"Report Title: {self.title}")
2. Association ("Knows-a")
A structural connection where two objects communicate, but neither owns the other. Both have independent lifecycles.
class Student:
def __init__(self, name: str):
self.name = name
class Teacher:
def __init__(self, name: str):
self.name = name
self.students: list[Student] = [] # Association: Knows about students
def assign_student(self, student: Student) -> None:
self.students.append(student)
3. Aggregation ("Has-a" — Weak Ownership)
A specialized form of association representing a whole-to-part relationship. The container object holds references to component objects, but if the container is destroyed, the components continue to exist independently.
class Book:
def __init__(self, title: str):
self.title = title
class Library:
def __init__(self, name: str, books: list[Book]):
self.name = name
self.books = books # Aggregation: Books exist outside the Library
# Books exist independently first
b1 = Book("Clean Code")
lib = Library("City Central", [b1])
# If 'lib' is deleted, 'b1' still exists in memory
del lib
print(b1.title) # Output: Clean Code
4. Composition ("Has-a" — Strong Ownership)
The tightest relationship. The parent object owns and manages the lifecycle of the child object. The child is instantiated inside the parent and cannot exist without it. If the parent is destroyed, all child components are destroyed too.
class BuildingRoom:
def __init__(self, room_number: str):
self.room_number = room_number
class Building:
def __init__(self, name: str):
self.name = name
# Composition: Rooms are created directly INSIDE Building
self.rooms = [BuildingRoom("101"), BuildingRoom("102")]
# Rooms are completely tied to the lifecycle of Building
campus_building = Building("Science Hall")
3. Practice Challenge: University Management System 🎓
Let me walk you through a complete University Management System that combines all four object relationships in a single, cohesive architecture.
┌─────────────────────────────────────────────────────────────────────────────┐
│ UNIVERSITY SYSTEM ARCHITECTURE │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────┐ ┌─────────────────┐
│ University │ ──(Composition)─►│ Department │
└────────────────┘ └────────┬────────┘
│
(Aggregation)
│
▼
┌────────────────┐ ┌─────────────────┐
│ Student │ ──(Association)─►│ Professor │
└───────┬────────┘ └─────────────────┘
│
(Dependency)
│
▼
┌────────────────┐
│TranscriptPrinter│
└────────────────┘
Step 1: Initialize Your Workspace
uv init oop_day4 && cd oop_day4
touch university_system.py
Step 2: Implement university_system.py
# university_system.py
from typing import List, Optional
# ==========================================
# 1. INDEPENDENT DOMAIN ENTITIES
# ==========================================
class Professor:
"""Independent entity used in Aggregation."""
def __init__(self, prof_id: str, name: str, specialization: str):
self.prof_id = prof_id
self.name = name
self.specialization = specialization
def __repr__(self) -> str:
return f"Prof. {self.name} ({self.specialization})"
class Student:
"""Independent entity used in Association and Dependency."""
def __init__(self, student_id: str, name: str, gpa: float):
self.student_id = student_id
self.name = name
self.gpa = gpa
# Association: Student maintains references to advisor(s)
self.advisor: Optional[Professor] = None
def assign_advisor(self, professor: Professor) -> None:
"""Establishes an Association with a Professor."""
self.advisor = professor
print(f"🤝 [Association] {self.name} is now advised by {professor.name}")
# ==========================================
# 2. DEPENDENCY COMPONENT
# ==========================================
class TranscriptPrinter:
"""Utility service that demonstrates Dependency ("Uses-a")."""
@staticmethod
def print_transcript(student: Student) -> None:
"""Dependency: Receives Student as a temporary method argument."""
print("\n==================================================")
print(f" OFFICIAL TRANSCRIPT: {student.name.upper()} ")
print("==================================================")
print(f" Student ID : {student.student_id}")
print(f" Current GPA: {student.gpa:.2f}")
advisor_str = student.advisor.name if student.advisor else "None Assigned"
print(f" Advisor : {advisor_str}")
print("--------------------------------------------------\n")
# ==========================================
# 3. AGGREGATION & COMPOSITION CONTAINERS
# ==========================================
class Department:
"""Component used in Composition by University, and Aggregation with Professors."""
def __init__(self, name: str):
self.name = name
# Aggregation: Professors exist independently outside Department
self.faculty: List[Professor] = []
def add_faculty(self, professor: Professor) -> None:
"""Aggregation: Accepts externally created Professor objects."""
self.faculty.append(professor)
print(f"📚 [Aggregation] {professor.name} joined the {self.name} Department")
class University:
"""Root container illustrating Composition ("Has-a" Strong)."""
def __init__(self, name: str):
self.name = name
# Composition: Departments are instantiated INSIDE University
# Their lifecycle is strictly tied to this University instance
self.departments: List[Department] = [
Department("Computer Science"),
Department("Mathematics"),
Department("Physics")
]
def get_department(self, name: str) -> Optional[Department]:
for dept in self.departments:
if dept.name.lower() == name.lower():
return dept
return None
def display_structure(self) -> None:
print(f"\n🏛️ University: {self.name}")
print("--------------------------------------------------")
for dept in self.departments:
print(f" 🏢 Department: {dept.name}")
if dept.faculty:
for prof in dept.faculty:
print(f" └─ Faculty: {prof}")
else:
print(" └─ Faculty: [No staff assigned]")
# ==========================================
# TEST EXECUTION SUITE
# ==========================================
if __name__ == "__main__":
# --- 1. COMPOSITION DEMO ---
# Creating University automatically instantiates its Departments
mit = University("Massachusetts Institute of Technology")
# --- 2. AGGREGATION DEMO ---
# Create Professors independently (they exist on their own)
prof_knuth = Professor("P-101", "Donald Knuth", "Algorithms")
prof_euler = Professor("P-102", "Leonhard Euler", "Number Theory")
# Aggregate professors into departments
cs_dept = mit.get_department("Computer Science")
math_dept = mit.get_department("Mathematics")
if cs_dept:
cs_dept.add_faculty(prof_knuth)
if math_dept:
math_dept.add_faculty(prof_euler)
mit.display_structure()
# --- 3. ASSOCIATION DEMO ---
# Student and Professor know about each other without ownership
student_alice = Student("S-9001", "Alice Hopper", 3.95)
student_alice.assign_advisor(prof_knuth)
# --- 4. DEPENDENCY DEMO ---
# TranscriptPrinter temporarily processes student_alice
TranscriptPrinter.print_transcript(student_alice)
# --- LIFECYCLE CHECK ---
print("Checking Lifecycles:")
print(f" • Deleting University instance '{mit.name}'...")
del mit
# 'mit' is gone, but prof_knuth still exists in memory!
print(f" ✓ Professor {prof_knuth.name} still exists independently: {prof_knuth}")
Step 3: Run & Verify
uv run university_system.py
Output Summary
📚 [Aggregation] Donald Knuth joined the Computer Science Department
📚 [Aggregation] Leonhard Euler joined the Mathematics Department
🏛️ University: Massachusetts Institute of Technology
--------------------------------------------------
🏢 Department: Computer Science
└─ Faculty: Prof. Donald Knuth (Algorithms)
🏢 Department: Mathematics
└─ Faculty: Prof. Leonhard Euler (Number Theory)
🏢 Department: Physics
└─ Faculty: [No staff assigned]
🤝 [Association] Alice Hopper is now advised by Donald Knuth
==================================================
OFFICIAL TRANSCRIPT: ALICE HOPPER
==================================================
Student ID : S-9001
Current GPA: 3.95
Advisor : Donald Knuth
--------------------------------------------------
Checking Lifecycles:
• Deleting University instance 'Massachusetts Institute of Technology'...
✓ Professor Donald Knuth still exists independently: Prof. Donald Knuth (Algorithms)
Top comments (0)