Object Oriented Programming

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:

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

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:

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 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:

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 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:

Here’s how I implemented it:

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

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.