Object-Oriented Programming: Complete Guide

Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects rather than functions. It's the foundation of languages like Java, C++, Python, and JavaScript.

Why OOP?

OOP helps you write code that is:

  • Modular: Break complex systems into manageable pieces
  • Reusable: Write once, use many times through inheritance
  • Maintainable: Changes in one place don't break others
  • Intuitive: Model real-world entities naturally

The Four Pillars of OOP

  1. Encapsulation: Bundle data and methods together
  2. Inheritance: Create new classes from existing ones
  3. Polymorphism: Same interface, different implementations
  4. Abstraction: Hide complexity, expose simplicity

Classes & Objects

A class is a blueprint. An object is an instance of that blueprint.

Defining a Class

# Python
class Dog:
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

    def bark(self):
        return f"{self.name} says woof!"

# Create objects (instances)
buddy = Dog("Buddy", "Golden Retriever")
max = Dog("Max", "German Shepherd")

print(buddy.bark())  # "Buddy says woof!"
print(max.name)      # "Max"

JavaScript Class

class Dog {
    constructor(name, breed) {
        this.name = name;
        this.breed = breed;
    }

    bark() {
        return `${this.name} says woof!`;
    }
}

const buddy = new Dog("Buddy", "Golden Retriever");
console.log(buddy.bark());  // "Buddy says woof!"

Encapsulation

Encapsulation bundles data (attributes) and methods that operate on that data within a class, and restricts direct access to some components.

Private vs Public

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner        # Public
        self.__balance = balance  # Private (convention: __)

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

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

    def get_balance(self):
        return self.__balance  # Controlled access

# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)
print(account.get_balance())  # 1500

# Can't access directly
# account.__balance  # AttributeError

Benefits

  • Protect internal state from invalid modifications
  • Change implementation without affecting external code
  • Validate data before changes

Inheritance

Inheritance allows a class to inherit attributes and methods from another class.

Basic Inheritance

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

    def speak(self):
        pass  # To be implemented by subclasses

class Dog(Animal):
    def speak(self):
        return f"{self.name} says woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says meow!"

# Usage
dog = Dog("Buddy")
cat = Cat("Whiskers")

print(dog.speak())  # "Buddy says woof!"
print(cat.speak())  # "Whiskers says meow!"

Using super()

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

class Manager(Employee):
    def __init__(self, name, salary, department):
        super().__init__(name, salary)  # Call parent constructor
        self.department = department

    def give_raise(self, employee, amount):
        employee.salary += amount

manager = Manager("Alice", 80000, "Engineering")
print(manager.name)        # "Alice"
print(manager.department)  # "Engineering"

When to Use Inheritance

  • "Is-a" relationship: A Dog IS AN Animal
  • Shared behavior: Multiple classes share common methods
  • Prefer composition over inheritance when in doubt

Polymorphism

Polymorphism means "many forms." The same interface can have different implementations.

Method Overriding

class Shape:
    def area(self):
        raise NotImplementedError

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

# Polymorphism in action
shapes = [Rectangle(4, 5), Circle(3)]
for shape in shapes:
    print(shape.area())  # Different implementation, same interface

Duck Typing (Python)

# "If it walks like a duck and quacks like a duck, it's a duck"
class Duck:
    def speak(self):
        return "Quack!"

class Person:
    def speak(self):
        return "Hello!"

def make_speak(thing):
    print(thing.speak())  # Works with any object that has speak()

make_speak(Duck())    # "Quack!"
make_speak(Person())  # "Hello!"

Abstraction

Abstraction hides complex implementation details and exposes only what's necessary.

Abstract Classes

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def connect(self):
        pass

    @abstractmethod
    def query(self, sql):
        pass

class PostgreSQL(Database):
    def connect(self):
        print("Connecting to PostgreSQL...")

    def query(self, sql):
        print(f"Executing: {sql}")

class MongoDB(Database):
    def connect(self):
        print("Connecting to MongoDB...")

    def query(self, sql):
        print(f"Converting SQL to MongoDB query: {sql}")

# Can't instantiate abstract class
# db = Database()  # TypeError

# Must use concrete implementation
db = PostgreSQL()
db.connect()
db.query("SELECT * FROM users")

SOLID Principles

SOLID is a set of five design principles for writing maintainable OOP code.

S - Single Responsibility Principle

A class should have only one reason to change.

# Bad: Multiple responsibilities
class User:
    def save_to_database(self): ...
    def generate_report(self): ...
    def send_email(self): ...

# Good: Single responsibility
class User:
    def __init__(self, name, email): ...

class UserRepository:
    def save(self, user): ...

class UserReportGenerator:
    def generate(self, user): ...

O - Open/Closed Principle

Open for extension, closed for modification.

L - Liskov Substitution Principle

Subclasses should be substitutable for their base classes.

I - Interface Segregation Principle

Many specific interfaces are better than one general interface.

D - Dependency Inversion Principle

Depend on abstractions, not concretions.

# Bad: Depends on concrete class
class OrderService:
    def __init__(self):
        self.db = MySQLDatabase()  # Tightly coupled

# Good: Depends on abstraction
class OrderService:
    def __init__(self, database: Database):
        self.db = database  # Can use any Database implementation

Design Patterns

Common solutions to recurring design problems.

Singleton

Ensure only one instance of a class exists.

class DatabaseConnection:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

# Both variables point to the same instance
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True

Factory

Create objects without specifying the exact class.

class AnimalFactory:
    @staticmethod
    def create(animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        raise ValueError(f"Unknown animal: {animal_type}")

# Usage
animal = AnimalFactory.create("dog")

Observer

Notify multiple objects when state changes.

Strategy

Define a family of algorithms and make them interchangeable.

Related Guides