Object-Oriented Programming

Welcome to MindMentor!

Object-Oriented Programming

Computer Science

How Do You Model the Real World in Code?

You're building a game. You have players, enemies, weapons, items. Each has properties (health, damage, position). Each can perform actions (move, attack, defend).

How do you organize this in code?

You could use separate variables for everything: player1_health = 100, player1_x = 50, player1_y = 30, player2_health = 100, player2_x = 80, player2_y = 20, enemy1_health = 50, enemy1_x = 100, ...

This gets messy fast. Hundreds of variables. Hard to track. Hard to modify. Hard to understand.

Object-Oriented Programming (OOP) solves this. Instead of thinking in isolated variables and functions, you think in objects—things that have properties and behaviors, just like real-world objects.

A player is an object. It has health. It has a position. It can move. It can attack.

An enemy is an object. Same properties and behaviors.

OOP organizes code the way humans naturally think about the world—in terms of things and what they do.

Understanding OOP changes everything. You start to see programs as collections of interacting objects. You recognize relationships between components. You design reusable, maintainable systems.

This isn't just a programming technique. It's a fundamental way of thinking about software design—used in virtually every modern application.

Let's explore how to model the world in code.

Classes and Objects

What Are Classes and Objects? Definition: A class is a blueprint or template that defines the structure and behavior of objects. An object is a specific instance of a class.

Analogy: Class: Cookie cutter (the template); Object: Individual cookies (made from template). The cookie cutter defines the shape. But you can make many cookies from one cutter. Each cookie is a separate object.

Defining a Class

Example: Student class

class Student:
def __init__(self, name, student_id, major):
self.name = name
self.student_id = student_id
self.major = major
self.gpa = 0.0

def study(self, hours):
print(f"{self.name} studied for {hours} hours")

def get_info(self):
return f"Student: {self.name}, ID: {self.student_id}, Major: {self.major}, GPA: {self.gpa}"

Class components: Attributes (properties): name, student_id, major, gpa; Methods (behaviors): study(), get_info()

Creating Objects (Instances)

alice = Student("Alice Johnson", "S12345", "Computer Science")
bob = Student("Bob Smith", "S12346", "Mathematics")

print(alice.name) # Alice Johnson
print(bob.name) # Bob Smith

alice.study(3) # Alice Johnson studied for 3 hours
bob.study(2) # Bob Smith studied for 2 hours

alice.gpa = 3.8
bob.gpa = 3.5

print(alice.get_info())
# Student: Alice Johnson, ID: S12345, Major: Computer Science, GPA: 3.8

Key point: Alice and Bob are separate objects. Changing Alice's GPA doesn't affect Bob's.

The "self" Parameter: "self" refers to the current instance of the class, allowing access to its attributes and methods. When you call alice.introduce(), self refers to alice. When you call bob.introduce(), self refers to bob.

Constructors

Definition: A constructor is a special method that initializes a new object when it's created, setting up initial state and performing setup tasks.

The init Method

In Python, __init__ is the constructor:

class BankAccount:
def __init__(self, account_number, owner, initial_balance=0):
self.account_number = account_number
self.owner = owner
self.balance = initial_balance
print(f"Account created for {owner}")

def deposit(self, amount):
self.balance += amount

def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
return True
return False

account1 = BankAccount("ACC001", "Alice", 1000)
account2 = BankAccount("ACC002", "Bob")

Constructor Overloading: Some languages (like Java, C++) allow multiple constructors. Python doesn't have constructor overloading, but achieves similar effect with default parameters.

Static vs Non-Static Members

Non-Static (Instance) Members

Definition: Instance members belong to specific objects. Each object has its own copy.

class Car:
def __init__(self, brand, model):
self.brand = brand # Instance variable
self.model = model # Instance variable
self.odometer = 0 # Instance variable

def drive(self, miles):
self.odometer += miles

car1 = Car("Toyota", "Camry")
car2 = Car("Honda", "Civic")
car1.drive(100)
car2.drive(50)
print(car1.odometer) # 100
print(car2.odometer) # 50

Static (Class) Members

Definition: Static members belong to the class itself, not individual objects. All objects share the same static member.

class Car:
total_cars = 0 # Static variable (class variable)

def __init__(self, brand, model):
self.brand = brand
self.model = model
Car.total_cars += 1

@staticmethod
def get_total_cars():
return Car.total_cars

car1 = Car("Toyota", "Camry")
car2 = Car("Honda", "Civic")
car3 = Car("Ford", "Mustang")
print(Car.total_cars) # 3
print(Car.get_total_cars()) # 3

Use cases for static members: Constants (PI shared by all circles), Counters (track total objects created), Utility functions (operations that don't need object data).

Encapsulation

Definition: Encapsulation is the bundling of data and methods that operate on that data within a class, restricting direct access to some components to protect object integrity.

Public vs Private

Python convention (weak encapsulation): Public: self.name; Private (by convention): self._name (single underscore); Strongly private: self.__name (double underscore, name mangling)

class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Public
self._account_number = "..." # Protected (convention)
self.__balance = balance # Private

def deposit(self, amount):
if amount > 0:
self.__balance += amount

def get_balance(self):
return self.__balance

account = BankAccount("Alice", 1000)
print(account.owner) # OK (public)
print(account.get_balance()) # OK (via method)
# print(account.__balance) # ERROR! Private

Why Encapsulation? Data validation (can validate age before setting), Flexibility (can change internal implementation without breaking code), Security (hide sensitive data from external access).

Inheritance

Definition: Inheritance is a mechanism where a new class (subclass/child) is derived from an existing class (superclass/parent), inheriting its attributes and methods while potentially adding or modifying behavior.

Basic Inheritance

class Animal:
def __init__(self, name, age):
self.name = name
self.age = age

def make_sound(self):
print("Some generic animal sound")

def sleep(self):
print(f"{self.name} is sleeping")

class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age)
self.breed = breed

def make_sound(self):
print("Woof! Woof!")

def fetch(self):
print(f"{self.name} is fetching the ball")

dog = Dog("Buddy", 3, "Golden Retriever")
dog.sleep() # Buddy is sleeping
dog.make_sound() # Woof! Woof!
dog.fetch() # Buddy is fetching the ball

The "super()" Function: super() calls methods from the parent class, typically used in constructors and when extending parent behavior.

Benefits of Inheritance: Code reuse (don't rewrite common functionality), Logical hierarchy (models real-world relationships), Polymorphism (treat different objects similarly), Extensibility (add features without modifying existing code).

Polymorphism

Definition: Polymorphism is the ability of different objects to respond to the same method call in their own way, allowing one interface to be used for different underlying forms. Literally: "Many shapes"

Polymorphism Through Inheritance

class Shape:
def area(self):
pass

class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height

class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2

shapes = [Rectangle(5,10), Circle(7), Rectangle(3,8)]
for shape in shapes:
print(f"Area: {shape.area()}")

Benefits of Polymorphism: Flexibility (add new types without changing existing code), Simplified code (single interface for multiple types), Extensibility (new subclasses integrate seamlessly), Maintainability (changes localized to specific classes).

Abstract Classes

Definition: An abstract class is a class that cannot be instantiated and is designed to be inherited by subclasses that implement its abstract methods. Purpose: Define a contract—what methods subclasses must implement.

Creating Abstract Classes

from abc import ABC, abstractmethod

class Animal(ABC):
def __init__(self, name):
self.name = name

@abstractmethod
def make_sound(self):
pass

@abstractmethod
def move(self):
pass

def sleep(self):
print(f"{self.name} is sleeping")

class Dog(Animal):
def make_sound(self):
return "Woof!"
def move(self):
return "Running on four legs"

dog = Dog("Buddy")
print(dog.make_sound()) # Woof!
dog.sleep() # Buddy is sleeping

Why Abstract Classes? Enforce interface (guarantee subclasses implement required methods), Documentation (clear contract), Prevent instantiation (can't create incomplete objects), Shared functionality (provide common implementation).

Composition and Aggregation

Composition: "Has-A" Relationship (Strong)

Definition: Composition is a strong "has-a" relationship where objects are composed of other objects, and the contained objects cannot exist independently.

class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
print("Engine started")

class Car:
def __init__(self, brand, model, horsepower):
self.brand = brand
self.model = model
self.engine = Engine(horsepower) # Car HAS-A Engine

car = Car("Toyota", "Camry", 200)
car.engine.start() # Engine started

Key characteristic: Engine is part of Car. If Car is destroyed, Engine is destroyed too.

Aggregation: Weaker "Has-A" Relationship

Definition: Aggregation is a weaker "has-a" relationship where objects are associated but can exist independently.

class Employee:
def __init__(self, name):
self.name = name

class Department:
def __init__(self, name):
self.name = name
self.employees = []
def add_employee(self, employee):
self.employees.append(employee)

alice = Employee("Alice")
it_dept = Department("IT")
it_dept.add_employee(alice) # Department HAS employees

Key characteristic: Employees can exist without Department.

Composition vs Aggregation: Composition has strong relationship where contained object dies with container; Aggregation has weak relationship where contained object can exist independently. Examples: Car-Engine (composition), Department-Employees (aggregation).

Design Patterns

Definition: Design patterns are reusable solutions to commonly occurring problems in software design, providing templates for how to solve problems in various contexts.

Singleton Pattern

Problem: Need exactly one instance of a class (e.g., configuration manager, database connection).

class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.connected = False
return cls._instance

db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True (same object)

Use cases: Configuration managers, logging, caching, database connections.

Factory Pattern

Problem: Create objects without specifying exact class.

class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()

animal = AnimalFactory.create_animal("dog")

Use cases: Object creation based on configuration, plugin systems, GUI components.

Observer Pattern

Problem: Multiple objects need to be notified when something changes.

class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def notify(self, message):
for observer in self._observers:
observer.update(message)

class Observer:
def __init__(self, name):
self.name = name
def update(self, message):
print(f"{self.name} received: {message}")

Use cases: Event systems, UI updates, publish-subscribe systems, notifications.

Strategy Pattern

Problem: Select algorithm at runtime.

class SortStrategy:
def sort(self, data):
pass

class Sorter:
def __init__(self, strategy):
self.strategy = strategy
def sort_data(self, data):
return self.strategy.sort(data)

sorter = Sorter(BubbleSort())
sorter.sort_data(data)

Use cases: Payment methods, compression algorithms, route finding.

Putting It All Together

You started wondering how to model the real world in code.

Now you understand.

Classes define blueprints—structure and behavior. Objects are instances with their own data.

Constructors initialize objects, setting up initial state when created.

Static members belong to the class itself, shared by all instances. Instance members belong to individual objects.

Encapsulation bundles data and methods, hiding internal details and protecting integrity through access control.

Inheritance creates hierarchies—child classes inherit from parents, promoting code reuse and logical relationships.

Polymorphism allows different objects to respond to the same interface in their own way—one method call, many behaviors.

Abstract classes define contracts that subclasses must fulfill, preventing incomplete instantiation.

Composition (strong ownership) and aggregation (weak association) model "has-a" relationships with different lifetimes.

Design patterns provide proven solutions—Singleton for single instances, Factory for object creation, Observer for notifications, Strategy for interchangeable algorithms.

Every modern application uses OOP—games with player objects, banking systems with account objects, websites with user objects, operating systems with process objects.

Understanding OOP changes how you think about software design. You're no longer writing scattered functions and variables. You're creating organized systems that model reality.