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
- Encapsulation: Bundle data and methods together
- Inheritance: Create new classes from existing ones
- Polymorphism: Same interface, different implementations
- 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.