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:

Quick Start

  1. Understand what objects and classes are
  2. Learn each of the four pillars with examples
  3. Practice with real-world scenarios
  4. 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

PillarPurposeKey Benefit
EncapsulationBundling data and methods togetherData protection and hiding complexity
AbstractionHiding implementation detailsSimplification and reduced complexity
InheritanceCreating new classes from existing onesCode reusability and relationships
PolymorphismMany forms of the same interfaceFlexibility 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

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

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

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

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

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

When to Use Inheritance

Use when:

Avoid when:


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

Runtime Polymorphism (Method Overriding):

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

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:

Abstraction:

Inheritance:

Polymorphism:

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 +=