I spent two semesters learning object oriented programming and honestly, it was a struggle at first. The syntax wasn’t the hard part. What broke my brain was learning to think differently about how programs work. Instead of writing step by step instructions, I had to start thinking about objects that talk to each other.
Changing How I Think About Code
Coming from procedural programming, I was used to thinking “first do this, then do that, then do this other thing.” Object oriented programming flipped that around. Now I had to think “what things exist in my program, and how do they interact?”
Here’s an example that really helped it click for me:
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
Instead of asking “How do I track task completion?” I started asking “What does it mean to be a Task, and what should Tasks be able to do?”
Why Inheritance Actually Makes Sense
Let’s look at a classic example that finally made inheritance click for me:
This shows how you can model real world relationships in code. Here’s how it works:
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 Are Actually Useful
We kept hearing about these SOLID principles, and at first they seemed like academic nonsense. But they actually help you write better code. Take the Single Responsibility Principle: each class should have one job.
Here’s how I applied it to clean up my task management system:
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 Show Up Naturally
One thing that surprised me was how design patterns aren’t just academic exercises. They actually solve real problems you run into. The Observer pattern, for example, is perfect for notification systems:
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}")
What I Actually Learned
A few things that really stuck with me:
Inheritance vs Composition: Just because you can use inheritance doesn’t mean you should. Sometimes it’s better to have objects contain other objects rather than inherit from them.
Object interactions matter more than objects themselves: The real power isn’t in modeling individual things, but in how those things work together.
Design patterns emerge naturally: You don’t need to memorize them. When you run into a problem, the pattern that solves it will make sense.
The biggest lesson was that good object oriented design takes practice. You have to write bad code first to understand why the good practices exist.
I got to apply a lot of these concepts when building Recess Chess with some classmates. Modeling chess pieces, game states, and player moves really drove home how useful object oriented thinking can be for complex systems.
If you’re learning OOP and it feels overwhelming, stick with it. The concepts will start clicking once you build a few projects and see how everything fits together.