Object-Oriented Programming

After spending two semesters wrestling with inheritance hierarchies and polymorphism, I’ve developed a newfound appreciation for the elegance of object-oriented programming (OOP). While the basics might seem straightforward, the deeper I dove into OOP concepts, the more I realized how profoundly they shape modern software architecture.

The Mental Model Shift

Coming from a procedural programming background, the hardest part wasn’t learning the syntax – it was rewiring my brain to think in terms of objects and messages. Instead of seeing programs as sequences of instructions, I started viewing them as communities of objects collaborating to solve problems.
Here’s a simple example that transformed my understanding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from dataclasses import dataclass
from datetime import datetime

@dataclass
class Task:
"""Represents a single task in our task management system.

Attributes:
title: The name of the task
created_at: Timestamp when task was created
completed_at: Timestamp when task was completed, if any
subtasks: List of smaller tasks that compose this task
"""
title: str
created_at: datetime
completed_at: datetime | None = None
subtasks: list['Task'] | None = None

def mark_complete(self) -> None:
"""Marks the task as complete with current timestamp."""
self.completed_at = datetime.now()
if self.subtasks:
for subtask in self.subtasks:
subtask.mark_complete()

@property
def is_complete(self) -> bool:
"""Checks if the task and all subtasks are complete."""
if not self.completed_at:
return False
if self.subtasks:
return all(subtask.is_complete for subtask in self.subtasks)
return True

This code represents a fundamental shift from thinking “How do I track task completion?” to “What does it mean to be a Task, and how should Tasks behave?”

The Power of Abstraction

Let’s visualize how objects can model real-world relationships:

This hierarchy demonstrates one of the most powerful aspects of OOP: the ability to model complex relationships while hiding implementation details. Let’s implement this in Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from abc import ABC, abstractmethod

class Animal(ABC):
"""Abstract base class representing any animal.

Attributes:
name: The animal's name
age: The animals's age in years
"""
def __init__(self, name: str, age: int):
self.name = name
self.age = age

def make_sound(self) -> str:
"""Returns the sound this animal makes"""
return "..."

@abstractmethod
def move(self) -> None:
"""Defines how the animal moves.

This method must be implemented by all concrete subclasses.
"""
pass

class Dog(Animal):
"""Represents a dog with breed-specific behaviors.

Attributes:
breed: The dog's breed
"""
def __init__(self, name: str, age: int, breed: str):
super().__init__(name, age)
self.breed = breed

def fetch(self, item: str) -> None:
"""Simulates the dog fetching an item."""
print(f"{self.name} fetched the {item}!")

def move(self) -> None:
"""Implements the move method for dogs."""
print(f"{self.name} runs on four legs.")

The SOLID Principles in Practice

Throughout the course, we kept returning to the SOLID principles. Initially, they seemed like abstract guidelines, but they’ve become invaluable tools in my design process.

Let’s enhance our task management system to better follow the Single Responsibility Principle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from typing import Protocol
from task import Task

class NotificationService(Protocol):
"""Protocol defining the interface for notification services."""
def notify(self, message: str) -> None:
"""Sends a notification with the given message."""
pass

class TaskManager:
"""Handles operations and state management for tasks.

This class follows the Single Responsibility Principle by focusing
solely on task management logic.

Attributes:
notification_service: Service used to send notifications
tasks: List of managed tasks
"""
def __init__(self, notification_service: NotificationService) -> None:
self.notification_service = notification_service
self.tasks: list[Task] = []

def add_task(self, task: Task) -> None:
"""Adds a new task and notifies relevant parties."""
self.tasks.append(task)
self.notification_service.notify(f"New task created: {task.title}")

def complete_task(self, task_title: str) -> None:
"""Marks a task as complete and handles notifications."""
task = self._find_task(task_title)
if task:
task.mark_complete()
self.notification_service.notify(f"Task Completed: {task.title}")

def _find_task(self, title: str) -> Task | None:
"""Internal helper to find a task by title."""
return next((task for task in self.tasks if task.title == title), None)

Design Patterns: Beyond Theory

One of my favorite discoveries was how design patterns emerge naturally when solving real problems. Take the Observer pattern, which we can use to create a flexible task notification system:

Here’s the implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from abc import ABC, abstractmethod

class Observer(ABC):
"""Abstract base class for observers in the notification system."""
@abstractmethod
def update(self, event: str) -> None:
"""Handle an update from the subject."""
pass

class Subject(ABC):
"""Abstract base class for subjects that can be observed."""
def __init__(self):
self._observers: set[Observer] = set()

def attach(self, observer: Observer) -> None:
"""Adds an observer to the notification list."""
self._observers.add(observer)

def detach(self, observer: Observer) -> None:
"""Removes an observer from the notification list."""
self._observers.discard(observer)

def notify(self, event: str) -> None:
"""Notifies all observers of an event."""
for observer in self._observers:
observer.update(event)

class TaskSubject(Subject):
"""Concrete subject for task-related notifications."""
def create_task(self, title: str) -> None:
"""Creates a new task and notifies observers."""
# Task creation logic here
self.notify(f"Task created: {title}")

Reflections and Lessons Learned

The journey from understanding basic class definitions to implementing complex design patterns has been transformative. OOP isn’t just about organizing code – it’s about modeling the world in a way that makes complex systems manageable.

Some key insights I’ve gained:

  1. Inheritance should model “is-a” relationships, but composition often leads to more flexible designs.
  2. The power of OOP lies not in its ability to represent objects, but in its capacity to model their interactions.
  3. Design patterns aren’t solutions to memorize, but tools that emerge from applying good design principles.

Looking back, I can see how each concept builds on the previous ones, creating a rich toolkit for solving complex problems. The challenge now is to apply these principles thoughtfully, avoiding over-engineering while leveraging OOP’s strengths when they truly add value.

As a practical example, I applied these OOP principles in developing Recess Chess, an online chess application built with fellow students at NTNU, where object-oriented design helped us model the complex interactions between chess pieces, game states, and player moves.

I’m excited to continue exploring these concepts in my future projects. If you’ve had similar experiences or different insights about OOP, I’d love to hear about them!