Object-Oriented Programming: The Four Pillars
Overview
Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around data, or objects, rather than functions and logic. An object contains both data (attributes) and code (methods) that manipulate that data.
The Four Pillars: Encapsulation, Abstraction, Inheritance, and Polymorphism
Why OOP? Code reusability, modularity, flexibility, maintainability, and real-world modeling.
Getting Started
Prerequisites
Before you begin, ensure you have:
- Basic programming knowledge in any language
- Understanding of variables, functions, and data types
- Familiarity with classes and objects (helpful but not required)
- A development environment for your chosen language
Quick Start
- Understand what objects and classes are
- Learn each of the four pillars with examples
- Practice with real-world scenarios
- Build small projects applying OOP principles
What is Object-Oriented Programming?
Core Concepts
Object: An instance of a class containing data and behavior.
Class: A blueprint or template for creating objects.
Basic Example (Python):
# Define a class
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def bark(self):
return f"{self.name} says Woof!"
# Create objects
my_dog = Dog("Buddy", "Golden Retriever")
your_dog = Dog("Max", "Labrador")
print(my_dog.bark()) # Output: Buddy says Woof!
Basic Example (JavaScript):
// Define a class
class Dog {
constructor(name, breed) {
this.name = name;
this.breed = breed;
}
bark() {
return `${this.name} says Woof!`;
}
}
// Create objects
const myDog = new Dog("Buddy", "Golden Retriever");
console.log(myDog.bark()); // Output: Buddy says Woof!
The Four Pillars Overview
| Pillar | Purpose | Key Benefit |
|---|---|---|
| Encapsulation | Bundling data and methods together | Data protection and hiding complexity |
| Abstraction | Hiding implementation details | Simplification and reduced complexity |
| Inheritance | Creating new classes from existing ones | Code reusability and relationships |
| Polymorphism | Many forms of the same interface | Flexibility and extensibility |
Pillar 1: Encapsulation
Definition
Encapsulation is the bundling of data (attributes) and methods that operate on that data into a single unit (class), while restricting direct access to some components.
Key Concepts
- Data Hiding: Keep internal state private
- Access Modifiers: Control visibility (public, private, protected)
- Getters and Setters: Controlled access to private data
Python Example
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number # Public
self.__balance = balance # Private (name mangling with __)
# Getter method
def get_balance(self):
return self.__balance
# Setter method with validation
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 __str__(self):
return f"Account {self.account_number}: ${self.__balance}"
# Usage
account = BankAccount("12345", 1000)
# Direct access to private variable (not recommended, but possible in Python)
# account.__balance = 999999 # This won't work as expected
# Proper way to interact
account.deposit(500)
print(account.get_balance()) # Output: 1500
account.withdraw(200)
print(account) # Output: Account 12345: $1300
Java Example
public class BankAccount {
private String accountNumber; // Private
private double balance; // Private
// Constructor
public BankAccount(String accountNumber, double balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
// Getter
public double getBalance() {
return balance;
}
// Setter with validation
public boolean deposit(double amount) {
if (amount > 0) {
balance += amount;
return true;
}
return false;
}
public boolean withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
@Override
public String toString() {
return "Account " + accountNumber + ": $" + balance;
}
}
Benefits of Encapsulation
- Data Protection: Prevents unauthorized access
- Validation: Control how data is modified
- Flexibility: Change internal implementation without affecting external code
- Maintainability: Easier to debug and update
Real-World Analogy
Think of a car: you interact with the steering wheel, pedals, and gear shift (public interface), but you don’t directly manipulate the engine internals (private implementation).
Pillar 2: Abstraction
Definition
Abstraction is the process of hiding complex implementation details and showing only the essential features of an object. It focuses on what an object does rather than how it does it.
Key Concepts
- Abstract Classes: Cannot be instantiated, serve as base classes
- Interfaces: Contracts that define what methods a class must implement
- Hide Complexity: Show only relevant information
Python Example with Abstract Classes
from abc import ABC, abstractmethod
# Abstract base class
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount):
pass
@abstractmethod
def refund(self, transaction_id):
pass
# Concrete method (shared by all subclasses)
def validate_amount(self, amount):
return amount > 0
# Concrete implementation for Credit Card
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount):
if self.validate_amount(amount):
return f"Processing ${amount} via Credit Card"
return "Invalid amount"
def refund(self, transaction_id):
return f"Refunding transaction {transaction_id} to Credit Card"
# Concrete implementation for PayPal
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount):
if self.validate_amount(amount):
return f"Processing ${amount} via PayPal"
return "Invalid amount"
def refund(self, transaction_id):
return f"Refunding transaction {transaction_id} to PayPal"
# Usage
def checkout(processor: PaymentProcessor, amount: float):
print(processor.process_payment(amount))
# Client code doesn't need to know implementation details
credit_card = CreditCardProcessor()
paypal = PayPalProcessor()
checkout(credit_card, 100.00) # Processing $100.0 via Credit Card
checkout(paypal, 50.00) # Processing $50.0 via PayPal
Java Interface Example
// Interface defining contract
interface PaymentProcessor {
boolean processPayment(double amount);
boolean refund(String transactionId);
}
// Concrete implementation
class CreditCardProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing $" + amount + " via Credit Card");
return true;
}
@Override
public boolean refund(String transactionId) {
System.out.println("Refunding " + transactionId + " to Credit Card");
return true;
}
}
class PayPalProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing $" + amount + " via PayPal");
return true;
}
@Override
public boolean refund(String transactionId) {
System.out.println("Refunding " + transactionId + " to PayPal");
return true;
}
}
// Usage
public class Main {
public static void checkout(PaymentProcessor processor, double amount) {
processor.processPayment(amount);
}
public static void main(String[] args) {
PaymentProcessor cc = new CreditCardProcessor();
PaymentProcessor pp = new PayPalProcessor();
checkout(cc, 100.00);
checkout(pp, 50.00);
}
}
Benefits of Abstraction
- Simplification: Focus on what matters
- Reduced Complexity: Hide unnecessary details
- Flexibility: Easy to add new implementations
- Code Organization: Clear contracts and responsibilities
Real-World Analogy
When you drive a car, you use abstract controls (steering wheel, pedals). You don’t need to know how the fuel injection system or transmission works internally.
Pillar 3: Inheritance
Definition
Inheritance is a mechanism where a new class (child/derived/subclass) inherits properties and behaviors from an existing class (parent/base/superclass), allowing code reuse and establishing relationships.
Key Concepts
- Parent Class (Superclass): The class being inherited from
- Child Class (Subclass): The class that inherits
- IS-A Relationship: Subclass IS-A type of superclass
- Method Overriding: Subclass provides specific implementation
Python Example
# Parent class
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
return f"{self.name} is eating"
def sleep(self):
return f"{self.name} is sleeping"
def make_sound(self):
return "Some generic sound"
# Child class 1
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age) # Call parent constructor
self.breed = breed
# Override parent method
def make_sound(self):
return f"{self.name} barks: Woof!"
# New method specific to Dog
def fetch(self):
return f"{self.name} is fetching the ball"
# Child class 2
class Cat(Animal):
def __init__(self, name, age, indoor):
super().__init__(name, age)
self.indoor = indoor
# Override parent method
def make_sound(self):
return f"{self.name} meows: Meow!"
# New method specific to Cat
def scratch(self):
return f"{self.name} is scratching"
# Usage
dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2, True)
print(dog.eat()) # Inherited: Buddy is eating
print(dog.make_sound()) # Overridden: Buddy barks: Woof!
print(dog.fetch()) # New method: Buddy is fetching the ball
print(cat.sleep()) # Inherited: Whiskers is sleeping
print(cat.make_sound()) # Overridden: Whiskers meows: Meow!
print(cat.scratch()) # New method: Whiskers is scratching
JavaScript Example
// Parent class
class Vehicle {
constructor(brand, model, year) {
this.brand = brand;
this.model = model;
this.year = year;
}
start() {
return `${this.brand} ${this.model} is starting`;
}
stop() {
return `${this.brand} ${this.model} is stopping`;
}
getInfo() {
return `${this.year} ${this.brand} ${this.model}`;
}
}
// Child class 1
class Car extends Vehicle {
constructor(brand, model, year, doors) {
super(brand, model, year);
this.doors = doors;
}
// Override
start() {
return `${super.start()} with ${this.doors} doors`;
}
// New method
honk() {
return "Beep beep!";
}
}
// Child class 2
class Motorcycle extends Vehicle {
constructor(brand, model, year, type) {
super(brand, model, year);
this.type = type;
}
// Override
start() {
return `${this.type} ${super.start()}`;
}
// New method
wheelie() {
return "Performing a wheelie!";
}
}
// Usage
const car = new Car("Toyota", "Camry", 2024, 4);
const bike = new Motorcycle("Harley", "Street 750", 2024, "Cruiser");
console.log(car.start()); // Toyota Camry is starting with 4 doors
console.log(car.honk()); // Beep beep!
console.log(bike.start()); // Cruiser Harley Street 750 is starting
console.log(bike.wheelie()); // Performing a wheelie!
Multi-Level Inheritance
class LivingBeing:
def breathe(self):
return "Breathing..."
class Animal(LivingBeing):
def move(self):
return "Moving..."
class Dog(Animal):
def bark(self):
return "Woof!"
# Dog inherits from Animal, which inherits from LivingBeing
dog = Dog()
print(dog.breathe()) # From LivingBeing
print(dog.move()) # From Animal
print(dog.bark()) # From Dog
Benefits of Inheritance
- Code Reusability: Don’t repeat common code
- Extensibility: Easy to add new features
- Hierarchical Classification: Natural modeling of relationships
- Maintenance: Update parent class, all children benefit
When to Use Inheritance
✅ Use when:
- There’s a clear IS-A relationship
- You need to share common behavior
- Subclasses are specializations of the parent
❌ Avoid when:
- There’s only a HAS-A relationship (use composition)
- Inheritance tree becomes too deep
- You’re forcing unnatural relationships
Pillar 4: Polymorphism
Definition
Polymorphism means “many forms.” It allows objects of different classes to be treated as objects of a common parent class, and enables methods to behave differently based on the object calling them.
Types of Polymorphism
Compile-time Polymorphism (Method Overloading):
- Same method name, different parameters
- Resolved at compile time
Runtime Polymorphism (Method Overriding):
- Same method signature in parent and child
- Resolved at runtime
Python Example - Method Overriding
class Shape:
def __init__(self, color):
self.color = color
def area(self):
pass
def describe(self):
return f"A {self.color} shape"
class Circle(Shape):
def __init__(self, color, radius):
super().__init__(color)
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Triangle(Shape):
def __init__(self, color, base, height):
super().__init__(color)
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
# Polymorphism in action
def print_shape_info(shape: Shape):
print(f"{shape.describe()}")
print(f"Area: {shape.area()}")
print("-" * 30)
# All different objects, same interface
shapes = [
Circle("red", 5),
Rectangle("blue", 4, 6),
Triangle("green", 3, 4)
]
for shape in shapes:
print_shape_info(shape)
# Output:
# A red shape
# Area: 78.53975
# ------------------------------
# A blue shape
# Area: 24
# ------------------------------
# A green shape
# Area: 6.0
# ------------------------------
Java Example - Polymorphism with Interfaces
interface Drawable {
void draw();
double getArea();
}
class Circle implements Drawable {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing a circle");
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Drawable {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
@Override
public double getArea() {
return width * height;
}
}
// Polymorphic behavior
public class Main {
public static void renderShape(Drawable shape) {
shape.draw();
System.out.println("Area: " + shape.getArea());
}
public static void main(String[] args) {
Drawable[] shapes = {
new Circle(5),
new Rectangle(4, 6)
};
for (Drawable shape : shapes) {
renderShape(shape);
}
}
}
Method Overloading Example (Compile-time Polymorphism)
class Calculator:
def add(self, a, b=None, c=None):
if b is not None and c is not None:
return a + b + c
elif b is not None:
return a + b
else:
return a
calc = Calculator()
print(calc.add(5)) # 5
print(calc.add(5, 3)) # 8
print(calc.add(5, 3, 2)) # 10
class Calculator {
// Method overloading - same name, different parameters
public int add(int a, int b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
public double add(double a, double b) {
return a + b;
}
}
Benefits of Polymorphism
- Flexibility: Write code that works with multiple types
- Extensibility: Add new types without changing existing code
- Maintainability: Single interface for multiple implementations
- Simplification: Treat different objects uniformly
Real-World Analogy
A smartphone’s charging port is polymorphic: you can plug in various chargers (USB-C, wireless, etc.), and each implements the “charging” behavior differently, but all charge your phone.
All Four Pillars Together
Complete Example: E-commerce System
from abc import ABC, abstractmethod
from datetime import datetime
# ABSTRACTION: Abstract base class
class PaymentMethod(ABC):
@abstractmethod
def process_payment(self, amount):
pass
@abstractmethod
def get_transaction_fee(self, amount):
pass
# INHERITANCE & POLYMORPHISM: Different payment implementations
class CreditCard(PaymentMethod):
def __init__(self, card_number, cvv):
# ENCAPSULATION: Private data
self.__card_number = card_number
self.__cvv = cvv
def process_payment(self, amount):
fee = self.get_transaction_fee(amount)
total = amount + fee
return f"Charged ${total:.2f} to card ending in {self.__card_number[-4:]}"
def get_transaction_fee(self, amount):
return amount * 0.029 # 2.9% fee
class PayPal(PaymentMethod):
def __init__(self, email):
self.__email = email
def process_payment(self, amount):
fee = self.get_transaction_fee(amount)
total = amount + fee
return f"Charged ${total:.2f} to PayPal account {self.__email}"
def get_transaction_fee(self, amount):
return amount * 0.034 # 3.4% fee
class Bitcoin(PaymentMethod):
def __init__(self, wallet_address):
self.__wallet_address = wallet_address
def process_payment(self, amount):
fee = self.get_transaction_fee(amount)
total = amount + fee
return f"Transferred ${total:.2f} worth of BTC to {self.__wallet_address[:10]}..."
def get_transaction_fee(self, amount):
return 2.50 # Flat fee
# ENCAPSULATION: Order class bundles data and behavior
class Order:
def __init__(self, order_id, items):
self.__order_id = order_id
self.__items = items
self.__total = sum(item['price'] for item in items)
self.__status = "pending"
self.__payment_method = None
def get_total(self):
return self.__total
def set_payment_method(self, payment_method: PaymentMethod):
self.__payment_method = payment_method
# POLYMORPHISM: Works with any PaymentMethod subclass
def checkout(self):
if not self.__payment_method:
return "No payment method selected"
result = self.__payment_method.process_payment(self.__total)
self.__status = "completed"
return f"Order {self.__order_id}: {result}"
def get_order_summary(self):
items_list = "\n".join([f" - {item['name']}: ${item['price']}"
for item in self.__items])
return f"""
Order #{self.__order_id}
Items:
{items_list}
Total: ${self.__total:.2f}
Status: {self.__status}
"""
# Usage demonstrating all four pillars
def main():
# Create an order
order = Order("ORD-001", [
{"name": "Laptop", "price": 999.99},
{"name": "Mouse", "price": 29.99}
])
print(order.get_order_summary())
# Try different payment methods (POLYMORPHISM)
payment_methods = [
CreditCard("4532123456789012", "123"),
PayPal("user@example.com"),
Bitcoin("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
]
for payment in payment_methods:
order.set_payment_method(payment)
print(order.checkout())
print()
if __name__ == "__main__":
main()
Design Patterns Using OOP Pillars
Singleton Pattern (Encapsulation)
class Database:
__instance = None
def __new__(cls):
if cls.__instance is None:
cls.__instance = super().__new__(cls)
cls.__instance.__initialized = False
return cls.__instance
def __init__(self):
if self.__initialized:
return
self.__connection = "Connected to database"
self.__initialized = True
def query(self, sql):
return f"Executing: {sql}"
# Always returns same instance
db1 = Database()
db2 = Database()
print(db1 is db2) # True
Factory Pattern (Abstraction & Polymorphism)
class Animal(ABC):
@abstractmethod
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
else:
raise ValueError("Unknown animal type")
# Usage
factory = AnimalFactory()
animals = [
factory.create_animal("dog"),
factory.create_animal("cat")
]
for animal in animals:
print(animal.speak())
Strategy Pattern (All Four Pillars)
from abc import ABC, abstractmethod
# Strategy interface (Abstraction)
class SortStrategy(ABC):
@abstractmethod
def sort(self, data):
pass
# Concrete strategies (Inheritance & Polymorphism)
class BubbleSort(SortStrategy):
def sort(self, data):
n = len(data)
for i in range(n):
for j in range(0, n-i-1):
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
return data
class QuickSort(SortStrategy):
def sort(self, data):
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
# Context class (Encapsulation)
class Sorter:
def __init__(self, strategy: SortStrategy):
self.__strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self.__strategy = strategy
def sort_data(self, data):
return self.__strategy.sort(data)
# Usage
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSort())
print(sorter.sort_data(data.copy()))
sorter.set_strategy(QuickSort())
print(sorter.sort_data(data.copy()))
Best Practices
When to Use Each Pillar
Encapsulation:
- Always use for data protection
- Provide public interfaces for interaction
- Validate data in setters
- Hide implementation details
Abstraction:
- Define clear interfaces/contracts
- Create abstract base classes for common behavior
- Focus on “what” not “how”
- Use when multiple implementations exist
Inheritance:
- Only for IS-A relationships
- Keep inheritance hierarchies shallow (2-3 levels max)
- Consider composition over inheritance when appropriate
- Use for code reuse and specialization
Polymorphism:
- Write flexible, extensible code
- Use interfaces/abstract classes as types
- Enable runtime behavior changes
- Reduce conditional logic
Common Mistakes to Avoid
❌ Breaking Encapsulation:
# Bad
class User:
def __init__(self):
self.password = "secret123" # Exposed!
# Good
class User:
def __init__(self):
self.__password = "secret123"
def verify_password(self, password):
return self.__password == password
❌ Overusing Inheritance:
# Bad - forced inheritance
class Square(Circle): # Square is NOT a Circle!
pass
# Good - composition
class Square:
def __init__(self, side):
self.side = side
❌ Violating Abstraction:
# Bad - exposing implementation
class Cache:
def __init__(self):
self.redis_client = Redis() # Implementation detail exposed
# Good - hiding implementation
class Cache:
def __init__(self):
self.__storage = Redis()
def get(self, key):
return self.__storage.get(key)
Real-World Applications
Banking System
from abc import ABC, abstractmethod
from datetime import datetime
class Account(ABC):
def __init__(self, account_number, balance=0):
self._account_number = account_number
self._balance = balance
self._transactions = []
@abstractmethod
def calculate_interest(self):
pass
def deposit(self, amount):
if amount > 0:
self._balance += amount
self._transactions.append({
'type': 'deposit',
'amount': amount,
'date': datetime.now()
})
return True
return False
def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
self._transactions.append({
'type': 'withdrawal',
'amount': amount,
'date': datetime.now()
})
return True
return False
def get_balance(self):
return self._balance
class SavingsAccount(Account):
def __init__(self, account_number, balance=0, interest_rate=0.02):
super().__init__(account_number, balance)
self.__interest_rate = interest_rate
def calculate_interest(self):
return self._balance * self.__interest_rate
class CheckingAccount(Account):
def __init__(self, account_number, balance=0, overdraft_limit=500):
super().__init__(account_number, balance)
self.__overdraft_limit = overdraft_limit
def calculate_interest(self):
return 0 # No interest on checking
def withdraw(self, amount):
if 0 < amount <= (self._balance + self.__overdraft_limit):
self._balance -= amount
self._transactions.append({
'type': 'withdrawal',
'amount': amount,
'date': datetime.now()
})
return True
return False
Game Development
class Character(ABC):
def __init__(self, name, health, attack_power):
self.name = name
self._health = health
self._max_health = health
self._attack_power = attack_power
@abstractmethod
def special_ability(self):
pass
def attack(self, target):
damage = self._attack_power
target.take_damage(damage)
return f"{self.name} attacks {target.name} for {damage} damage!"
def take_damage(self, damage):
self._health -= damage
if self._health < 0:
self._health = 0
def is_alive(self):
return self._health > 0
class Warrior(Character):
def __init__(self, name):
super().__init__(name, health=120, attack_power=25)
self.__rage = 0
def special_ability(self):
self.__rage +=