DEV Community

Thiruvengadam Sakthivel
Thiruvengadam Sakthivel

Posted on

#04 - Connecting Objects: Understanding Object Relationships in Python

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    │
└─────────────┴─────────────┴──────────────────────┴──────────────────────────┘

Enter fullscreen mode Exit fullscreen mode

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}")

Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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")

Enter fullscreen mode Exit fullscreen mode

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│
       └────────────────┘

Enter fullscreen mode Exit fullscreen mode

Step 1: Initialize Your Workspace

uv init oop_day4 && cd oop_day4
touch university_system.py

Enter fullscreen mode Exit fullscreen mode

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}")

Enter fullscreen mode Exit fullscreen mode

Step 3: Run & Verify

uv run university_system.py

Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

Top comments (0)