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:
from dataclasses import dataclass from datetime import datetime
@dataclass classTask: """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
defmark_complete(self) -> None: """Marks the task as complete with current timestamp.""" self.completed_at = datetime.now() ifself.subtasks: for subtask inself.subtasks: subtask.mark_complete() @property defis_complete(self) -> bool: """Checks if the task and all subtasks are complete.""" ifnotself.completed_at: returnFalse ifself.subtasks: returnall(subtask.is_complete for subtask inself.subtasks) returnTrue
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:
classAnimal(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
defmake_sound(self) -> str: """Returns the sound this animal makes""" return"..." @abstractmethod defmove(self) -> None: """Defines how the animal moves. This method must be implemented by all concrete subclasses. """ pass
classDog(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 deffetch(self, item: str) -> None: """Simulates the dog fetching an item.""" print(f"{self.name} fetched the {item}!")
defmove(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:
classNotificationService(Protocol): """Protocol defining the interface for notification services.""" defnotify(self, message: str) -> None: """Sends a notification with the given message.""" pass
classTaskManager: """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] = [] defadd_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}") defcomplete_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.""" returnnext((task for task inself.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:
classObserver(ABC): """Abstract base class for observers in the notification system.""" @abstractmethod defupdate(self, event: str) -> None: """Handle an update from the subject.""" pass
classSubject(ABC): """Abstract base class for subjects that can be observed.""" def__init__(self): self._observers: set[Observer] = set() defattach(self, observer: Observer) -> None: """Adds an observer to the notification list.""" self._observers.add(observer)
defdetach(self, observer: Observer) -> None: """Removes an observer from the notification list.""" self._observers.discard(observer) defnotify(self, event: str) -> None: """Notifies all observers of an event.""" for observer inself._observers: observer.update(event)
classTaskSubject(Subject): """Concrete subject for task-related notifications.""" defcreate_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:
Inheritance should model “is-a” relationships, but composition often leads to more flexible designs.
The power of OOP lies not in its ability to represent objects, but in its capacity to model their interactions.
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!